Fixes #26 — Multi-select levels and irregular-type practice filter
UserProgress gains selectedLevelsBlob and enabledIrregularCategoriesBlob (mirrors the existing tense-blob pattern). The multi-level setter keeps the legacy selectedLevel String in sync with the highest-ranked selection, so widget sync, AI scenarios, and achievement checks keep working unchanged. Legacy single-level users are migrated on first read. Settings replaces the level Picker with per-level toggles and adds an Irregular Types section with three toggles. Practice pool is the literal intersection: empty levels means zero results, empty irregular categories means no irregularity constraint. Pure filter logic lives in SharedModels (PracticeFilter, VerbLevel.highest) and is covered by 20 Swift Testing cases. ReferenceStore delegates so the intersection behavior is unit-tested without a ModelContainer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,50 @@ struct ReferenceStore {
|
||||
Set(fetchVerbs(selectedLevel: selectedLevel).map(\.id))
|
||||
}
|
||||
|
||||
/// Union of data-levels for all selected user-facing levels.
|
||||
/// Empty input produces an empty result — callers decide how to handle that.
|
||||
func fetchVerbs(selectedLevels: Set<String>) -> [Verb] {
|
||||
guard !selectedLevels.isEmpty else { return [] }
|
||||
let ids = PracticeFilter.verbIDs(
|
||||
matchingLevels: selectedLevels,
|
||||
in: fetchVerbs().map { .init(id: $0.id, level: $0.level) }
|
||||
)
|
||||
return fetchVerbs().filter { ids.contains($0.id) }
|
||||
}
|
||||
|
||||
/// Practice verb pool intersecting selected levels with selected irregular-span categories.
|
||||
/// Delegates to `PracticeFilter` so the intersection logic is unit-tested
|
||||
/// in SharedModels without a ModelContainer (Issue #26).
|
||||
func allowedVerbIDs(
|
||||
selectedLevels: Set<String>,
|
||||
irregularCategories: Set<IrregularSpan.SpanCategory>
|
||||
) -> Set<Int> {
|
||||
PracticeFilter.allowedVerbIDs(
|
||||
verbs: fetchVerbs().map { .init(id: $0.id, level: $0.level) },
|
||||
spans: allIrregularSlots(),
|
||||
selectedLevels: selectedLevels,
|
||||
irregularCategories: irregularCategories
|
||||
)
|
||||
}
|
||||
|
||||
/// Convenience: full Verb objects passing both filters.
|
||||
func fetchVerbs(
|
||||
selectedLevels: Set<String>,
|
||||
irregularCategories: Set<IrregularSpan.SpanCategory>
|
||||
) -> [Verb] {
|
||||
let ids = allowedVerbIDs(
|
||||
selectedLevels: selectedLevels,
|
||||
irregularCategories: irregularCategories
|
||||
)
|
||||
return fetchVerbs().filter { ids.contains($0.id) }
|
||||
}
|
||||
|
||||
private func allIrregularSlots() -> [PracticeFilter.IrregularSlot] {
|
||||
let descriptor = FetchDescriptor<IrregularSpan>()
|
||||
let spans = (try? context.fetch(descriptor)) ?? []
|
||||
return spans.map { .init(verbId: $0.verbId, category: $0.category) }
|
||||
}
|
||||
|
||||
func fetchVerb(id: Int) -> Verb? {
|
||||
let descriptor = FetchDescriptor<Verb>(predicate: #Predicate<Verb> { $0.id == id })
|
||||
return (try? context.fetch(descriptor))?.first
|
||||
|
||||
Reference in New Issue
Block a user