From e2a45565460f2ac3cd71183e9785566ac54b651e Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:22:05 +0200 Subject: [PATCH] [Filter] Restore exact, case-insensitive set code search (#7336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Filter] Restore exact, case-insensitive set code search (#7332, #7333) * [Filter] Group set query suffix modes into a subrule Address review feedback: keep the 'e'/'set' prefix in one place and move the three suffix modes (exact, negated, release-date comparison) into a dedicated choice-like rule so sv.choice() still dispatches on them. Add coverage for the release-date comparison mode. --------- Co-authored-by: Lukas BrĂ¼bach --- .../libcockatrice/filters/filter_string.cpp | 16 +++- tests/carddatabase/filter_string_test.cpp | 84 +++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index 1b63ca122..df0c70ae7 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -20,7 +20,8 @@ SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart -SetQuery <- ('e'/'set') SetExpression / ([:] FlexStringValue) +SetQuery <- ('e'/'set') SetQueryValue +SetQueryValue <- ([=:] FlexStringValue) / (<[!][=]?> FlexStringValue) / SetExpression OracleQuery <- 'o' [:] MatcherString @@ -133,8 +134,8 @@ static void setupParserRules() const auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { return matcher(x->getCardType()); }; }; - search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter { - if (sv.choice() == 1) { + search["SetQueryValue"] = [](const peg::SemanticValues &sv) -> Filter { + if (sv.choice() == 0) { auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { QList sets = x->getSets().keys(); @@ -143,6 +144,15 @@ static void setupParserRules() return std::any_of(sets.begin(), sets.end(), matchesSet); }; } + if (sv.choice() == 1) { + auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { + QList sets = x->getSets().keys(); + + auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; + return std::none_of(sets.begin(), sets.end(), matchesSet); + }; + } auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp index 2b92c65df..dcf1cf0a2 100644 --- a/tests/carddatabase/filter_string_test.cpp +++ b/tests/carddatabase/filter_string_test.cpp @@ -2,6 +2,9 @@ #include "test_card_database_path_provider.h" #include "gtest/gtest.h" +#include +#include +#include #include #include #include @@ -71,6 +74,13 @@ QUERY(Color2, cat, "c:gw", true) QUERY(Color3, cat, "c!g", true) QUERY(Color4, cat, "c!gw", false) +QUERY(SetCodeCaseInsensitive1, cat, "set:cat", true) +QUERY(SetCodeCaseInsensitive2, cat, "set:CAT", true) +QUERY(SetCodeCaseInsensitive3, cat, "set:CAt", true) +QUERY(SetCodeShortForm, cat, "e:cat", true) +QUERY(SetCodeWrongSet, cat, "set:who", false) +QUERY(SetCodeWrongSet2, doctor, "set:cat", false) + QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true) CardInfoPtr localizedCat() @@ -126,6 +136,80 @@ TEST_F(CardQuery, SearchLanguageIsBoundPerInstance) } // namespace +class SetQuery : public ::testing::Test +{ +protected: + void SetUp() override + { + // EOE and EOC share a release date, like a set and its Commander counterpart. + const QDate sharedReleaseDate(2026, 3, 13); + mainSet = CardSet::newInstance(&controller, "EOE", "Edge of Eternities", "expansion", sharedReleaseDate, + CardSet::PriorityPrimary); + commanderSet = CardSet::newInstance(&controller, "EOC", "Edge of Eternities Commander", "commander", + sharedReleaseDate, CardSet::PrioritySecondary); + oldSet = CardSet::newInstance(&controller, "OLD", "Older Set", "expansion", QDate(2020, 1, 1), + CardSet::PriorityPrimary); + + inBothSets = newCardWithPrintings({{"EOE", mainSet}, {"EOC", commanderSet}}); + onlyInCommanderSet = newCardWithPrintings({{"EOC", commanderSet}}); + onlyInOldSet = newCardWithPrintings({{"OLD", oldSet}}); + + // SetExpression resolves set codes to release dates through the global set list. + auto *database = CardDatabaseManager::getInstance(); + database->addSet(mainSet); + database->addSet(commanderSet); + database->addSet(oldSet); + } + + CardInfoPtr newCardWithPrintings(const QList> &printings) + { + SetToPrintingsMap setsInfo; + for (const auto &printing : printings) { + setsInfo[printing.first].append(PrintingInfo(printing.second)); + } + return CardInfo::newInstance("Test Card", "", false, {}, {}, {}, setsInfo, CardInfo::UiAttributes()); + } + + NoopCardSetPriorityController controller; + CardSetPtr mainSet; + CardSetPtr commanderSet; + CardSetPtr oldSet; + CardInfoPtr inBothSets; + CardInfoPtr onlyInCommanderSet; + CardInfoPtr onlyInOldSet; +}; + +TEST_F(SetQuery, ExactMatchSeparatesSameDaySets) +{ + EXPECT_TRUE(FilterString("set:EOE").check(inBothSets)); + EXPECT_TRUE(FilterString("e:EOE").check(inBothSets)); + EXPECT_TRUE(FilterString("set:EOC").check(inBothSets)); + EXPECT_FALSE(FilterString("set:EOE").check(onlyInCommanderSet)); + EXPECT_TRUE(FilterString("set:EOC").check(onlyInCommanderSet)); +} + +TEST_F(SetQuery, ExactMatchIsCaseInsensitive) +{ + EXPECT_TRUE(FilterString("set:eoe").check(inBothSets)); + EXPECT_TRUE(FilterString("set:EoE").check(inBothSets)); + EXPECT_FALSE(FilterString("set:eoe").check(onlyInCommanderSet)); +} + +TEST_F(SetQuery, NotEqualsMatchesPrintingsOutsideTheSet) +{ + EXPECT_FALSE(FilterString("set!EOE").check(inBothSets)); + EXPECT_TRUE(FilterString("set!EOE").check(onlyInCommanderSet)); +} + +TEST_F(SetQuery, ComparisonMatchesByReleaseDate) +{ + // OLD (2020) predates EOE/EOC (2026-03-13), so comparison operators still use release dates. + EXPECT_TRUE(FilterString("setEOE").check(onlyInOldSet)); + EXPECT_TRUE(FilterString("set>=EOE").check(inBothSets)); + EXPECT_FALSE(FilterString("set