Files
Vorrania/ios/Sources/BestBeforeText.swift
Scarriffle e42e11bf23 iOS: Name/Marke per Text scannen + Frischedatum ohne Jahr erkennen
- Neuer TextScanView: beim Anlegen Name und Marke per Texterkennung einlesen
  (erkannte Zeilen antippen, wie beim MHD-Scan) - kein Abtippen mehr, wenn ein
  Produkt nicht in Open Food/Products Facts steht.
- BestBeforeText erkennt jetzt auch Tag.Monat ohne Jahr (z.B. "05.01" bei
  Frischware / "zu verbrauchen bis") und ergaenzt das aktuelle Jahr (bzw. das
  naechste, falls schon vergangen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 14:35:46 +02:00

111 lines
4.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"),
// Nur Tag und Monat, kein Jahr (z.B. 05.01" bei Frischware / zu
// verbrauchen bis"). Das Jahr ergänzt build(). Steht bewusst zuletzt,
// damit Muster mit Jahr Vorrang haben.
(#"(\d{1,2})[./-](\d{1,2})(?!\d)"#, ["d", "m"], "day"),
]
/// Alle plausiblen Daten aus einem Textstueck, in Fundreihenfolge und ohne Dubletten.
static func matches(in text: String) -> [Match] {
var found: [Match] = []
var belegt: [Range<String.Index>] = []
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 if precision == "day", let tag = teile["d"] {
// Kein Jahr aufgedruckt (Frischware): aktuelles Jahr annehmen; liegt
// das Datum schon in der Vergangenheit, das nächste Jahr nehmen.
let cal = Calendar.current
let aktuell = cal.component(.year, from: Date())
var probe = DateComponents()
probe.year = aktuell; probe.month = monat; probe.day = tag
if let d = cal.date(from: probe), d < cal.startOfDay(for: Date()) {
jahr = aktuell + 1
} else {
jahr = aktuell
}
} 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)
}
}