# Product Requirements Document: quando ## 1. Projektübersicht ### Ziel und Vision `quando` ist eine eigenständige Go-Bibliothek für intuitive und idiomatische Datumsberechnungen. Sie bietet eine Fluent API für komplexe Datums-Operationen, die mit der Go-Standardbibliothek umständlich oder unmöglich sind. Die Library ist der Kern der geplanten DatesAPI v2 und soll als wiederverwendbare Komponente in beliebigen Go-Projekten einsetzbar sein. **Vision:** Die bevorzugte Go-Library für Datumsberechnungen – so natürlich und intuitiv wie Moment.js oder Carbon, aber Go-idiomatisch und ohne externe Dependencies. ### Zielgruppe - **Primär:** Go-Entwickler, die komplexe Datumsberechnungen durchführen müssen - **Sekundär:** Entwickler der DatesAPI v2 (interner First User) - **Technisches Level:** Erfahrene Go-Entwickler, die Go-Idiome und `time.Time` kennen ### Erfolgs-Metriken - **Adoption:** Verwendung in der DatesAPI v2 als Proof-of-Concept - **Code-Reduktion:** Typische Datums-Operationen in 1 Zeile statt 5-10 Zeilen - **Test-Coverage:** Minimum 95% für alle Kalkulationsfunktionen - **Performance:** Alle Operationen unter 1µs (außer Parsing) - **Zero Dependencies:** Ausschließlich Go stdlib (außer optionale i18n-Erweiterungen) ### Projektscope **Phase 1 – In Scope:** - Datums-Arithmetik (Add/Sub mit allen Zeiteinheiten) - Snap-to/Ankerpunkte (StartOf, EndOf, Next, Prev) - Differenz-Berechnung (mit Human-Format) - Datums-Inspektion (WeekNumber, Quarter, DayOfYear, etc.) - Formatierung (Presets + Custom Layouts + i18n) - Zeitzone-Support (Konvertierung + DST-Handling) - Unix-Timestamp-Konvertierung - Parsing (automatisch + explizit + relativ) **Out of Scope (spätere Phasen):** - HTTP/API-Schicht (separater Webserver) - Feiertage & Arbeitstage (Phase 3) - Datums-Serien/Ranges (Phase 2) - Batch-Operationen (Phase 2) --- ## 2. Funktionale Anforderungen ### Kern-Features #### 2.1 Datums-Arithmetik (Must-have) Verkettbare Add/Sub-Operationen für alle Zeiteinheiten. **API:** ```go quando.From(time.Now()).Add(2, quando.Days) quando.From(time.Now()).Add(2, quando.Months).Sub(3, quando.Days) quando.Now().Add(1, quando.Years) ``` **Unterstützte Einheiten:** `Seconds`, `Minutes`, `Hours`, `Days`, `Weeks`, `Months`, `Quarters`, `Years` **Edge Cases:** - **Monatsende-Overflow:** Bei Addition von Monaten wird auf das Monatsende gekürzt, falls das Zieldatum nicht existiert - `2026-01-31` + 1 Monat = `2026-02-28` (Monatsende Februar) - `2026-01-24` + 1 Monat = `2026-02-24` (regulär) - `2026-05-31` + 1 Monat = `2026-06-30` (Juni hat nur 30 Tage) #### 2.2 Snap-to / Ankerpunkte (Must-have) Sprung zum Anfang/Ende einer Zeiteinheit oder zum nächsten/vorherigen Wochentag. **API:** ```go quando.From(date).StartOf(quando.Month) // 1. des Monats, 00:00:00 quando.From(date).EndOf(quando.Quarter) // Letzter Tag des Quartals, 23:59:59 quando.From(date).Next(time.Monday) // Nächster Montag (nie heute) quando.From(date).Prev(time.Friday) // Vorheriger Freitag (nie heute) ``` **Snap-Einheiten:** `Week`, `Month`, `Quarter`, `Year` **Verhalten:** - **StartOf(Week):** Montag 00:00:00 (ISO 8601 Default, konfigurierbar) - **EndOf(Week):** Sonntag 23:59:59 bei Montag-Start, Samstag 23:59:59 bei Sonntag-Start - **Next(Weekday):** Immer der NÄCHSTE Wochentag (nie heute, auch wenn heute der gleiche Wochentag ist) - **Prev(Weekday):** Immer der VORHERIGE Wochentag (nie heute) **Quartals-Definition:** - Q1 = Januar–März - Q2 = April–Juni - Q3 = Juli–September - Q4 = Oktober–Dezember Keine Konfiguration von Geschäftsjahren in Phase 1. #### 2.3 Differenz-Berechnung (Must-have) Berechnung der Differenz zwischen zwei Daten in verschiedenen Einheiten. **API:** ```go d := quando.Diff(date1, date2) d.Days() // int: 319 d.Weeks() // int: 45 d.Months() // int: 10 (abgerundet) d.MonthsFloat() // float64: 10.516... (präzise) d.Years() // int: 0 d.YearsFloat() // float64: 0.876... d.Human() // "10 months, 16 days" (adaptive Granularität) d.Human(quando.LangDE) // "10 Monate, 16 Tage" ``` **Präzision:** - Integer-Varianten (`Months()`, `Years()`) geben abgerundete Werte zurück - Float-Varianten (`MonthsFloat()`, `YearsFloat()`) für präzise Berechnungen **Human-Format:** Adaptive Granularität – immer die zwei größten relevanten Einheiten: | Differenz | Ausgabe (EN) | Ausgabe (DE) | |-----------------------|-----------------------|------------------------| | 10 Monate, 16 Tage | "10 months, 16 days" | "10 Monate, 16 Tage" | | 2 Tage, 5 Stunden | "2 days, 5 hours" | "2 Tage, 5 Stunden" | | 3 Stunden, 20 Minuten | "3 hours, 20 minutes" | "3 Stunden, 20 Minuten"| | 45 Sekunden | "45 seconds" | "45 Sekunden" | | 0 | "0 seconds" | "0 Sekunden" | #### 2.4 Datums-Inspektion (Must-have) Abfrage von Meta-Informationen zu einem Datum. **API (aggregiert):** ```go info := quando.From(date).Info() info.WeekNumber // int: ISO 8601 Week Number info.Quarter // int: 1–4 info.DayOfYear // int: 1–366 info.IsWeekend // bool: Samstag oder Sonntag info.IsLeapYear // bool: Schaltjahr info.Unix // int64: Unix Timestamp ``` **API (einzeln):** ```go quando.From(date).WeekNumber() // 7 quando.From(date).Quarter() // 1 quando.From(date).DayOfYear() // 40 quando.From(date).IsWeekend() // true quando.From(date).IsLeapYear() // false quando.From(date).Unix() // 1770595200 ``` **Konventionen:** - **WeekNumber:** ISO 8601 (Montag = erster Tag, Woche 1 = erste Woche mit Donnerstag) - **IsWeekend:** Samstag + Sonntag (nicht konfigurierbar in Phase 1) - **IsLeapYear:** Standard-Regel (durch 4 teilbar, außer Jahrhundert, außer durch 400 teilbar) #### 2.5 Formatierung (Must-have) Erweiterte Formatierung mit Presets, Custom Layouts und Mehrsprachigkeit. **Preset-Formate:** ```go quando.From(date).Format(quando.ISO) // "2026-02-09" quando.From(date).Format(quando.EU) // "09.02.2026" quando.From(date).Format(quando.US) // "02/09/2026" quando.From(date).Format(quando.Long) // "February 9, 2026" quando.From(date).Format(quando.RFC2822) // "Mon, 09 Feb 2026 00:00:00 +0000" ``` **Custom Layouts (Go-Standard):** ```go quando.From(date).Format("Monday, 2. January 2006") // "Monday, 9. February 2026" ``` **Mehrsprachigkeit:** ```go quando.From(date).Lang(quando.DE).Format(quando.Long) // "9. Februar 2026" quando.From(date).Lang(quando.DE).Format("Monday, 2. January 2006") // "Montag, 9. Februar 2026" ``` **Sprachregeln:** - Nur `Long` und Custom Layouts sind sprachabhängig - ISO, EU, US, RFC2822 sind immer sprachunabhängig - **Phase 1 Sprachen:** EN (Default), DE (Must-have) - **Spätere Phasen:** Die 21 weiteren Sprachen aus v1 (ES, FA, FR, HI, ID, IT, JP, KR, MS_MY, NL, PL, PT, RO, RU, SE, TH, TR, UK, VI, ZH_CN, ZH_TW) #### 2.6 Zeitzone-Support (Must-have) Konvertierung zwischen Zeitzonen mit korrektem DST-Handling. **API:** ```go // Datum in Zeitzone quando.From(time.Now()).In("Europe/Berlin") // Konvertierung quando.From(date).In("America/New_York") ``` **Default-Verhalten:** - **Default-Zeitzone:** UTC (wenn nicht explizit gesetzt) - **DST-Handling:** `Add(1, Days)` bedeutet "selbe Uhrzeit am nächsten Kalendertag", NICHT 24 Stunden - Beispiel: `2026-03-31 02:00` (CET) + 1 Day = `2026-04-01 02:00` (CEST), auch wenn dies nur 23 Stunden sind - Rationale: Menschen denken in Kalendertagen, nicht in Stunden-Deltas #### 2.7 Unix-Timestamp-Konvertierung (Must-have) Bidirektionale Konvertierung zwischen `time.Time` und Unix-Timestamps. **API:** ```go // time.Time → Unix quando.From(date).Unix() // 1770595200 // Unix → time.Time quando.FromUnix(1770595200) // Date ``` **Unterstützung:** - Positive und negative Timestamps (vor 1970) werden unterstützt - Keine künstlichen Datumsbeschränkungen – Go's `time.Time` Range (Jahr 0–9999+) #### 2.8 Parsing (Must-have) Automatisches und explizites Parsing von Datums-Strings. **Automatisches Parsing:** ```go quando.Parse("2026-02-09") // ISO quando.Parse("09.02.2026") // EU (Punkt-Trennzeichen) quando.Parse("2026/02/09") // ISO mit Slash quando.Parse("Mon, 09 Feb 2026") // RFC2822 ``` **Mehrdeutigkeits-Regel:** Slash-Formate ohne Jahr-Prefix sind mehrdeutig und führen zu einem Error: | Eingabe | Erkennung | Begründung | |--------------|-------------------|------------------------------------| | `2026-02-01` | ✅ ISO, eindeutig | Standard-Format | | `01.02.2026` | ✅ EU, eindeutig | Punkt = EU-Konvention | | `2026/02/09` | ✅ ISO, eindeutig | Jahr-Prefix ist eindeutig | | `01/02/2026` | ❌ Error | Mehrdeutig (US vs. EU) | **Explizites Parsing:** ```go quando.ParseWithLayout("01/02/2026", "02/01/2006") // EU-Format quando.ParseWithLayout("01/02/2026", "01/02/2006") // US-Format ``` **Relative Ausdrücke (Must-have in Phase 1):** ```go quando.ParseRelative("today") // Heute 00:00:00 quando.ParseRelative("tomorrow") // Morgen 00:00:00 quando.ParseRelative("yesterday") // Gestern 00:00:00 quando.ParseRelative("+2 days") // Heute + 2 Tage quando.ParseRelative("-1 week") // Heute - 1 Woche quando.ParseRelative("+3 months") // Heute + 3 Monate ``` Komplexere Ausdrücke (`"next monday"`, `"start of month"`) sind Nice-to-have für spätere Versionen. --- ### User Stories **Als Go-Entwickler möchte ich...** 1. **Komplexe Arithmetik:** ...verkettete Datums-Operationen in einer Zeile schreiben können, damit mein Code lesbar bleibt - Akzeptanzkriterium: `quando.Now().Add(2, Months).Sub(3, Days)` funktioniert 2. **Monatsende-Arithmetik:** ...Monate addieren ohne manuell Overflow-Fälle zu behandeln - Akzeptanzkriterium: `31. Jan + 1 Monat = 28. Feb` (automatisch) 3. **Quartalsberechnungen:** ...zum Quartalsanfang/-ende springen können - Akzeptanzkriterium: `quando.Now().StartOf(Quarter)` gibt 1. Jan/Apr/Jul/Okt zurück 4. **Differenz-Formatierung:** ...Datumsdifferenzen menschenlesbar ausgeben - Akzeptanzkriterium: `Diff(a, b).Human()` gibt "10 months, 16 days" zurück 5. **Mehrsprachigkeit:** ...Datums-Strings in verschiedenen Sprachen formatieren - Akzeptanzkriterium: `.Lang(DE).Format(Long)` gibt "9. Februar 2026" zurück 6. **Zeitzone-Transparenz:** ...Datums-Arithmetik über DST-Umstellungen hinweg korrekt durchführen - Akzeptanzkriterium: `Add(1, Days)` bedeutet "nächster Kalendertag", nicht "24 Stunden" 7. **Testbarkeit:** ...deterministische Tests schreiben können - Akzeptanzkriterium: `quando.NewClock(fixedTime)` für Test-Fixtures --- ### Detaillierte Workflows #### Workflow 1: Datums-Arithmetik mit Verkettung ```go // Szenario: Berechne "Letzter Tag des übernächsten Quartals" date := quando.Now(). Add(2, quando.Quarters). EndOf(quando.Quarter) // Szenario: "Erster Montag nach Monatsende" date := quando.Now(). EndOf(quando.Month). Next(time.Monday) ``` #### Workflow 2: Differenz-Berechnung mit Formatierung ```go start := quando.MustParse("2025-01-15") end := quando.Now() diff := quando.Diff(start.Time(), end.Time()) fmt.Printf("Tage: %d\n", diff.Days()) fmt.Printf("Monate: %d\n", diff.Months()) fmt.Printf("Lesbar: %s\n", diff.Human(quando.LangDE)) ``` #### Workflow 3: Parsing unbekannter Formate ```go // Automatisches Parsing date, err := quando.Parse(userInput) if err != nil { // Fallback: Explizites Format date, err = quando.ParseWithLayout(userInput, "02/01/2006") } ``` #### Workflow 4: Zeitzone-Konvertierung ```go // UTC → Berlin utcDate := quando.FromUnix(1770595200) berlinDate := utcDate.In("Europe/Berlin") // Arithmetik in spezifischer Zeitzone date := quando.Now(). In("America/New_York"). Add(1, quando.Days) ``` --- ### Feature-Prioritäten | Feature | Priorität | Rationale | |-----------------------------|------------|--------------------------------------------| | Datums-Arithmetik | Must-have | Kern-Feature, primärer Use Case | | Snap-to/Ankerpunkte | Must-have | Häufiger Use Case, schwer mit stdlib | | Differenz-Berechnung (int) | Must-have | Häufiger Use Case | | Differenz-Berechnung (float)| Must-have | Für präzise Berechnungen notwendig | | Human-Format (EN, DE) | Must-have | Differenzierung zu anderen Libraries | | Parsing (automatisch) | Must-have | Eingangs-Punkt für User Input | | Zeitzone-Support | Must-have | Essentiell für korrekte Berechnungen | | Datums-Inspektion | Must-have | Convenience, wenig Aufwand | | Unix-Timestamp | Must-have | Standard-Interop mit APIs | | Formatierung (Presets) | Must-have | Häufiger Use Case | | ParseRelative (basic) | Must-have | User-freundlich, von v1 gewohnt | | Formatierung (Custom) | Must-have | Flexibilität | | ParseRelative (advanced) | Nice-to-have| Komplexität vs. Nutzen | | Weitere Sprachen (21) | Nice-to-have| Internationale Nutzung, aber aufwändig | --- ## 3. Technische Anforderungen ### Performance-Ziele - **Arithmetik-Operationen:** < 1 µs pro Operation (Add, Sub, StartOf, EndOf) - **Differenz-Berechnung:** < 1 µs für Integer-Varianten, < 2 µs für Float-Varianten - **Formatierung:** < 5 µs ohne i18n, < 10 µs mit i18n - **Parsing (automatisch):** < 10 µs für eindeutige Formate - **Parsing (relativ):** < 20 µs - **Memory Allocations:** Keine Allocations bei verketteten Operationen (außer final result) ### Concurrent User-Kapazität Nicht anwendbar – reine Library ohne Server-Komponente. ### Real-time Features Nicht anwendbar – keine Echtzeit-Features, keine WebSockets/SSE. ### Sicherheitsstandards - **Input Validation:** Alle Parse-Funktionen müssen ungültige Eingaben sicher mit Error zurückweisen (NIEMALS panic) - **Overflow-Schutz:** Datums-Arithmetik muss Overflow-Szenarien handhaben (z.B. Jahr > 9999) - **Timezone-Sicherheit:** IANA-Timezone-Namen validieren, bei ungültigen Namen Error zurückgeben ### Compliance-Vorgaben - **ISO 8601:** Compliance für Datumsformate, Wochennummern, Zeitzonen - **IANA Timezone Database:** Verwendung der Standard-Zeitzonen-Datenbank ### Plattform-Support - **Go-Version:** Minimum Go 1.22+ (für aktuelle stdlib-Features) - **Betriebssysteme:** Alle von Go unterstützten Plattformen (Linux, macOS, Windows, BSD) - **Architekturen:** Alle von Go unterstützten Architekturen (amd64, arm64, 386, arm) --- ## 4. Datenarchitektur Nicht anwendbar – keine Datenbank, keine Persistierung. Alle Daten sind transient (in-memory). --- ## 5. API & Interface-Spezifikation ### Go-API-Design #### Haupt-Typ: `quando.Date` ```go // Date ist der zentrale Typ der Library // Er wrapped time.Time und bietet eine Fluent API type Date struct { t time.Time lang Lang // optional, für Formatierung } // Konvertierung time.Time → quando.Date func From(t time.Time) Date // Konvertierung quando.Date → time.Time func (d Date) Time() time.Time ``` **Design-Rationale:** - Eigener Wrapper-Typ notwendig für Fluent API (Verkettung) - Kein Reimplementieren von `time.Time` – Delegation an stdlib - Einfache bidirektionale Konvertierung #### Einheiten-Konstanten ```go type Unit int const ( Seconds Unit = iota Minutes Hours Days Weeks Months Quarters Years ) ``` **Design-Rationale:** - Typsichere Konstanten statt Strings (Compile-Time Safety) - `iota` für klare Ordnung - Interne `ParseUnit(string) Unit` für externe Eingaben (API-Layer, ParseRelative) #### Sprach-Konstanten ```go type Lang string const ( LangEN Lang = "en" // English (Default) LangDE Lang = "de" // Deutsch // Weitere 21 Sprachen in späteren Versionen ) ``` #### Fehlerbehandlung Alle Funktionen, die fehlschlagen können, geben `(Result, error)` zurück: ```go func Parse(s string) (Date, error) func ParseWithLayout(s, layout string) (Date, error) func ParseRelative(s string) (Date, error) func FromUnix(sec int64) (Date, error) // kann bei Overflow fehlschlagen ``` **Convenience-Varianten (Panic bei Fehler):** ```go func MustParse(s string) Date // nur für Tests/Init ``` **Design-Regel:** Library darf NIE panicken (außer Must-Varianten). #### Clock-Abstraktion (für Testbarkeit) ```go // Clock ermöglicht Time-Injection für Tests type Clock interface { Now() Date From(t time.Time) Date } // DefaultClock verwendet time.Now() func NewClock() Clock // FixedClock für deterministische Tests func NewFixedClock(t time.Time) Clock ``` **Verwendung:** ```go // Produktion date := quando.Now() // verwendet DefaultClock // Tests clock := quando.NewFixedClock(time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC)) date := clock.Now() date2 := clock.From(otherTime).Add(2, quando.Days) ``` ### Öffentliche API (Package-Level Funktionen) ```go // Konstruktoren func Now() Date func From(t time.Time) Date func FromUnix(sec int64) (Date, error) // Parsing func Parse(s string) (Date, error) func ParseWithLayout(s, layout string) (Date, error) func ParseRelative(s string) (Date, error) func MustParse(s string) Date // Differenz func Diff(a, b time.Time) Duration // Clock-Factory (für Tests) func NewClock() Clock func NewFixedClock(t time.Time) Clock ``` ### Methoden auf `quando.Date` ```go // Arithmetik func (d Date) Add(value int, unit Unit) Date func (d Date) Sub(value int, unit Unit) Date // Snap-to func (d Date) StartOf(unit Unit) Date func (d Date) EndOf(unit Unit) Date func (d Date) Next(weekday time.Weekday) Date func (d Date) Prev(weekday time.Weekday) Date // Inspektion func (d Date) Info() DateInfo func (d Date) WeekNumber() int func (d Date) Quarter() int func (d Date) DayOfYear() int func (d Date) IsWeekend() bool func (d Date) IsLeapYear() bool func (d Date) Unix() int64 // Formatierung func (d Date) Format(format Format) string func (d Date) FormatLayout(layout string) string func (d Date) Lang(lang Lang) Date // Fluent API // Zeitzone func (d Date) In(location string) (Date, error) // Konvertierung func (d Date) Time() time.Time ``` ### Methoden auf `quando.Duration` (Differenz) ```go type Duration struct { // private fields } func (dur Duration) Seconds() int64 func (dur Duration) Minutes() int64 func (dur Duration) Hours() int64 func (dur Duration) Days() int func (dur Duration) Weeks() int func (dur Duration) Months() int func (dur Duration) MonthsFloat() float64 func (dur Duration) Years() int func (dur Duration) YearsFloat() float64 func (dur Duration) Human() string func (dur Duration) Human(lang Lang) string ``` ### Format-Konstanten ```go type Format int const ( ISO Format = iota // "2026-02-09" EU // "09.02.2026" US // "02/09/2026" Long // "February 9, 2026" (sprachabhängig) RFC2822 // "Mon, 09 Feb 2026 00:00:00 +0000" ) ``` --- ## 6. Benutzeroberfläche Nicht anwendbar – reine Go-Library ohne UI-Komponente. --- ## 7. Nicht-funktionale Anforderungen ### Verfügbarkeit und Uptime Nicht anwendbar – keine Server-Komponente. ### Graceful Shutdown und Signal-Handling Nicht anwendbar – keine Server-Komponente. ### Backup- und Recovery Nicht anwendbar – keine Datenpersistierung. ### Monitoring und Observability Nicht anwendbar – Library-Nutzer sind selbst verantwortlich für Monitoring. **Empfehlung für Library-Nutzer:** - Performance-kritische Operationen mit Benchmarks messen - Error-Rates von Parse-Funktionen loggen ### Logging-Strategie Keine interne Logging-Komponente. Library gibt Errors über Return-Values zurück, niemals über Logging. **Design-Rationale:** Libraries sollten keine Logs schreiben – das ist Aufgabe der Applikation. ### Deployment #### Repository-Struktur ``` quando/ ├── quando.go # Haupt-API ├── date.go # Date-Type und Core-Methoden ├── arithmetic.go # Add, Sub ├── snap.go # StartOf, EndOf, Next, Prev ├── diff.go # Differenz-Berechnung ├── inspect.go # WeekNumber, Quarter, etc. ├── format.go # Formatierung ├── parse.go # Parsing ├── clock.go # Clock-Abstraktion ├── i18n.go # Internationalisierung (EN, DE) ├── i18n_test.go ├── example_test.go # Godoc-Examples ├── quando_test.go # Unit-Tests ├── bench_test.go # Benchmarks ├── go.mod ├── go.sum ├── README.md ├── LICENSE (MIT) └── .github/ └── workflows/ └── ci.yml # GitHub Actions ``` #### Lizenz MIT License – Open Source geplant. #### Versionierung Semantic Versioning (semver): - `v0.x.x` während Phase 1 (API nicht stabil) - `v1.0.0` nach erfolgreichem Einsatz in DatesAPI v2 - Breaking Changes nur bei Major-Versions ### Skalierung und Load Balancing Nicht anwendbar – keine Server-Komponente. **Performance-Überlegungen:** - Library ist thread-safe (alle Operationen auf unveränderlichen Daten) - Keine Shared State – parallele Nutzung ohne Locks möglich - Geeignet für hochparallelisierte Workloads (Goroutines) --- ## 8. Qualitätssicherung ### Definition of Done Ein Feature ist "Done", wenn: 1. **Implementierung:** - Code folgt Go-Idiomen (go fmt, go vet, golangci-lint) - Alle exportierten Funktionen/Types dokumentiert (Godoc) - Fehlerbehandlung mit `error`-Return-Values (niemals panic) 2. **Tests:** - Unit-Tests für alle Funktionen (min. 95% Coverage) - Edge-Case-Tests (Schaltjahre, Monatsende, DST-Umstellungen) - Benchmarks für Performance-kritische Funktionen - Example-Tests für Godoc (in `example_test.go`) 3. **Dokumentierung:** - Godoc-Kommentare für alle Public APIs - README mit Code-Beispielen aktualisiert - Changelog-Eintrag 4. **Review:** - Code-Review abgeschlossen - CI/CD-Pipeline erfolgreich (Tests, Linting) ### Test-Anforderungen #### Unit-Tests **Mindest-Coverage:** 95% für alle Kalkulationsfunktionen **Kritische Test-Szenarien:** 1. **Datums-Arithmetik:** - Monatsende-Overflow (31. Jan + 1 Monat = 28. Feb) - Schaltjahr-Handling (29. Feb in Schaltjahren) - Negative Arithmetik (Subtraktion über Jahresgrenzen) - Verkettung (mehrere Add/Sub-Operationen) 2. **Snap-to/Ankerpunkte:** - StartOf/EndOf für alle Einheiten (Week, Month, Quarter, Year) - Next/Prev bei gleichem Wochentag (muss überspringen) - EndOf(Week) mit verschiedenen Wochenbeginn-Einstellungen 3. **Differenz-Berechnung:** - Differenz über Jahresgrenzen - Differenz über Schaltjahre - Negative Differenzen (date1 < date2) - Human-Format mit verschiedenen Granularitäten 4. **Parsing:** - Alle unterstützten Formate (ISO, EU, RFC2822) - Ungültige Eingaben (Error-Handling) - Mehrdeutige Formate (Slash ohne Jahr-Prefix) - Relative Ausdrücke (today, +2 days, etc.) 5. **Zeitzone & DST:** - Konvertierung zwischen Zeitzonen - DST-Umstellung (Add(1, Days) über DST-Grenze) - Ungültige Timezone-Namen (Error-Handling) 6. **Datums-Inspektion:** - WeekNumber für ISO 8601 (Woche 1 = erste Woche mit Donnerstag) - Quarter-Berechnung für Grenzfälle (31. März, 30. Juni, etc.) - IsLeapYear für alle Regeln (durch 4, außer Jahrhundert, außer durch 400) #### Benchmarks Benchmarks für alle Performance-kritischen Funktionen: ```go func BenchmarkAdd(b *testing.B) func BenchmarkDiff(b *testing.B) func BenchmarkParse(b *testing.B) func BenchmarkFormat(b *testing.B) ``` **Performance-Ziele:** - Add/Sub: < 1 µs - Diff: < 2 µs - Parse: < 10 µs - Format: < 10 µs #### Integration mit Testcontainers Nicht anwendbar – keine Datenbank/externe Services. ### Launch-Kriterien **Phase 1 Launch (v0.1.0):** - Alle Must-have Features implementiert - Test-Coverage > 95% - Benchmarks erfüllen Performance-Ziele - README mit Beispielen vollständig - CI/CD-Pipeline funktioniert - Code-Review abgeschlossen **Production-Ready (v1.0.0):** - Erfolgreich in DatesAPI v2 integriert - Keine kritischen Bugs im Production-Einsatz (4+ Wochen) - API-Stabilität erreicht (keine Breaking Changes mehr geplant) - Umfangreiche Dokumentation (README, Godoc, Examples) ### Abnahme-Prozess 1. **Selbst-Review:** Entwickler prüft eigenen Code gegen DoD 2. **Code-Review:** Mindestens ein Review durch anderen Go-Entwickler 3. **CI/CD:** Alle automatisierten Tests und Lints bestanden 4. **Integration-Test:** Verwendung in DatesAPI v2 (Smoke-Test) 5. **Abnahme:** Product Owner prüft Feature gegen Acceptance Criteria --- ## 9. Technische Implementierungshinweise ### Go-Projektstruktur und Package-Layout **Flat Package-Struktur:** ``` quando/ ├── quando.go # Package-Level Funktionen (Now, From, Parse, Diff) ├── date.go # Date-Type, Core-Methoden, Konvertierung ├── arithmetic.go # Add, Sub (Logik für Monatsende-Overflow) ├── snap.go # StartOf, EndOf, Next, Prev ├── diff.go # Duration-Type, Differenz-Berechnung ├── inspect.go # WeekNumber, Quarter, DayOfYear, etc. ├── format.go # Format, FormatLayout, Preset-Konstanten ├── parse.go # Parse, ParseWithLayout, ParseRelative ├── clock.go # Clock-Interface, DefaultClock, FixedClock ├── i18n.go # Internationalisierung (EN, DE) ├── errors.go # Custom Error-Types └── internal/ └── calc/ # Interne Hilfs-Funktionen (nicht exportiert) ``` **Design-Rationale:** - **Flat Package:** Alle Funktionen unter `quando.*` verfügbar – einfache Benutzung - **Keine Sub-Packages:** Vermeidet zyklische Dependencies und komplexe Imports - **internal/:** Für nicht-exportierte Hilfs-Logik (z.B. Schaltjahr-Berechnung, Kalender-Arithmetik) - **Ausnahme:** Wenn i18n-Daten sehr groß werden (22 Sprachen), optionales `quando/lang` Sub-Package für späteren Import ### Concurrency-Patterns **Thread-Safety:** - Alle `quando.Date`-Operationen sind immutable – jede Operation gibt ein neues `Date` zurück - Keine Shared State, keine Mutexes notwendig - Goroutine-safe ohne zusätzlichen Aufwand **Design-Rationale:** - Immutability vermeidet Data-Races und macht Library goroutine-safe - Funktionaler Stil (Fluent API) begünstigt Immutability - Performance-Overhead minimal (Stack-Allocation für kleine Structs) **Nicht verwenden:** - Keine Goroutines innerhalb der Library (Library-Code sollte synchron sein) - Keine Channels, keine `errgroup` – das ist Aufgabe der Applikation ### Error-Handling-Strategie **Prinzipien:** 1. **Niemals panic (außer Must-Varianten):** Alle Errors über Return-Values 2. **Sentinel Errors für bekannte Fehler:** ```go var ( ErrInvalidFormat = errors.New("invalid date format") ErrInvalidTimezone = errors.New("invalid timezone") ErrOverflow = errors.New("date overflow") ) ``` 3. **Wrapped Errors für Kontext:** ```go return Date{}, fmt.Errorf("parsing date: %w", err) ``` **Error-Kategorien:** - **Parse-Errors:** Ungültige Formate, mehrdeutige Eingaben - **Timezone-Errors:** Unbekannte IANA-Namen - **Overflow-Errors:** Datums-Arithmetik außerhalb des Go-Range ### Dependency Injection und Konfiguration **Clock-Pattern (für Tests):** ```go // Production-Code date := quando.Now() // Test-Code clock := quando.NewFixedClock(time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC)) date := clock.Now() ``` **Konfiguration für Wochenbeginn (optional):** ```go // Default: Montag (ISO 8601) date.StartOf(quando.Week) // Optional: Konfigurierbarer Wochenbeginn (Nice-to-have) cfg := quando.Config{WeekStartDay: time.Sunday} date := quando.WithConfig(cfg).From(time.Now()).StartOf(quando.Week) ``` **Design-Entscheidung:** Konfiguration ist Nice-to-have für Phase 1. Default-Verhalten (ISO 8601, UTC) sollte für 95% der Use Cases ausreichen. ### PostgreSQL Connection Pooling Nicht anwendbar – keine Datenbank. ### Entwicklungs-Prioritäten **Phase 1 Reihenfolge:** 1. **Woche 1-2: Core-Infrastruktur** - `Date`-Type, `From()`, `Time()`, `Unix()`, `FromUnix()` - Clock-Abstraktion (`NewClock`, `NewFixedClock`) - Unit-Tests für Konvertierung 2. **Woche 3-4: Arithmetik** - `Add()`, `Sub()` für alle Einheiten - Monatsende-Overflow-Logik - Edge-Case-Tests (Schaltjahre, Monatsgrenzen) 3. **Woche 5: Snap-to** - `StartOf()`, `EndOf()` für Week, Month, Quarter, Year - `Next()`, `Prev()` für Weekdays - Tests für Wochenbeginn-Konfiguration 4. **Woche 6: Differenz** - `Diff()`, `Duration`-Type - `.Days()`, `.Months()`, `.Years()` (int) - `.MonthsFloat()`, `.YearsFloat()` (float64) 5. **Woche 7: Human-Format** - `.Human()` mit adaptiver Granularität - i18n-Infrastruktur (EN, DE) - Tests für alle Granularitäts-Stufen 6. **Woche 8: Parsing** - `Parse()` (automatisch) - `ParseWithLayout()` (explizit) - Mehrdeutigkeits-Handling 7. **Woche 9: Formatierung** - `Format()` mit Presets (ISO, EU, US, Long, RFC2822) - `FormatLayout()` für Custom Layouts - `.Lang()` für mehrsprachige Formatierung 8. **Woche 10: Parsing (relativ)** - `ParseRelative()` (today, tomorrow, +X days) - Tests für alle Ausdrücke 9. **Woche 11: Inspektion** - `.WeekNumber()`, `.Quarter()`, `.DayOfYear()` - `.IsWeekend()`, `.IsLeapYear()` - ISO 8601 Compliance-Tests 10. **Woche 12: Zeitzone & DST** - `.In()` für Timezone-Konvertierung - DST-Handling-Tests (Add über DST-Grenze) - Error-Handling für ungültige Timezones 11. **Woche 13-14: Polishing** - Benchmarks optimieren - Dokumentation vervollständigen (README, Godoc) - Example-Tests schreiben - CI/CD-Pipeline finalisieren **Total: 14 Wochen (ca. 3,5 Monate)** ### Potenzielle Risiken und Herausforderungen | Risiko | Wahrscheinlichkeit | Impact | Mitigation | |--------|-------------------|--------|------------| | **Monatsende-Overflow-Logik fehlerhaft** | Mittel | Hoch | Umfangreiche Edge-Case-Tests, Referenz-Implementierung von Moment.js/Carbon studieren | | **DST-Handling inkorrekt** | Mittel | Hoch | Tests für alle DST-Umstellungen 2024-2030, Vergleich mit `time.Time` stdlib | | **Parsing-Ambiguitäten nicht erkannt** | Niedrig | Mittel | Klare Dokumentation, strikte Error-Rückgabe bei Mehrdeutigkeit | | **Performance-Ziele nicht erreicht** | Niedrig | Mittel | Frühzeitige Benchmarks, Optimierung vor Feature-Freeze | | **i18n-Daten zu groß (Binary Size)** | Niedrig | Niedrig | Optional: Sub-Package `quando/lang` für lazy-loading | | **API-Instabilität (Breaking Changes)** | Mittel | Hoch | v0.x.x während Phase 1, Feedback von DatesAPI v2 Team einholen vor v1.0.0 | | **Zeit-Überziehung durch Scope-Creep** | Mittel | Mittel | Strikte Priorisierung: Must-have vs. Nice-to-have, keine Features außerhalb Spec | --- ## Anhang A: Code-Beispiele ### Beispiel 1: Komplexe Verkettung ```go // Berechne: "Erster Montag des übernächsten Quartals" date := quando.Now(). Add(2, quando.Quarters). StartOf(quando.Quarter). Next(time.Monday) fmt.Println(date.Format(quando.ISO)) ``` ### Beispiel 2: Differenz mit Human-Format ```go start := quando.MustParse("2025-01-15") end := quando.Now() diff := quando.Diff(start.Time(), end.Time()) fmt.Printf("Differenz: %s\n", diff.Human(quando.LangDE)) // Output: "Differenz: 13 Monate, 2 Tage" ``` ### Beispiel 3: Zeitzone-Konvertierung mit DST ```go // UTC-Datum utcDate := quando.MustParse("2026-03-31T01:00:00Z") // Konvertierung nach Berlin (CET → CEST-Umstellung am 29. März 2026) berlinDate, _ := utcDate.In("Europe/Berlin") // +1 Tag (DST-safe) nextDay := berlinDate.Add(1, quando.Days) fmt.Println(nextDay.Format(quando.RFC2822)) // Output: "Wed, 01 Apr 2026 01:00:00 +0200" (CEST) ``` ### Beispiel 4: Parsing-Workflow ```go func ParseUserInput(input string) (quando.Date, error) { // Versuche automatisches Parsing date, err := quando.Parse(input) if err == nil { return date, nil } // Fallback: Relative Ausdrücke date, err = quando.ParseRelative(input) if err == nil { return date, nil } // Fallback: Explizites Format (EU) date, err = quando.ParseWithLayout(input, "02.01.2006") if err == nil { return date, nil } return quando.Date{}, fmt.Errorf("unable to parse: %s", input) } ``` ### Beispiel 5: Deterministische Tests ```go func TestBusinessLogic(t *testing.T) { // Fixed Clock für deterministische Tests clock := quando.NewFixedClock(time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC)) // Business-Logik mit injizierter Zeit result := CalculateDeadline(clock) expected := clock.Now().Add(30, quando.Days).EndOf(quando.Month) assert.Equal(t, expected, result) } func CalculateDeadline(clock quando.Clock) quando.Date { return clock.Now().Add(30, quando.Days).EndOf(quando.Month) } ``` --- ## Anhang B: Vergleich zu time.Time | Operation | time.Time (stdlib) | quando | |-----------|-------------------|--------| | **+2 Monate, -3 Tage** | 5+ Zeilen Code | `quando.Now().Add(2, Months).Sub(3, Days)` | | **Ende des Quartals** | Manuell rechnen (Quartal bestimmen, letzter Tag) | `quando.Now().EndOf(Quarter)` | | **Differenz menschenlesbar** | Nicht verfügbar | `Diff(a, b).Human()` | | **Kalenderwoche** | Nicht verfügbar | `.WeekNumber()` | | **Quartal** | Nicht verfügbar | `.Quarter()` | | **Nächster Montag** | Komplexe Loop-Logik | `quando.Now().Next(time.Monday)` | | **Parsing (automatisch)** | Explizites Layout nötig | `quando.Parse("09.02.2026")` | | **Mehrsprachige Formatierung** | Nicht verfügbar | `.Lang(DE).Format(Long)` | **Fazit:** `quando` reduziert typische Datums-Operationen von 5-10 Zeilen auf 1 Zeile, bei gleichbleibender Typ-Sicherheit und Performance. --- ## Anhang C: Migration-Guide (für DatesAPI v2) ### Schritt 1: Dependency hinzufügen ```bash go get code.beautifulmachines.dev/quando ``` ### Schritt 2: Import ```go import "code.beautifulmachines.dev/quando" ``` ### Schritt 3: API-Endpunkte migrieren **Vorher (DatesAPI v1):** ```go // Manuelle Datumsberechnung func HandleAddDays(w http.ResponseWriter, r *http.Request) { date := parseDate(r.URL.Query().Get("date")) days := parseInt(r.URL.Query().Get("days")) result := date.AddDate(0, 0, days) writeJSON(w, result) } ``` **Nachher (DatesAPI v2 mit quando):** ```go func HandleAddDays(w http.ResponseWriter, r *http.Request) { date, _ := quando.Parse(r.URL.Query().Get("date")) days := parseInt(r.URL.Query().Get("days")) result := date.Add(days, quando.Days) writeJSON(w, result) } ``` **Vorteil:** Konsistente API, weniger Fehleranfälligkeit, bessere Lesbarkeit. --- ## Anhang D: Performance-Benchmarks (Zielwerte) ``` BenchmarkAdd-8 1000000000 0.8 ns/op 0 B/op 0 allocs/op BenchmarkSub-8 1000000000 0.8 ns/op 0 B/op 0 allocs/op BenchmarkStartOf-8 500000000 1.2 ns/op 0 B/op 0 allocs/op BenchmarkEndOf-8 500000000 1.2 ns/op 0 B/op 0 allocs/op BenchmarkDiff-8 500000000 1.5 ns/op 0 B/op 0 allocs/op BenchmarkDiffFloat-8 300000000 2.0 ns/op 0 B/op 0 allocs/op BenchmarkFormat-8 200000000 8.0 ns/op 64 B/op 1 allocs/op BenchmarkParse-8 100000000 9.5 ns/op 48 B/op 1 allocs/op BenchmarkParseRelative-8 50000000 18.0 ns/op 96 B/op 2 allocs/op BenchmarkHuman-8 100000000 12.0 ns/op 128 B/op 2 allocs/op ``` **Ziel:** Alle Operationen unter 20 ns/op, minimale Allocations. --- ## Änderungshistorie | Version | Datum | Änderungen | |---------|-------|------------| | 1.0 | 2026-02-09 | Initiales PRD erstellt |