import Foundation /// Erkennt Mindesthaltbarkeitsdaten in Text, wie er auf Verpackungen steht. /// /// Aufdrucke sind uneinheitlich: mal "12.09.2026", mal "09/26", mal /// "mind. haltbar bis: 2026-09-12". Gefundene Treffer werden dem Benutzer zur /// Auswahl vorgelegt und nie ungefragt uebernommen – eine Fehllesung beim MHD /// faellt sonst erst auf, wenn die Ware schon falsch einsortiert ist. enum BestBeforeText { struct Match: Identifiable, Hashable { let id = UUID() let date: Date /// "day" oder "month" – ein Aufdruck "09/2026" nennt keinen Tag. let precision: String var label: String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "de_DE") formatter.dateFormat = precision == "month" ? "MM/yyyy" : "dd.MM.yyyy" return formatter.string(from: date) } } // Reihenfolge ist wichtig: zuerst die Muster mit Tag, damit "12.09.2026" // nicht als Monatsangabe "12.09" missverstanden wird. private static let patterns: [(regex: String, order: [Character], precision: String)] = [ (#"(\d{1,2})[./-](\d{1,2})[./-](\d{4})"#, ["d", "m", "y"], "day"), (#"(\d{4})[./-](\d{1,2})[./-](\d{1,2})"#, ["y", "m", "d"], "day"), (#"(\d{1,2})[./-](\d{1,2})[./-](\d{2})(?!\d)"#, ["d", "m", "Y"], "day"), (#"(\d{1,2})[./-](\d{4})"#, ["m", "y"], "month"), (#"(\d{4})[./-](\d{1,2})(?!\d)"#, ["y", "m"], "month"), (#"(\d{1,2})[./-](\d{2})(?!\d)"#, ["m", "Y"], "month"), ] /// Alle plausiblen Daten aus einem Textstueck, in Fundreihenfolge und ohne Dubletten. static func matches(in text: String) -> [Match] { var found: [Match] = [] var belegt: [Range] = [] for pattern in patterns { guard let regex = try? NSRegularExpression(pattern: pattern.regex) else { continue } let range = NSRange(text.startIndex..., in: text) for result in regex.matches(in: text, range: range) { guard let treffer = Range(result.range, in: text) else { continue } // Ein Textabschnitt zaehlt nur einmal: sonst liest das Monatsmuster // aus einem bereits erkannten Tagesdatum noch einen zweiten Treffer. if belegt.contains(where: { $0.overlaps(treffer) }) { continue } var teile: [Character: Int] = [:] for (index, key) in pattern.order.enumerated() { guard let teil = Range(result.range(at: index + 1), in: text), let zahl = Int(text[teil]) else { continue } teile[key] = zahl } guard let datum = build(teile, precision: pattern.precision) else { continue } belegt.append(treffer) found.append(Match(date: datum, precision: pattern.precision)) } } return found } private static func build(_ teile: [Character: Int], precision: String) -> Date? { let monat = teile["m"] ?? 0 guard (1...12).contains(monat) else { return nil } // "Y" ist ein zweistelliges Jahr: 26 -> 2026. let jahr: Int if let vier = teile["y"] { jahr = vier } else if let zwei = teile["Y"] { jahr = 2000 + zwei } else { return nil } // Aufdrucke liegen realistisch in diesem Fenster; alles andere ist // vermutlich eine Chargennummer und kein MHD. let aktuell = Calendar.current.component(.year, from: Date()) guard (aktuell - 5)...(aktuell + 30) ~= jahr else { return nil } var komponenten = DateComponents() komponenten.year = jahr komponenten.month = monat komponenten.day = precision == "month" ? 1 : (teile["d"] ?? 1) if precision == "day" { guard let tag = teile["d"], (1...31).contains(tag) else { return nil } } return Calendar.current.date(from: komponenten) } }