import Foundation /// Vergleich einer getippten User-Antwort gegen die erwartete Antwort. /// 1:1-Port aus `cards/packages/cards-domain/src/typing.ts`: /// Normalisierung (lowercase, trim, NFD-Diakritika-Stripping), /// dann exact-match → `correct`. Sonst Levenshtein-Distanz mit /// Threshold `max(1, floor(answer.length * 0.2))` → `close`. enum TypingMatch: Equatable { case correct case close case wrong } enum Typing { /// `aliases` ist ein Komma-getrennter String aus dem `aliases`-Feld /// der Karte (optional). Jeder Alias zählt als gültige Antwort. static func check(input: String, answer: String, aliases: String? = nil) -> TypingMatch { let normInput = normalize(input) guard !normInput.isEmpty else { return .wrong } var candidates = [answer] if let aliases { candidates.append(contentsOf: aliases.split(separator: ",").map(String.init)) } let normalizedCandidates = candidates .map(normalize) .filter { !$0.isEmpty } guard !normalizedCandidates.isEmpty else { return .wrong } if normalizedCandidates.contains(normInput) { return .correct } let shortestLen = normalizedCandidates.map(\.count).min() ?? normInput.count let threshold = max(1, Int(Double(shortestLen) * 0.2)) for candidate in normalizedCandidates where levenshtein(normInput, candidate) <= threshold { return .close } return .wrong } private static func normalize(_ string: String) -> String { let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) let lowered = trimmed.lowercased() // NFD-Dekomposition + Combining-Marks entfernen (z.B. ä → a) let decomposed = lowered.decomposedStringWithCanonicalMapping let stripped = decomposed.unicodeScalars.filter { scalar in !(0x0300 ... 0x036F).contains(scalar.value) } return String(String.UnicodeScalarView(stripped)) } static func levenshtein(_ a: String, _ b: String) -> Int { let aChars = Array(a) let bChars = Array(b) let m = aChars.count let n = bChars.count if m == 0 { return n } if n == 0 { return m } var row = Array(0 ... n) for i in 1 ... m { var prev = row[0] row[0] = i for j in 1 ... n { let tmp = row[j] if aChars[i - 1] == bChars[j - 1] { row[j] = prev } else { row[j] = 1 + Swift.min(prev, row[j], row[j - 1]) } prev = tmp } } return row[n] } }