import Foundation /// Geld, so wie das Backend es führt: ganze Cent plus Währungscode. /// /// Nirgends Fliesskomma. Die API kennt ausschliesslich Cent-Ganzzahlen /// (einzige Ausnahme ist der Kurzbefehl-Endpunkt), und das bleibt hier so – /// ein `Double` hätte bei Summen über viele Buchungen Rundungsfehler. struct Geldbetrag: Hashable, Sendable { let cents: Int let waehrung: String init(cents: Int, waehrung: String) { self.cents = cents self.waehrung = waehrung.uppercased() } var dezimal: Decimal { Decimal(cents) / Decimal(100) } var istNull: Bool { cents == 0 } var istNegativ: Bool { cents < 0 } /// Für die Anzeige, in der Sprache des Geräts. /// /// `vorzeichen: .immer` schreibt auch bei positiven Beträgen ein Plus – /// das braucht die Buchungsliste, wo Einnahme und Ausgabe nebeneinander /// stehen und das Vorzeichen die einzige Unterscheidung ist. func formatiert(vorzeichen: Vorzeichenstil = .nurMinus) -> String { let wert = vorzeichen == .ohne ? abs(dezimal) : dezimal var text = wert.formatted(.currency(code: waehrung).locale(.autoupdatingCurrent)) if vorzeichen == .immer, cents > 0 { text = "+" + text } return text } enum Vorzeichenstil: Sendable { /// Minus bei negativen Beträgen, sonst nichts. Der Normalfall. case nurMinus /// Auch bei positiven ein Plus. case immer /// Immer ohne – wenn die Richtung schon woanders steht. case ohne } /// Ohne Nachkommastellen, für enge Stellen wie Diagramm-Achsen. var gerundet: String { (dezimal as NSDecimalNumber).doubleValue.formatted( .currency(code: waehrung).precision(.fractionLength(0))) } static func + (links: Geldbetrag, rechts: Geldbetrag) -> Geldbetrag { precondition(links.waehrung == rechts.waehrung, "Betraege verschiedener Waehrungen lassen sich nicht addieren") return Geldbetrag(cents: links.cents + rechts.cents, waehrung: links.waehrung) } static prefix func - (betrag: Geldbetrag) -> Geldbetrag { Geldbetrag(cents: -betrag.cents, waehrung: betrag.waehrung) } } extension Geldbetrag { /// Was der Nutzer tippt, in Cent. /// /// Nimmt Komma und Punkt als Dezimaltrenner – auf einer deutschen Tastatur /// tippt man „12,50", auf der Zehnertastatur „12.50", und beides meint /// dasselbe. Tausendertrenner und Währungszeichen fliegen raus. static func ausEingabe(_ eingabe: String, waehrung: String) -> Geldbetrag? { var text = eingabe.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return nil } let negativ = text.hasPrefix("-") text = text.filter { $0.isNumber || $0 == "," || $0 == "." } // Ohne eine einzige Ziffer ist das keine Zahl, sondern ein Vertipper. // Sonst käme aus "abc" ein Betrag von 0,00 heraus. guard text.contains(where: \.isNumber) else { return nil } // Steht beides drin, ist das hintere der Dezimaltrenner und das // vordere der Tausendertrenner ("1.234,50" wie "1,234.50"). let letztesKomma = text.lastIndex(of: ",") let letzterPunkt = text.lastIndex(of: ".") let trenner: Character? switch (letztesKomma, letzterPunkt) { case (nil, nil): trenner = nil case (_?, nil): trenner = "," case (nil, _?): trenner = "." case (let komma?, let punkt?): trenner = komma > punkt ? "," : "." } var ganze = text var bruch = "" if let trenner, let stelle = text.lastIndex(of: trenner) { ganze = String(text[text.startIndex.. 2 { ganze += bruch bruch = "" } bruch = String((bruch + "00").prefix(2)) guard let vorne = Int(ganze.isEmpty ? "0" : ganze), let hinten = Int(bruch) else { return nil } let summe = vorne * 100 + hinten return Geldbetrag(cents: negativ ? -summe : summe, waehrung: waehrung) } /// Zum Vorbelegen eines Eingabefelds: „12.50", ohne Währung und Trenner. var alsEingabe: String { let betrag = abs(cents) return "\(betrag / 100),\(String(format: "%02d", betrag % 100))" } }