mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
* [CardDB] Sort power/toughness without splitting every comparison
When the card database is sorted by the P/T column, lessThan() runs
O(n log n) times over the whole view, and each call split both strings
into a QStringList ("power/toughness") and then read the two elements
back. For the roughly 36k cards in the database that is millions of
list and string allocations, all for a value that contains at most one
separator.
The comparator now locates the single '/' with indexOf and passes the
two halves as QStringView to lessThanNumerically(), which is changed
from QString to QStringView. The numeric parsing, the "1+*" fallback and
the locale-aware tie-break are unchanged, and strings with no separator
or more than one still fall through to the plain string comparison,
exactly as split()-based size checks did before.
lessThanNumerically() also initializes its two bool out-parameters; they
were declared uninitialized, which only worked because toFloat() always
assigns them.
Compared old and new comparator over a corpus of 2024 power/toughness
strings (4096576 ordered pairs, including malformed values): zero
differences. Full build and ctest (628 tests) pass.
* Extract well-formedness checks in P/T comparator
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
302 lines
9.6 KiB
C++
302 lines
9.6 KiB
C++
#include "card_database_display_model.h"
|
||
|
||
#include "card_database_model.h"
|
||
#include "libcockatrice/card/card_localization.h"
|
||
#include "libcockatrice/filters/filter_string.h"
|
||
#include "libcockatrice/filters/filter_tree.h"
|
||
|
||
#include <QAbstractItemModel>
|
||
#include <QChar>
|
||
#include <QList>
|
||
#include <QMap>
|
||
#include <QSharedPointer>
|
||
#include <QStringList>
|
||
#include <QVariant>
|
||
#include <QtGlobal>
|
||
#include <qnamespace.h>
|
||
#include <string>
|
||
#include <utility>
|
||
|
||
class QObject;
|
||
|
||
CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
||
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr)
|
||
{
|
||
filterTree = nullptr;
|
||
setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||
setSortCaseSensitivity(Qt::CaseInsensitive);
|
||
|
||
dirtyTimer.setSingleShot(true);
|
||
connect(&dirtyTimer, &QTimer::timeout, this, [this]() {
|
||
invalidate();
|
||
emit modelDirty();
|
||
});
|
||
|
||
loadedRowCount = 0;
|
||
}
|
||
|
||
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
|
||
{
|
||
QSortFilterProxyModel::setSourceModel(model);
|
||
|
||
connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); });
|
||
|
||
connect(model, &QAbstractItemModel::rowsRemoved, this, [this]() { dirty(); });
|
||
|
||
connect(model, &QAbstractItemModel::modelReset, this, [this]() {
|
||
loadedRowCount = 0;
|
||
dirty();
|
||
});
|
||
}
|
||
|
||
QMap<wchar_t, wchar_t> CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'},
|
||
{L'”', L'\"'},
|
||
{L'‘', L'\''},
|
||
{L'’', L'\''}};
|
||
|
||
bool CardDatabaseDisplayModel::canFetchMore(const QModelIndex &index) const
|
||
{
|
||
return loadedRowCount < sourceModel()->rowCount(index);
|
||
}
|
||
|
||
void CardDatabaseDisplayModel::fetchMore(const QModelIndex &index)
|
||
{
|
||
int remainder = sourceModel()->rowCount(index) - loadedRowCount;
|
||
int itemsToFetch = qMin(100, remainder);
|
||
|
||
if (itemsToFetch == 0) {
|
||
return;
|
||
}
|
||
|
||
const auto startIndex = qMin(rowCount(QModelIndex()), loadedRowCount);
|
||
beginInsertRows(QModelIndex(), startIndex, startIndex + itemsToFetch - 1);
|
||
|
||
loadedRowCount += itemsToFetch;
|
||
endInsertRows();
|
||
}
|
||
|
||
int CardDatabaseDisplayModel::rowCount(const QModelIndex &parent) const
|
||
{
|
||
return QSortFilterProxyModel::rowCount(parent);
|
||
}
|
||
|
||
bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||
{
|
||
const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString();
|
||
const QString rightString = sourceModel()->data(right, CardDatabaseModel::SortRole).toString();
|
||
|
||
if (!cardName.isEmpty() && left.column() == CardDatabaseModel::NameColumn) {
|
||
const bool isLeftType = leftString.startsWith(cardName, Qt::CaseInsensitive);
|
||
const bool isRightType = rightString.startsWith(cardName, Qt::CaseInsensitive);
|
||
|
||
// test for an exact match: isLeftType && leftString.size() == cardName.size()
|
||
// or an exclusive start match: isLeftType && !isRightType
|
||
if (isLeftType && (!isRightType || leftString.size() == cardName.size())) {
|
||
return true;
|
||
}
|
||
|
||
// same checks for the right string
|
||
if (isRightType && (!isLeftType || rightString.size() == cardName.size())) {
|
||
return false;
|
||
}
|
||
} else if (right.column() == CardDatabaseModel::PTColumn && left.column() == CardDatabaseModel::PTColumn) {
|
||
// The comparator runs O(n log n) times while the view is sorted, so
|
||
// splitting every power/toughness string into a QStringList allocates
|
||
// a lot for data that has exactly one separator. Work on views and
|
||
// only split the two halves out when both strings are well formed.
|
||
const QStringView leftPT = leftString;
|
||
const QStringView rightPT = rightString;
|
||
const qsizetype leftSlash = leftPT.indexOf(u'/');
|
||
const qsizetype rightSlash = rightPT.indexOf(u'/');
|
||
const bool leftIsWellFormed = leftSlash >= 0 && leftPT.indexOf(u'/', leftSlash + 1) < 0;
|
||
const bool rightIsWellFormed = rightSlash >= 0 && rightPT.indexOf(u'/', rightSlash + 1) < 0;
|
||
|
||
if (leftIsWellFormed && rightIsWellFormed) {
|
||
const int powerComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash));
|
||
if (powerComparison != 0) {
|
||
return powerComparison < 0;
|
||
}
|
||
// power equal, check toughness
|
||
return lessThanNumerically(leftPT.mid(leftSlash + 1), rightPT.mid(rightSlash + 1)) < 0;
|
||
}
|
||
}
|
||
return QString::localeAwareCompare(leftString, rightString) < 0;
|
||
}
|
||
|
||
int CardDatabaseDisplayModel::lessThanNumerically(QStringView left, QStringView right)
|
||
{
|
||
if (left == right) {
|
||
return 0;
|
||
}
|
||
|
||
bool okLeft = false;
|
||
bool okRight = false;
|
||
float leftNum = left.toFloat(&okLeft);
|
||
float rightNum = right.toFloat(&okRight);
|
||
|
||
if (okLeft && okRight) {
|
||
if (leftNum < rightNum) {
|
||
return -1;
|
||
} else if (leftNum > rightNum) {
|
||
return 1;
|
||
} else {
|
||
return 0;
|
||
}
|
||
}
|
||
// try and parsing again, for weird ones like "1+*"
|
||
QStringView leftAfterNum;
|
||
QStringView rightAfterNum;
|
||
if (!okLeft) {
|
||
int leftNumIndex = 0;
|
||
for (; leftNumIndex < left.size(); leftNumIndex++) {
|
||
if (!left.at(leftNumIndex).isDigit()) {
|
||
break;
|
||
}
|
||
}
|
||
if (leftNumIndex != 0) {
|
||
leftNum = left.left(leftNumIndex).toFloat(&okLeft);
|
||
leftAfterNum = left.right(leftNumIndex);
|
||
}
|
||
}
|
||
if (!okRight) {
|
||
int rightNumIndex = 0;
|
||
for (; rightNumIndex < right.size(); rightNumIndex++) {
|
||
if (!right.at(rightNumIndex).isDigit()) {
|
||
break;
|
||
}
|
||
}
|
||
if (rightNumIndex != 0) {
|
||
rightNum = right.left(rightNumIndex).toFloat(&okRight);
|
||
rightAfterNum = right.right(rightNumIndex);
|
||
}
|
||
}
|
||
if (okLeft && okRight) {
|
||
|
||
if (leftNum != rightNum) {
|
||
// both parsed as numbers, but different number
|
||
if (leftNum < rightNum) {
|
||
return -1;
|
||
} else {
|
||
return 1;
|
||
}
|
||
} else {
|
||
// both parsed, same number, but at least one has something else
|
||
// so compare the part after the number - prefer nothing
|
||
return QString::localeAwareCompare(leftAfterNum, rightAfterNum);
|
||
}
|
||
} else if (okLeft) {
|
||
return -1;
|
||
} else if (okRight) {
|
||
return 1;
|
||
}
|
||
// couldn't parse it, just return String comparison
|
||
return QString::localeAwareCompare(left, right);
|
||
}
|
||
bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const
|
||
{
|
||
CardInfoPtr info = static_cast<CardDatabaseModel *>(sourceModel())->getCard(sourceRow);
|
||
|
||
if (((isToken == ShowTrue) && !info->getIsToken()) || ((isToken == ShowFalse) && info->getIsToken())) {
|
||
return false;
|
||
}
|
||
|
||
if (filterString != nullptr) {
|
||
if (filterTree != nullptr && !filterTree->acceptsCard(info, searchLanguage)) {
|
||
return false;
|
||
}
|
||
return filterString->check(info);
|
||
}
|
||
|
||
return rowMatchesCardName(info);
|
||
}
|
||
|
||
bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
|
||
{
|
||
if (!cardName.isEmpty()) {
|
||
const bool matchesEnglish = info->getName().contains(cardName, Qt::CaseInsensitive);
|
||
const bool matchesLocalized =
|
||
!searchLanguage.isEnglishOnly() &&
|
||
info->getLocalizedName(searchLanguage.language).contains(cardName, Qt::CaseInsensitive);
|
||
if (!matchesEnglish && !matchesLocalized) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) {
|
||
return false;
|
||
}
|
||
|
||
if (filterTree != nullptr) {
|
||
return filterTree->acceptsCard(info, searchLanguage);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
void CardDatabaseDisplayModel::clearFilterAll()
|
||
{
|
||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
||
beginFilterChange();
|
||
#endif
|
||
cardName.clear();
|
||
cardText.clear();
|
||
cardTypes.clear();
|
||
cardColors.clear();
|
||
if (filterTree != nullptr) {
|
||
filterTree->clear();
|
||
}
|
||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0))
|
||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||
#else
|
||
invalidateFilter();
|
||
#endif
|
||
}
|
||
|
||
void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree)
|
||
{
|
||
if (this->filterTree != nullptr) {
|
||
disconnect(this->filterTree, nullptr, this, nullptr);
|
||
}
|
||
|
||
this->filterTree = _filterTree;
|
||
connect(this->filterTree, &FilterTree::changed, this, &CardDatabaseDisplayModel::filterTreeChanged);
|
||
invalidate();
|
||
}
|
||
|
||
void CardDatabaseDisplayModel::setStringFilter(const QString &_src)
|
||
{
|
||
searchText = _src;
|
||
delete filterString;
|
||
filterString = new FilterString(_src, searchLanguage);
|
||
dirty();
|
||
}
|
||
|
||
void CardDatabaseDisplayModel::setSearchLanguage(const CardSearchLanguage &searchLang)
|
||
{
|
||
if (searchLanguage == searchLang) {
|
||
return;
|
||
}
|
||
|
||
searchLanguage = searchLang;
|
||
|
||
if (filterString != nullptr) {
|
||
setStringFilter(searchText);
|
||
}
|
||
dirty();
|
||
}
|
||
|
||
void CardDatabaseDisplayModel::filterTreeChanged()
|
||
{
|
||
invalidate();
|
||
}
|
||
|
||
const QString CardDatabaseDisplayModel::sanitizeCardName(const QString &dirtyName, const QMap<wchar_t, wchar_t> &table)
|
||
{
|
||
std::wstring toReturn = dirtyName.toStdWString();
|
||
for (wchar_t &ch : toReturn) {
|
||
if (table.contains(ch)) {
|
||
ch = table.value(ch);
|
||
}
|
||
}
|
||
return QString::fromStdWString(toReturn);
|
||
}
|