mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 17:01:02 +00:00
convenience features (#21)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user