convenience features (#21)

This commit is contained in:
Sebastian Dine
2026-07-24 08:56:53 +02:00
committed by GitHub
parent ab0e3c5ae2
commit 7cf25d671f
22 changed files with 351 additions and 38 deletions
+1
View File
@@ -53,6 +53,7 @@ add_library(ccm_core STATIC
src/games/pokemonjp/JapanesePokemonGameModule.cpp
src/util/FsNames.cpp
src/util/SetNoNatural.cpp
)
target_include_directories(ccm_core
@@ -23,6 +23,7 @@ struct PokemonSetCompletionProgress {
PokemonRegion region{PokemonRegion::West};
std::string setId;
std::string setName;
std::string releaseDate; // YYYY/MM/DD from owned cards; may be empty
std::size_t ownedUnique{0};
std::size_t total{0};
@@ -50,8 +51,9 @@ pokemonRegionsInCollection(const std::vector<PokemonCard>& collection,
const PokemonSetCatalog& westCatalog,
const PokemonSetCatalog& asiaCatalog);
// Packs where the collection owns ≥1 matching card, ordered by setName then
// region. When regionFilter is set, only that region's catalog/cards count.
// Packs where the collection owns ≥1 matching card, ordered by releaseDate
// then setName then region. When regionFilter is set, only that region's
// catalog/cards count.
[[nodiscard]] std::vector<PokemonSetCompletionProgress>
computePokemonSetCompletion(const std::vector<PokemonCard>& collection,
const PokemonSetCatalog& westCatalog,
+14
View File
@@ -0,0 +1,14 @@
#pragma once
// Natural (alphanumeric) ordering for collector / set numbers.
// Digit runs compare as integers so "2" < "10" < "100"; non-digit runs use
// ordinary string order (e.g. "SWSH001" < "SWSH002").
#include <string_view>
namespace ccm {
// strcmp-style: <0 if a < b, 0 if equal (after natural + lex tie-break), >0 if a > b.
[[nodiscard]] int compareSetNoNatural(std::string_view a, std::string_view b) noexcept;
} // namespace ccm
+3 -1
View File
@@ -2,6 +2,7 @@
#include "ccm/games/pokemon/PokemonCardPreviewSource.hpp"
#include "ccm/util/Rfc3986.hpp"
#include "ccm/util/SetNoNatural.hpp"
#include <nlohmann/json.hpp>
@@ -93,7 +94,8 @@ Result<PokemonSetCatalogPack> PokemonSetSource::parseCatalogPackFromSetDetail(
std::sort(pack.cards.begin(), pack.cards.end(),
[](const PokemonCatalogCard& a, const PokemonCatalogCard& b) {
if (a.setNo != b.setNo) return a.setNo < b.setNo;
const int cmp = compareSetNoNatural(a.setNo, b.setNo);
if (cmp != 0) return cmp < 0;
return a.name < b.name;
});
if (pack.cards.empty()) {
@@ -2,6 +2,7 @@
#include "ccm/games/pokemonjp/JapanesePokemonCardPreviewSource.hpp"
#include "ccm/util/Rfc3986.hpp"
#include "ccm/util/SetNoNatural.hpp"
#include <nlohmann/json.hpp>
@@ -78,7 +79,8 @@ void gapFillFromEnCatalog(PokemonSetCatalogPack& pack,
void sortPackCards(PokemonSetCatalogPack& pack) {
std::sort(pack.cards.begin(), pack.cards.end(),
[](const PokemonCatalogCard& a, const PokemonCatalogCard& b) {
if (a.setNo != b.setNo) return a.setNo < b.setNo;
const int cmp = compareSetNoNatural(a.setNo, b.setNo);
if (cmp != 0) return cmp < 0;
return a.name < b.name;
});
}
+21 -5
View File
@@ -3,6 +3,7 @@
#include "ccm/games/pokemon/PokemonCardPreviewSource.hpp"
#include "ccm/games/pokemon/PokemonWestSetId.hpp"
#include "ccm/games/pokemonjp/JapanesePokemonCardPreviewSource.hpp"
#include "ccm/util/SetNoNatural.hpp"
#include <algorithm>
#include <array>
@@ -13,7 +14,12 @@ namespace ccm {
namespace {
using OwnedBySet = std::unordered_map<std::string, std::unordered_set<std::string>>;
struct OwnedSetInfo {
std::unordered_set<std::string> nos;
std::string releaseDate;
};
using OwnedBySet = std::unordered_map<std::string, OwnedSetInfo>;
bool passesLanguageFilter(const PokemonCard& card, std::optional<Language> languageFilter) {
return !languageFilter.has_value() || card.language == *languageFilter;
@@ -46,7 +52,11 @@ OwnedBySet ownedSetNosBySetId(const std::vector<PokemonCard>& collection,
if (setNo.empty()) continue;
const std::string setKey =
region == PokemonRegion::West ? westSetKey(card.set.id) : card.set.id;
out[setKey].insert(setNo);
auto& info = out[setKey];
info.nos.insert(setNo);
if (info.releaseDate.empty() && !card.set.releaseDate.empty()) {
info.releaseDate = card.set.releaseDate;
}
}
return out;
}
@@ -61,20 +71,21 @@ computeForCatalog(const std::vector<PokemonCard>& collection,
std::vector<PokemonSetCompletionProgress> out;
out.reserve(owned.size());
for (const auto& [setId, ownedNos] : owned) {
for (const auto& [setId, info] : owned) {
const auto* pack = catalog.findPack(setId);
if (pack == nullptr || pack->cards.empty()) continue;
std::size_t matched = 0;
for (const auto& card : pack->cards) {
const std::string catalogNo = normalizeForRegion(region, card.setNo);
if (!catalogNo.empty() && ownedNos.count(catalogNo) != 0) ++matched;
if (!catalogNo.empty() && info.nos.count(catalogNo) != 0) ++matched;
}
PokemonSetCompletionProgress row;
row.region = region;
row.setId = pack->setId;
row.setName = pack->setName;
row.releaseDate = info.releaseDate;
row.ownedUnique = matched;
row.total = pack->cards.size();
out.push_back(std::move(row));
@@ -149,6 +160,10 @@ computePokemonSetCompletion(const std::vector<PokemonCard>& collection,
std::sort(out.begin(), out.end(),
[](const PokemonSetCompletionProgress& a,
const PokemonSetCompletionProgress& b) {
// YYYY/MM/DD lex order is chronological (CardSorter parity).
if (a.releaseDate != b.releaseDate) {
return a.releaseDate < b.releaseDate;
}
if (a.setName != b.setName) return a.setName < b.setName;
return static_cast<int>(a.region) < static_cast<int>(b.region);
});
@@ -192,7 +207,8 @@ pokemonChecklistForSet(const std::vector<PokemonCard>& collection,
std::sort(out.begin(), out.end(),
[](const PokemonChecklistEntry& a, const PokemonChecklistEntry& b) {
if (a.setNo != b.setNo) return a.setNo < b.setNo;
const int cmp = compareSetNoNatural(a.setNo, b.setNo);
if (cmp != 0) return cmp < 0;
return a.name < b.name;
});
return out;
+69
View File
@@ -0,0 +1,69 @@
#include "ccm/util/SetNoNatural.hpp"
#include <cctype>
namespace ccm {
namespace {
[[nodiscard]] bool isAsciiDigit(char c) noexcept {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
}
[[nodiscard]] int cmpChar(char a, char b) noexcept {
const auto ua = static_cast<unsigned char>(a);
const auto ub = static_cast<unsigned char>(b);
if (ua < ub) return -1;
if (ua > ub) return 1;
return 0;
}
} // namespace
int compareSetNoNatural(std::string_view a, std::string_view b) noexcept {
std::size_t i = 0;
std::size_t j = 0;
while (i < a.size() && j < b.size()) {
const bool aDigit = isAsciiDigit(a[i]);
const bool bDigit = isAsciiDigit(b[j]);
if (aDigit && bDigit) {
std::size_t aEnd = i;
while (aEnd < a.size() && isAsciiDigit(a[aEnd])) ++aEnd;
std::size_t bEnd = j;
while (bEnd < b.size() && isAsciiDigit(b[bEnd])) ++bEnd;
std::size_t aSig = i;
while (aSig < aEnd && a[aSig] == '0') ++aSig;
std::size_t bSig = j;
while (bSig < bEnd && b[bSig] == '0') ++bSig;
const std::size_t aLen = aEnd - aSig;
const std::size_t bLen = bEnd - bSig;
if (aLen != bLen) return aLen < bLen ? -1 : 1;
for (std::size_t k = 0; k < aLen; ++k) {
const int c = cmpChar(a[aSig + k], b[bSig + k]);
if (c != 0) return c;
}
i = aEnd;
j = bEnd;
continue;
}
const int c = cmpChar(a[i], b[j]);
if (c != 0) return c;
++i;
++j;
}
if (i == a.size() && j == b.size()) {
if (a == b) return 0;
return a < b ? -1 : 1;
}
return i == a.size() ? -1 : 1;
}
} // namespace ccm