Files
Vorrania/ios/Sources/BestBeforeText.swift
Scarriffle c4acc8bec7 MHD per Texterkennung einlesen
In der Chargenzeile beim Einlagern gibt es einen Knopf "MHD scannen". Er
oeffnet die Texterkennung von VisionKit; erkannte Daten erscheinen als
Vorschlag und werden erst uebernommen, wenn man sie antippt. Bewusst kein
automatisches Uebernehmen: Eine Fehllesung beim MHD faellt sonst erst auf,
wenn die Ware bereits falsch einsortiert ist.

Der Parser deckt die Schreibweisen ab, die auf Verpackungen vorkommen -
Tagesdaten (12.09.2026, 2026-09-12, 12/09/2026, 12.09.26) und Monatsangaben
(09/2026, 09.2026, 2026-09, 09/26). Erkennt der Aufdruck nur Monat und Jahr,
stellt sich die Eingabe sichtbar auf Monat/Jahr um.

Damit aus Chargennummern und EAN-Codes keine Phantasiedaten werden, greifen
mehrere Einschraenkungen: Muster mit Tag werden zuerst geprueft, ein bereits
erkannter Textabschnitt wird nicht erneut ausgewertet, Monate ausserhalb 1-12
entfallen und das Jahr muss im Fenster von fuenf Jahren rueckwaerts bis
dreissig vorwaerts liegen.

Der Knopf erscheint nur, wenn das Geraet Texterkennung unterstuetzt
(Neural Engine); sonst bleibt die Eingabe von Hand.

Getestet: Der Parser wurde eigenstaendig uebersetzt und gegen 14 Faelle
geprueft, alle korrekt - darunter, dass "12.09.2026" nicht als Monatsangabe
gelesen wird, dass eine EAN keine Treffer erzeugt und dass unplausible Jahre
und Monate verworfen werden. Der iOS-Geraetebuild laeuft fehlerfrei durch.
Die Erkennung an einer echten Verpackung habe ich nicht selbst ausprobiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 18:10:24 +02:00

95 lines
4.0 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"),
]
/// 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 {
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)
}
}