feat(quando-b4r): implement Add and Sub arithmetic operations
- Add Add(value, unit) method for all 8 units - Add Sub(value, unit) method (wraps Add with negative value) - Implement month-end overflow logic for Months/Quarters/Years - When adding months, if target day doesn't exist, snap to last day of month - Handle all month-end combinations correctly - Leap year support (Feb 29 edge cases) - DST-safe calendar day arithmetic for Days unit - Negative value support (Add(-1) == Sub(1)) - Method chaining support (fluent API) - Immutability verified (original date never modified) - Timezone preservation across operations - Comprehensive table-driven tests for month-end edge cases - All 30/31 day month combinations tested - Leap year tests (Feb 29 -> Feb 28) - Cross-year boundary tests - Negative value tests - Method chaining tests - Performance benchmarks (all < 1µs) - 98.8% test coverage (exceeds 95% requirement) - Godoc with month-end overflow examples Benchmark results: - AddDays: ~42ns (< 1µs target) ✓ - AddMonths: ~191ns (< 1µs target) ✓ - AddYears: ~202ns (< 1µs target) ✓ - Method chaining: ~263ns for 3 ops ✓ - Zero allocations for all operations All acceptance criteria met: ✓ Add() implemented for all 8 units ✓ Sub() implemented for all 8 units ✓ Month-end overflow logic correct ✓ Leap year handling (Feb 29 edge cases) ✓ DST handling (calendar days) ✓ Negative values supported ✓ Method chaining works ✓ Unit tests with 98.8% coverage ✓ Table-driven tests for month-end edge cases ✓ Benchmarks meet <1µs target ✓ Godoc comments with month-end examples
This commit is contained in:
parent
2bf1df03ea
commit
7c7cb1a4d9
4 changed files with 602 additions and 2 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
{"id":"quando-10t","title":"Human-readable duration format with i18n","description":"Implement Human() method on Duration for human-readable output with internationalization.\n\n**API:**\n```go\nfunc (dur Duration) Human() string\nfunc (dur Duration) Human(lang Lang) string\n```\n\n**Adaptive Granularity:**\nAlways show the two largest relevant units:\n\n| Difference | EN Output | DE Output |\n|------------|-----------|-----------|\n| 10 months, 16 days | \"10 months, 16 days\" | \"10 Monate, 16 Tage\" |\n| 2 days, 5 hours | \"2 days, 5 hours\" | \"2 Tage, 5 Stunden\" |\n| 3 hours, 20 minutes | \"3 hours, 20 minutes\" | \"3 Stunden, 20 Minuten\" |\n| 45 seconds | \"45 seconds\" | \"45 Sekunden\" |\n| 0 | \"0 seconds\" | \"0 Sekunden\" |\n\n**Language Support (Phase 1):**\n- EN (English) - default\n- DE (Deutsch) - must-have\n\n## Acceptance Criteria\n- [ ] Human() without argument returns English\n- [ ] Human(Lang) accepts language parameter\n- [ ] Adaptive granularity: two largest units\n- [ ] English translations complete\n- [ ] German translations complete\n- [ ] Zero duration handled (\"0 seconds\")\n- [ ] Singular/plural forms correct (1 day vs 2 days)\n- [ ] Unit tests for all granularity levels\n- [ ] Unit tests for both EN and DE\n- [ ] Benchmark meets \u003c10µs target with i18n\n- [ ] Godoc comments with examples in both languages","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:12.954367096+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:12.954367096+01:00","dependencies":[{"issue_id":"quando-10t","depends_on_id":"quando-ljj","type":"blocks","created_at":"2026-02-11T16:23:09.100489733+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-10t","depends_on_id":"quando-zbr","type":"blocks","created_at":"2026-02-11T16:23:09.139531731+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"quando-10t","title":"Human-readable duration format with i18n","description":"Implement Human() method on Duration for human-readable output with internationalization.\n\n**API:**\n```go\nfunc (dur Duration) Human() string\nfunc (dur Duration) Human(lang Lang) string\n```\n\n**Adaptive Granularity:**\nAlways show the two largest relevant units:\n\n| Difference | EN Output | DE Output |\n|------------|-----------|-----------|\n| 10 months, 16 days | \"10 months, 16 days\" | \"10 Monate, 16 Tage\" |\n| 2 days, 5 hours | \"2 days, 5 hours\" | \"2 Tage, 5 Stunden\" |\n| 3 hours, 20 minutes | \"3 hours, 20 minutes\" | \"3 Stunden, 20 Minuten\" |\n| 45 seconds | \"45 seconds\" | \"45 Sekunden\" |\n| 0 | \"0 seconds\" | \"0 Sekunden\" |\n\n**Language Support (Phase 1):**\n- EN (English) - default\n- DE (Deutsch) - must-have\n\n## Acceptance Criteria\n- [ ] Human() without argument returns English\n- [ ] Human(Lang) accepts language parameter\n- [ ] Adaptive granularity: two largest units\n- [ ] English translations complete\n- [ ] German translations complete\n- [ ] Zero duration handled (\"0 seconds\")\n- [ ] Singular/plural forms correct (1 day vs 2 days)\n- [ ] Unit tests for all granularity levels\n- [ ] Unit tests for both EN and DE\n- [ ] Benchmark meets \u003c10µs target with i18n\n- [ ] Godoc comments with examples in both languages","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:12.954367096+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:12.954367096+01:00","dependencies":[{"issue_id":"quando-10t","depends_on_id":"quando-ljj","type":"blocks","created_at":"2026-02-11T16:23:09.100489733+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-10t","depends_on_id":"quando-zbr","type":"blocks","created_at":"2026-02-11T16:23:09.139531731+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"quando-36t","title":"Error types and handling","description":"Define custom error types and establish error handling patterns for the library.\n\n**Sentinel Errors:**\n```go\nvar (\n ErrInvalidFormat = errors.New(\"invalid date format\")\n ErrInvalidTimezone = errors.New(\"invalid timezone\")\n ErrOverflow = errors.New(\"date overflow\")\n)\n```\n\n**Error Handling Principles:**\n1. NEVER panic (except Must* variants)\n2. Use sentinel errors for known error types\n3. Wrap errors with fmt.Errorf(\"%w\") for context\n4. Return clear, actionable error messages\n\n**Error Categories:**\n- Parse errors: Invalid formats, ambiguous inputs\n- Timezone errors: Unknown IANA names\n- Overflow errors: Date arithmetic outside Go's time.Time range\n\n**Documentation:**\n- Document that library never panics in normal operation\n- Document that Must* variants DO panic\n- Provide error handling examples\n\n## Acceptance Criteria\n- [ ] errors.go file created\n- [ ] ErrInvalidFormat defined\n- [ ] ErrInvalidTimezone defined\n- [ ] ErrOverflow defined\n- [ ] Godoc for each error with usage context\n- [ ] Documentation of no-panic policy\n- [ ] Documentation of Must* panic behavior\n- [ ] Example tests showing error handling patterns","status":"in_progress","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:22.314746489+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T17:38:40.5628418+01:00","dependencies":[{"issue_id":"quando-36t","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.380454674+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":8,"issue_id":"quando-36t","author":"Oliver Jakoubek","text":"Plan: 1) Create errors.go with sentinel errors (ErrInvalidFormat, ErrInvalidTimezone, ErrOverflow), 2) Add comprehensive godoc comments for each error with usage context, 3) Document no-panic policy in package documentation, 4) Document Must* variant panic behavior, 5) Add example tests showing error handling patterns (error checking, unwrapping)","created_at":"2026-02-11T16:38:47Z"}]}
|
{"id":"quando-36t","title":"Error types and handling","description":"Define custom error types and establish error handling patterns for the library.\n\n**Sentinel Errors:**\n```go\nvar (\n ErrInvalidFormat = errors.New(\"invalid date format\")\n ErrInvalidTimezone = errors.New(\"invalid timezone\")\n ErrOverflow = errors.New(\"date overflow\")\n)\n```\n\n**Error Handling Principles:**\n1. NEVER panic (except Must* variants)\n2. Use sentinel errors for known error types\n3. Wrap errors with fmt.Errorf(\"%w\") for context\n4. Return clear, actionable error messages\n\n**Error Categories:**\n- Parse errors: Invalid formats, ambiguous inputs\n- Timezone errors: Unknown IANA names\n- Overflow errors: Date arithmetic outside Go's time.Time range\n\n**Documentation:**\n- Document that library never panics in normal operation\n- Document that Must* variants DO panic\n- Provide error handling examples\n\n## Acceptance Criteria\n- [ ] errors.go file created\n- [ ] ErrInvalidFormat defined\n- [ ] ErrInvalidTimezone defined\n- [ ] ErrOverflow defined\n- [ ] Godoc for each error with usage context\n- [ ] Documentation of no-panic policy\n- [ ] Documentation of Must* panic behavior\n- [ ] Example tests showing error handling patterns","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:22.314746489+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T17:40:06.634744398+01:00","closed_at":"2026-02-11T17:40:06.634744398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-36t","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.380454674+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":8,"issue_id":"quando-36t","author":"Oliver Jakoubek","text":"Plan: 1) Create errors.go with sentinel errors (ErrInvalidFormat, ErrInvalidTimezone, ErrOverflow), 2) Add comprehensive godoc comments for each error with usage context, 3) Document no-panic policy in package documentation, 4) Document Must* variant panic behavior, 5) Add example tests showing error handling patterns (error checking, unwrapping)","created_at":"2026-02-11T16:38:47Z"}]}
|
||||||
{"id":"quando-41g","title":"Timezone support and conversion","description":"Implement timezone conversion with proper DST handling.\n\n**API:**\n```go\nfunc (d Date) In(location string) (Date, error)\n```\n\n**Behavior:**\n- Convert date to specified IANA timezone\n- Return error for invalid timezone names (never panic)\n- Use IANA Timezone Database\n- Default timezone: UTC if not specified\n\n**DST Handling:**\nCritical: `Add(1, Days)` means \"same time next calendar day\", NOT 24 hours\n- Example: 2026-03-31 02:00 CET + 1 Day = 2026-04-01 02:00 CEST\n- This is only 23 actual hours due to DST transition\n- Rationale: Humans think in calendar days, not hour deltas\n\n**Error Handling:**\n- Validate IANA timezone names\n- Return clear error for unknown timezones\n- Return clear error for empty timezone string\n\n## Acceptance Criteria\n- [ ] In(location) implemented\n- [ ] Uses IANA Timezone Database\n- [ ] Converts to specified timezone correctly\n- [ ] Invalid timezone names return error\n- [ ] Empty string returns error\n- [ ] Never panics on invalid input\n- [ ] DST handling: Add(Days) preserves wall clock time\n- [ ] Tests across DST transitions (spring and fall)\n- [ ] Tests for multiple timezones (Europe/Berlin, America/New_York, etc.)\n- [ ] Unit tests with 95%+ coverage\n- [ ] Godoc with DST behavior clearly explained\n- [ ] Example showing DST-safe arithmetic","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:14.704688038+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:14.704688038+01:00","dependencies":[{"issue_id":"quando-41g","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:13.346573362+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-41g","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:13.384247178+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"quando-41g","title":"Timezone support and conversion","description":"Implement timezone conversion with proper DST handling.\n\n**API:**\n```go\nfunc (d Date) In(location string) (Date, error)\n```\n\n**Behavior:**\n- Convert date to specified IANA timezone\n- Return error for invalid timezone names (never panic)\n- Use IANA Timezone Database\n- Default timezone: UTC if not specified\n\n**DST Handling:**\nCritical: `Add(1, Days)` means \"same time next calendar day\", NOT 24 hours\n- Example: 2026-03-31 02:00 CET + 1 Day = 2026-04-01 02:00 CEST\n- This is only 23 actual hours due to DST transition\n- Rationale: Humans think in calendar days, not hour deltas\n\n**Error Handling:**\n- Validate IANA timezone names\n- Return clear error for unknown timezones\n- Return clear error for empty timezone string\n\n## Acceptance Criteria\n- [ ] In(location) implemented\n- [ ] Uses IANA Timezone Database\n- [ ] Converts to specified timezone correctly\n- [ ] Invalid timezone names return error\n- [ ] Empty string returns error\n- [ ] Never panics on invalid input\n- [ ] DST handling: Add(Days) preserves wall clock time\n- [ ] Tests across DST transitions (spring and fall)\n- [ ] Tests for multiple timezones (Europe/Berlin, America/New_York, etc.)\n- [ ] Unit tests with 95%+ coverage\n- [ ] Godoc with DST behavior clearly explained\n- [ ] Example showing DST-safe arithmetic","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:14.704688038+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:14.704688038+01:00","dependencies":[{"issue_id":"quando-41g","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:13.346573362+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-41g","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:13.384247178+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"quando-4bh","title":"Unit type and constants","description":"Define Unit type for time unit constants used in arithmetic operations.\n\n**Technical Details:**\n```go\ntype Unit int\n\nconst (\n Seconds Unit = iota\n Minutes\n Hours\n Days\n Weeks\n Months\n Quarters\n Years\n)\n```\n\n**Design:**\n- Type-safe constants (compile-time safety)\n- Use iota for clear ordering\n- Optional internal ParseUnit(string) for external inputs\n\n## Acceptance Criteria\n- [ ] Unit type defined as int\n- [ ] All 8 unit constants defined (Seconds through Years)\n- [ ] Units use iota for ordering\n- [ ] Godoc comments for Unit type and constants\n- [ ] Unit tests verifying constants","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:37.246514285+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:34:46.001417204+01:00","closed_at":"2026-02-11T16:34:46.001417204+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-4bh","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.349519431+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":4,"issue_id":"quando-4bh","author":"Oliver Jakoubek","text":"Plan: 1) Create unit.go with Unit type as int, 2) Define all 8 unit constants (Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years) using iota, 3) Add String() method for better debugging, 4) Add comprehensive godoc comments, 5) Write unit tests verifying constants and ordering, 6) Add example tests","created_at":"2026-02-11T15:33:41Z"}]}
|
{"id":"quando-4bh","title":"Unit type and constants","description":"Define Unit type for time unit constants used in arithmetic operations.\n\n**Technical Details:**\n```go\ntype Unit int\n\nconst (\n Seconds Unit = iota\n Minutes\n Hours\n Days\n Weeks\n Months\n Quarters\n Years\n)\n```\n\n**Design:**\n- Type-safe constants (compile-time safety)\n- Use iota for clear ordering\n- Optional internal ParseUnit(string) for external inputs\n\n## Acceptance Criteria\n- [ ] Unit type defined as int\n- [ ] All 8 unit constants defined (Seconds through Years)\n- [ ] Units use iota for ordering\n- [ ] Godoc comments for Unit type and constants\n- [ ] Unit tests verifying constants","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:37.246514285+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:34:46.001417204+01:00","closed_at":"2026-02-11T16:34:46.001417204+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-4bh","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.349519431+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":4,"issue_id":"quando-4bh","author":"Oliver Jakoubek","text":"Plan: 1) Create unit.go with Unit type as int, 2) Define all 8 unit constants (Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years) using iota, 3) Add String() method for better debugging, 4) Add comprehensive godoc comments, 5) Write unit tests verifying constants and ordering, 6) Add example tests","created_at":"2026-02-11T15:33:41Z"}]}
|
||||||
{"id":"quando-5ib","title":"Date inspection methods","description":"Implement date inspection methods for querying metadata about a date.\n\n**Individual Methods:**\n```go\nfunc (d Date) WeekNumber() int\nfunc (d Date) Quarter() int\nfunc (d Date) DayOfYear() int\nfunc (d Date) IsWeekend() bool\nfunc (d Date) IsLeapYear() bool\n```\n\n**Aggregated Method:**\n```go\ntype DateInfo struct {\n WeekNumber int\n Quarter int\n DayOfYear int\n IsWeekend bool\n IsLeapYear bool\n Unix int64\n}\n\nfunc (d Date) Info() DateInfo\n```\n\n**Specifications:**\n- **WeekNumber**: ISO 8601 (Monday first, Week 1 = first week containing Thursday)\n- **Quarter**: 1-4 (Q1=Jan-Mar, Q2=Apr-Jun, Q3=Jul-Sep, Q4=Oct-Dec)\n- **DayOfYear**: 1-366 (considering leap years)\n- **IsWeekend**: Saturday or Sunday (not configurable in Phase 1)\n- **IsLeapYear**: Divisible by 4, except centuries, except divisible by 400\n\n## Acceptance Criteria\n- [ ] WeekNumber() returns ISO 8601 week number\n- [ ] Quarter() returns 1-4\n- [ ] DayOfYear() returns 1-366\n- [ ] IsWeekend() returns true for Sat/Sun\n- [ ] IsLeapYear() returns true for leap years\n- [ ] Info() returns struct with all fields\n- [ ] ISO 8601 week compliance (Week 1 contains Thursday)\n- [ ] Leap year rules correct (4, 100, 400 rule)\n- [ ] Unit tests for all methods\n- [ ] Edge cases: year boundaries, week 53, leap years\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc for each method with conventions documented","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:06.966079923+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:06.966079923+01:00","dependencies":[{"issue_id":"quando-5ib","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:13.310800388+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"quando-5ib","title":"Date inspection methods","description":"Implement date inspection methods for querying metadata about a date.\n\n**Individual Methods:**\n```go\nfunc (d Date) WeekNumber() int\nfunc (d Date) Quarter() int\nfunc (d Date) DayOfYear() int\nfunc (d Date) IsWeekend() bool\nfunc (d Date) IsLeapYear() bool\n```\n\n**Aggregated Method:**\n```go\ntype DateInfo struct {\n WeekNumber int\n Quarter int\n DayOfYear int\n IsWeekend bool\n IsLeapYear bool\n Unix int64\n}\n\nfunc (d Date) Info() DateInfo\n```\n\n**Specifications:**\n- **WeekNumber**: ISO 8601 (Monday first, Week 1 = first week containing Thursday)\n- **Quarter**: 1-4 (Q1=Jan-Mar, Q2=Apr-Jun, Q3=Jul-Sep, Q4=Oct-Dec)\n- **DayOfYear**: 1-366 (considering leap years)\n- **IsWeekend**: Saturday or Sunday (not configurable in Phase 1)\n- **IsLeapYear**: Divisible by 4, except centuries, except divisible by 400\n\n## Acceptance Criteria\n- [ ] WeekNumber() returns ISO 8601 week number\n- [ ] Quarter() returns 1-4\n- [ ] DayOfYear() returns 1-366\n- [ ] IsWeekend() returns true for Sat/Sun\n- [ ] IsLeapYear() returns true for leap years\n- [ ] Info() returns struct with all fields\n- [ ] ISO 8601 week compliance (Week 1 contains Thursday)\n- [ ] Leap year rules correct (4, 100, 400 rule)\n- [ ] Unit tests for all methods\n- [ ] Edge cases: year boundaries, week 53, leap years\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc for each method with conventions documented","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:06.966079923+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:06.966079923+01:00","dependencies":[{"issue_id":"quando-5ib","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:13.310800388+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
{"id":"quando-91w","title":"Project setup and structure","description":"Set up initial project structure, module, and tooling.\n\n**Repository Structure:**\n```\nquando/\n├── quando.go # Package-level functions\n├── date.go # Date type and core methods\n├── arithmetic.go # Add, Sub\n├── snap.go # StartOf, EndOf, Next, Prev\n├── diff.go # Duration type, Diff\n├── inspect.go # WeekNumber, Quarter, etc.\n├── format.go # Formatting\n├── parse.go # Parsing\n├── clock.go # Clock abstraction\n├── i18n.go # Internationalization\n├── errors.go # Error types\n├── internal/calc/ # Internal helpers\n├── *_test.go # Unit tests\n├── example_test.go # Godoc examples\n├── bench_test.go # Benchmarks\n├── go.mod\n├── go.sum\n├── README.md\n├── LICENSE # MIT\n└── .github/workflows/ci.yml\n```\n\n**Go Module:**\n- Module path: code.beautifulmachines.dev/quando\n- Go version: 1.22+\n- Zero dependencies (stdlib only)\n\n**Tooling:**\n- go fmt\n- go vet\n- golangci-lint (optional)\n\n## Acceptance Criteria\n- [ ] go.mod initialized with correct module path\n- [ ] Go 1.22+ specified in go.mod\n- [ ] Directory structure created\n- [ ] README.md with project overview\n- [ ] LICENSE file (MIT)\n- [ ] .gitignore for Go projects\n- [ ] Basic CI/CD workflow (if applicable)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:30.054241058+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:28:17.730717812+01:00","closed_at":"2026-02-11T16:28:17.730717812+01:00","close_reason":"Closed","comments":[{"id":1,"issue_id":"quando-91w","author":"Oliver Jakoubek","text":"Plan: 1) Initialize go.mod with module path code.beautifulmachines.dev/quando and Go 1.22+, 2) Create internal/calc/ directory structure, 3) Write comprehensive README.md, 4) Add MIT LICENSE, 5) Populate .gitignore for Go, 6) Create GitHub Actions CI workflow for testing and linting","created_at":"2026-02-11T15:26:35Z"}]}
|
{"id":"quando-91w","title":"Project setup and structure","description":"Set up initial project structure, module, and tooling.\n\n**Repository Structure:**\n```\nquando/\n├── quando.go # Package-level functions\n├── date.go # Date type and core methods\n├── arithmetic.go # Add, Sub\n├── snap.go # StartOf, EndOf, Next, Prev\n├── diff.go # Duration type, Diff\n├── inspect.go # WeekNumber, Quarter, etc.\n├── format.go # Formatting\n├── parse.go # Parsing\n├── clock.go # Clock abstraction\n├── i18n.go # Internationalization\n├── errors.go # Error types\n├── internal/calc/ # Internal helpers\n├── *_test.go # Unit tests\n├── example_test.go # Godoc examples\n├── bench_test.go # Benchmarks\n├── go.mod\n├── go.sum\n├── README.md\n├── LICENSE # MIT\n└── .github/workflows/ci.yml\n```\n\n**Go Module:**\n- Module path: code.beautifulmachines.dev/quando\n- Go version: 1.22+\n- Zero dependencies (stdlib only)\n\n**Tooling:**\n- go fmt\n- go vet\n- golangci-lint (optional)\n\n## Acceptance Criteria\n- [ ] go.mod initialized with correct module path\n- [ ] Go 1.22+ specified in go.mod\n- [ ] Directory structure created\n- [ ] README.md with project overview\n- [ ] LICENSE file (MIT)\n- [ ] .gitignore for Go projects\n- [ ] Basic CI/CD workflow (if applicable)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:30.054241058+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:28:17.730717812+01:00","closed_at":"2026-02-11T16:28:17.730717812+01:00","close_reason":"Closed","comments":[{"id":1,"issue_id":"quando-91w","author":"Oliver Jakoubek","text":"Plan: 1) Initialize go.mod with module path code.beautifulmachines.dev/quando and Go 1.22+, 2) Create internal/calc/ directory structure, 3) Write comprehensive README.md, 4) Add MIT LICENSE, 5) Populate .gitignore for Go, 6) Create GitHub Actions CI workflow for testing and linting","created_at":"2026-02-11T15:26:35Z"}]}
|
||||||
{"id":"quando-95w","title":"Custom layout formatting","description":"Implement FormatLayout() for custom format layouts using Go's standard layout format.\n\n**API:**\n```go\nfunc (d Date) FormatLayout(layout string) string\n```\n\n**Behavior:**\n- Uses Go's reference date format (Mon Jan 2 15:04:05 MST 2006)\n- Respects Lang() setting for month/weekday names\n- Delegates to time.Format() with translations applied\n\n**Examples:**\n```go\n// English (default)\ndate.FormatLayout(\"Monday, 2. January 2006\")\n// → \"Monday, 9. February 2026\"\n\n// German\ndate.Lang(LangDE).FormatLayout(\"Monday, 2. January 2006\")\n// → \"Montag, 9. Februar 2026\"\n```\n\n**Implementation:**\n- Replace month/weekday names based on Lang setting\n- Handle both full and abbreviated names\n- Preserve all other layout characters\n\n## Acceptance Criteria\n- [ ] FormatLayout() implemented\n- [ ] Uses Go's standard layout format\n- [ ] Default (EN) outputs English month/weekday names\n- [ ] DE lang outputs German month/weekday names\n- [ ] Full names translated (Monday, January)\n- [ ] Abbreviated names translated (Mon, Jan)\n- [ ] Non-date characters preserved in layout\n- [ ] Unit tests for various layouts\n- [ ] Unit tests for both EN and DE\n- [ ] Benchmark meets \u003c10µs target with i18n\n- [ ] Godoc with Go layout format reference\n- [ ] Example tests showing custom layouts","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:59.18488929+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:59.18488929+01:00","dependencies":[{"issue_id":"quando-95w","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:12.24298936+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-95w","depends_on_id":"quando-zbr","type":"blocks","created_at":"2026-02-11T16:23:12.275497777+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"quando-95w","title":"Custom layout formatting","description":"Implement FormatLayout() for custom format layouts using Go's standard layout format.\n\n**API:**\n```go\nfunc (d Date) FormatLayout(layout string) string\n```\n\n**Behavior:**\n- Uses Go's reference date format (Mon Jan 2 15:04:05 MST 2006)\n- Respects Lang() setting for month/weekday names\n- Delegates to time.Format() with translations applied\n\n**Examples:**\n```go\n// English (default)\ndate.FormatLayout(\"Monday, 2. January 2006\")\n// → \"Monday, 9. February 2026\"\n\n// German\ndate.Lang(LangDE).FormatLayout(\"Monday, 2. January 2006\")\n// → \"Montag, 9. Februar 2026\"\n```\n\n**Implementation:**\n- Replace month/weekday names based on Lang setting\n- Handle both full and abbreviated names\n- Preserve all other layout characters\n\n## Acceptance Criteria\n- [ ] FormatLayout() implemented\n- [ ] Uses Go's standard layout format\n- [ ] Default (EN) outputs English month/weekday names\n- [ ] DE lang outputs German month/weekday names\n- [ ] Full names translated (Monday, January)\n- [ ] Abbreviated names translated (Mon, Jan)\n- [ ] Non-date characters preserved in layout\n- [ ] Unit tests for various layouts\n- [ ] Unit tests for both EN and DE\n- [ ] Benchmark meets \u003c10µs target with i18n\n- [ ] Godoc with Go layout format reference\n- [ ] Example tests showing custom layouts","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:59.18488929+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:59.18488929+01:00","dependencies":[{"issue_id":"quando-95w","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:12.24298936+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-95w","depends_on_id":"quando-zbr","type":"blocks","created_at":"2026-02-11T16:23:12.275497777+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"quando-9sf","title":"Snap operations: Next and Prev weekday","description":"Implement Next and Prev methods to jump to next/previous occurrence of a weekday.\n\n**API:**\n```go\nfunc (d Date) Next(weekday time.Weekday) Date\nfunc (d Date) Prev(weekday time.Weekday) Date\n```\n\n**Critical Behavior:**\n- **Next(Monday)**: Always NEXT Monday, never today (even if today is Monday)\n- **Prev(Friday)**: Always PREVIOUS Friday, never today (even if today is Friday)\n- Preserves time of day from source date\n\n**Examples:**\n- Monday calling Next(Monday) → next Monday (7 days later)\n- Monday calling Prev(Monday) → previous Monday (7 days earlier)\n- Tuesday calling Next(Monday) → next Monday (6 days later)\n\n## Acceptance Criteria\n- [ ] Next() implemented for all weekdays\n- [ ] Prev() implemented for all weekdays\n- [ ] Next() never returns today (always future)\n- [ ] Prev() never returns today (always past)\n- [ ] Time of day preserved from source\n- [ ] Edge case: Same weekday correctly skips to next/prev week\n- [ ] Unit tests for all weekday combinations\n- [ ] Tests for same weekday edge case\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc comments with same-weekday behavior example","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:58.320692116+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T17:33:58.411235677+01:00","closed_at":"2026-02-11T17:33:58.411235677+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-9sf","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:07.357069002+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":6,"issue_id":"quando-9sf","author":"Oliver Jakoubek","text":"Plan: 1) Add Next(weekday) and Prev(weekday) methods to snap.go, 2) Implement logic to ALWAYS skip to next/prev occurrence (never return today), 3) Preserve time of day from source date, 4) Handle same-weekday edge case (Monday.Next(Monday) = next Monday), 5) Write comprehensive unit tests for all weekday combinations, 6) Add tests specifically for same-weekday edge case, 7) Add benchmarks, 8) Ensure godoc comments with examples","created_at":"2026-02-11T16:31:43Z"}]}
|
{"id":"quando-9sf","title":"Snap operations: Next and Prev weekday","description":"Implement Next and Prev methods to jump to next/previous occurrence of a weekday.\n\n**API:**\n```go\nfunc (d Date) Next(weekday time.Weekday) Date\nfunc (d Date) Prev(weekday time.Weekday) Date\n```\n\n**Critical Behavior:**\n- **Next(Monday)**: Always NEXT Monday, never today (even if today is Monday)\n- **Prev(Friday)**: Always PREVIOUS Friday, never today (even if today is Friday)\n- Preserves time of day from source date\n\n**Examples:**\n- Monday calling Next(Monday) → next Monday (7 days later)\n- Monday calling Prev(Monday) → previous Monday (7 days earlier)\n- Tuesday calling Next(Monday) → next Monday (6 days later)\n\n## Acceptance Criteria\n- [ ] Next() implemented for all weekdays\n- [ ] Prev() implemented for all weekdays\n- [ ] Next() never returns today (always future)\n- [ ] Prev() never returns today (always past)\n- [ ] Time of day preserved from source\n- [ ] Edge case: Same weekday correctly skips to next/prev week\n- [ ] Unit tests for all weekday combinations\n- [ ] Tests for same weekday edge case\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc comments with same-weekday behavior example","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:58.320692116+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T17:33:58.411235677+01:00","closed_at":"2026-02-11T17:33:58.411235677+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-9sf","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:07.357069002+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":6,"issue_id":"quando-9sf","author":"Oliver Jakoubek","text":"Plan: 1) Add Next(weekday) and Prev(weekday) methods to snap.go, 2) Implement logic to ALWAYS skip to next/prev occurrence (never return today), 3) Preserve time of day from source date, 4) Handle same-weekday edge case (Monday.Next(Monday) = next Monday), 5) Write comprehensive unit tests for all weekday combinations, 6) Add tests specifically for same-weekday edge case, 7) Add benchmarks, 8) Ensure godoc comments with examples","created_at":"2026-02-11T16:31:43Z"}]}
|
||||||
{"id":"quando-b4r","title":"Arithmetic operations: Add and Sub","description":"Implement Add and Sub methods for all time units with special month-end overflow handling.\n\n**API:**\n```go\nfunc (d Date) Add(value int, unit Unit) Date\nfunc (d Date) Sub(value int, unit Unit) Date\n```\n\n**Critical Requirements:**\n- Support all 8 units (Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years)\n- **Month-end overflow**: When adding months, if target day doesn't exist, snap to month end\n - 2026-01-31 + 1 month = 2026-02-28 (February end)\n - 2026-01-24 + 1 month = 2026-02-24 (regular)\n - 2026-05-31 + 1 month = 2026-06-30 (June has 30 days)\n- DST handling: Add(1, Days) = same time next calendar day, NOT 24 hours\n- Support method chaining (fluent API)\n- Immutability: return new Date, never modify receiver\n\n## Acceptance Criteria\n- [ ] Add() implemented for all 8 units\n- [ ] Sub() implemented for all 8 units\n- [ ] Month-end overflow logic correct for all month combinations\n- [ ] Leap year handling (Feb 29 edge cases)\n- [ ] DST handling tested across DST transitions\n- [ ] Negative values supported (Add(-1) == Sub(1))\n- [ ] Method chaining works (Add().Sub().Add())\n- [ ] Unit tests with 95%+ coverage\n- [ ] Table-driven tests for month-end edge cases\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc comments with month-end examples","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:45.138685425+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:20:45.138685425+01:00","dependencies":[{"issue_id":"quando-b4r","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:06.383654729+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-b4r","depends_on_id":"quando-4bh","type":"blocks","created_at":"2026-02-11T16:23:06.424777459+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-b4r","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:06.471629282+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"quando-b4r","title":"Arithmetic operations: Add and Sub","description":"Implement Add and Sub methods for all time units with special month-end overflow handling.\n\n**API:**\n```go\nfunc (d Date) Add(value int, unit Unit) Date\nfunc (d Date) Sub(value int, unit Unit) Date\n```\n\n**Critical Requirements:**\n- Support all 8 units (Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years)\n- **Month-end overflow**: When adding months, if target day doesn't exist, snap to month end\n - 2026-01-31 + 1 month = 2026-02-28 (February end)\n - 2026-01-24 + 1 month = 2026-02-24 (regular)\n - 2026-05-31 + 1 month = 2026-06-30 (June has 30 days)\n- DST handling: Add(1, Days) = same time next calendar day, NOT 24 hours\n- Support method chaining (fluent API)\n- Immutability: return new Date, never modify receiver\n\n## Acceptance Criteria\n- [ ] Add() implemented for all 8 units\n- [ ] Sub() implemented for all 8 units\n- [ ] Month-end overflow logic correct for all month combinations\n- [ ] Leap year handling (Feb 29 edge cases)\n- [ ] DST handling tested across DST transitions\n- [ ] Negative values supported (Add(-1) == Sub(1))\n- [ ] Method chaining works (Add().Sub().Add())\n- [ ] Unit tests with 95%+ coverage\n- [ ] Table-driven tests for month-end edge cases\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc comments with month-end examples","status":"in_progress","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:45.138685425+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T17:40:21.296417975+01:00","dependencies":[{"issue_id":"quando-b4r","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:06.383654729+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-b4r","depends_on_id":"quando-4bh","type":"blocks","created_at":"2026-02-11T16:23:06.424777459+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-b4r","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:06.471629282+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":9,"issue_id":"quando-b4r","author":"Oliver Jakoubek","text":"Plan: 1) Create arithmetic.go with Add() and Sub() methods, 2) Implement unit handling for all 8 units (Seconds through Years), 3) Implement month-end overflow logic: when adding months, if target day doesn't exist, snap to last day of month, 4) Handle leap years correctly (Feb 29 edge cases), 5) Use time.Time methods for Seconds/Minutes/Hours/Days for DST-safe calendar day arithmetic, 6) Sub() as wrapper calling Add() with negative value, 7) Comprehensive table-driven tests for month-end overflow edge cases, 8) DST transition tests, 9) Negative value tests, 10) Method chaining tests, 11) Benchmarks, 12) Godoc with month-end examples","created_at":"2026-02-11T16:40:29Z"}]}
|
||||||
{"id":"quando-dsx","title":"Snap operations: StartOf and EndOf","description":"Implement StartOf and EndOf methods to jump to beginning/end of time units.\n\n**API:**\n```go\nfunc (d Date) StartOf(unit Unit) Date\nfunc (d Date) EndOf(unit Unit) Date\n```\n\n**Supported Units:** Week, Month, Quarter, Year\n\n**Behavior:**\n- **StartOf(Week)**: Monday 00:00:00 (ISO 8601 default)\n- **EndOf(Week)**: Sunday 23:59:59\n- **StartOf(Month)**: 1st day of month, 00:00:00\n- **EndOf(Month)**: Last day of month, 23:59:59\n- **StartOf(Quarter)**: Q1=Jan 1, Q2=Apr 1, Q3=Jul 1, Q4=Oct 1\n- **EndOf(Quarter)**: Q1=Mar 31, Q2=Jun 30, Q3=Sep 30, Q4=Dec 31\n- **StartOf(Year)**: Jan 1, 00:00:00\n- **EndOf(Year)**: Dec 31, 23:59:59\n\n**Quarter Definition:**\n- Q1 = January–March\n- Q2 = April–June\n- Q3 = July–September\n- Q4 = October–Dezember\n\n## Acceptance Criteria\n- [ ] StartOf(Week) returns Monday 00:00:00\n- [ ] EndOf(Week) returns Sunday 23:59:59\n- [ ] StartOf(Month) returns 1st day 00:00:00\n- [ ] EndOf(Month) handles all month lengths correctly\n- [ ] StartOf(Quarter) returns correct quarter start\n- [ ] EndOf(Quarter) returns correct quarter end (handles 30/31 day months)\n- [ ] StartOf(Year) returns Jan 1 00:00:00\n- [ ] EndOf(Year) returns Dec 31 23:59:59\n- [ ] Leap year handling for February\n- [ ] Unit tests for all units and edge cases\n- [ ] ISO 8601 week compliance tests\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc comments with examples","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:52.371452631+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:39:08.822973772+01:00","closed_at":"2026-02-11T16:39:08.822973772+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-dsx","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:07.280217562+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-dsx","depends_on_id":"quando-4bh","type":"blocks","created_at":"2026-02-11T16:23:07.316281123+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":5,"issue_id":"quando-dsx","author":"Oliver Jakoubek","text":"Plan: 1) Create snap.go with StartOf() and EndOf() methods, 2) Implement Week snapping (Monday start, Sunday end per ISO 8601), 3) Implement Month snapping (handle all month lengths), 4) Implement Quarter snapping (Q1=Jan-Mar, Q2=Apr-Jun, Q3=Jul-Sep, Q4=Oct-Dec), 5) Implement Year snapping, 6) Add comprehensive unit tests (all units, edge cases, leap years, month-end variations), 7) Add benchmarks to meet \u003c1µs target, 8) Add godoc comments and example tests","created_at":"2026-02-11T15:35:05Z"}]}
|
{"id":"quando-dsx","title":"Snap operations: StartOf and EndOf","description":"Implement StartOf and EndOf methods to jump to beginning/end of time units.\n\n**API:**\n```go\nfunc (d Date) StartOf(unit Unit) Date\nfunc (d Date) EndOf(unit Unit) Date\n```\n\n**Supported Units:** Week, Month, Quarter, Year\n\n**Behavior:**\n- **StartOf(Week)**: Monday 00:00:00 (ISO 8601 default)\n- **EndOf(Week)**: Sunday 23:59:59\n- **StartOf(Month)**: 1st day of month, 00:00:00\n- **EndOf(Month)**: Last day of month, 23:59:59\n- **StartOf(Quarter)**: Q1=Jan 1, Q2=Apr 1, Q3=Jul 1, Q4=Oct 1\n- **EndOf(Quarter)**: Q1=Mar 31, Q2=Jun 30, Q3=Sep 30, Q4=Dec 31\n- **StartOf(Year)**: Jan 1, 00:00:00\n- **EndOf(Year)**: Dec 31, 23:59:59\n\n**Quarter Definition:**\n- Q1 = January–March\n- Q2 = April–June\n- Q3 = July–September\n- Q4 = October–Dezember\n\n## Acceptance Criteria\n- [ ] StartOf(Week) returns Monday 00:00:00\n- [ ] EndOf(Week) returns Sunday 23:59:59\n- [ ] StartOf(Month) returns 1st day 00:00:00\n- [ ] EndOf(Month) handles all month lengths correctly\n- [ ] StartOf(Quarter) returns correct quarter start\n- [ ] EndOf(Quarter) returns correct quarter end (handles 30/31 day months)\n- [ ] StartOf(Year) returns Jan 1 00:00:00\n- [ ] EndOf(Year) returns Dec 31 23:59:59\n- [ ] Leap year handling for February\n- [ ] Unit tests for all units and edge cases\n- [ ] ISO 8601 week compliance tests\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc comments with examples","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:52.371452631+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:39:08.822973772+01:00","closed_at":"2026-02-11T16:39:08.822973772+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-dsx","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:07.280217562+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-dsx","depends_on_id":"quando-4bh","type":"blocks","created_at":"2026-02-11T16:23:07.316281123+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":5,"issue_id":"quando-dsx","author":"Oliver Jakoubek","text":"Plan: 1) Create snap.go with StartOf() and EndOf() methods, 2) Implement Week snapping (Monday start, Sunday end per ISO 8601), 3) Implement Month snapping (handle all month lengths), 4) Implement Quarter snapping (Q1=Jan-Mar, Q2=Apr-Jun, Q3=Jul-Sep, Q4=Oct-Dec), 5) Implement Year snapping, 6) Add comprehensive unit tests (all units, edge cases, leap years, month-end variations), 7) Add benchmarks to meet \u003c1µs target, 8) Add godoc comments and example tests","created_at":"2026-02-11T15:35:05Z"}]}
|
||||||
{"id":"quando-gr5","title":"Automatic date parsing","description":"Implement automatic parsing that detects common date formats without explicit layout.\n\n**API:**\n```go\nfunc Parse(s string) (Date, error)\n```\n\n**Supported Formats:**\n- ISO: \"2026-02-09\"\n- ISO with slash: \"2026/02/09\"\n- EU (dot separator): \"09.02.2026\"\n- RFC2822: \"Mon, 09 Feb 2026 00:00:00 +0000\"\n\n**Ambiguity Rules:**\nSlash formats without year prefix are AMBIGUOUS and must error:\n\n| Input | Recognition | Reason |\n|-------|-------------|--------|\n| 2026-02-01 | ✅ ISO | Standard format |\n| 01.02.2026 | ✅ EU | Dot = EU convention |\n| 2026/02/09 | ✅ ISO | Year prefix unambiguous |\n| 01/02/2026 | ❌ ERROR | Ambiguous (US vs EU) |\n\n**Error Handling:**\n- Return clear error for ambiguous formats\n- Return clear error for invalid dates\n- Never panic\n\n## Acceptance Criteria\n- [ ] Parse() implemented\n- [ ] ISO format recognized (YYYY-MM-DD)\n- [ ] ISO slash format recognized (YYYY/MM/DD)\n- [ ] EU format recognized (DD.MM.YYYY)\n- [ ] RFC2822 format recognized\n- [ ] Ambiguous slash formats return error\n- [ ] Invalid dates return error\n- [ ] Never panics on any input\n- [ ] Unit tests for all supported formats\n- [ ] Unit tests for ambiguous/invalid inputs\n- [ ] Benchmark meets \u003c10µs target\n- [ ] Godoc with ambiguity rules documented\n- [ ] Example tests showing supported formats","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:28.074836359+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:28.074836359+01:00","dependencies":[{"issue_id":"quando-gr5","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:11.106618119+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-gr5","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:11.142801721+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"quando-gr5","title":"Automatic date parsing","description":"Implement automatic parsing that detects common date formats without explicit layout.\n\n**API:**\n```go\nfunc Parse(s string) (Date, error)\n```\n\n**Supported Formats:**\n- ISO: \"2026-02-09\"\n- ISO with slash: \"2026/02/09\"\n- EU (dot separator): \"09.02.2026\"\n- RFC2822: \"Mon, 09 Feb 2026 00:00:00 +0000\"\n\n**Ambiguity Rules:**\nSlash formats without year prefix are AMBIGUOUS and must error:\n\n| Input | Recognition | Reason |\n|-------|-------------|--------|\n| 2026-02-01 | ✅ ISO | Standard format |\n| 01.02.2026 | ✅ EU | Dot = EU convention |\n| 2026/02/09 | ✅ ISO | Year prefix unambiguous |\n| 01/02/2026 | ❌ ERROR | Ambiguous (US vs EU) |\n\n**Error Handling:**\n- Return clear error for ambiguous formats\n- Return clear error for invalid dates\n- Never panic\n\n## Acceptance Criteria\n- [ ] Parse() implemented\n- [ ] ISO format recognized (YYYY-MM-DD)\n- [ ] ISO slash format recognized (YYYY/MM/DD)\n- [ ] EU format recognized (DD.MM.YYYY)\n- [ ] RFC2822 format recognized\n- [ ] Ambiguous slash formats return error\n- [ ] Invalid dates return error\n- [ ] Never panics on any input\n- [ ] Unit tests for all supported formats\n- [ ] Unit tests for ambiguous/invalid inputs\n- [ ] Benchmark meets \u003c10µs target\n- [ ] Godoc with ambiguity rules documented\n- [ ] Example tests showing supported formats","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:28.074836359+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:28.074836359+01:00","dependencies":[{"issue_id":"quando-gr5","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:11.106618119+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-gr5","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:11.142801721+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"quando-j2s","title":"Core infrastructure: Date type and conversions","description":"Implement the core Date type that wraps time.Time and provides the foundation for the fluent API.\n\n**Technical Details:**\n- `Date` struct with private `time.Time` field and optional `Lang` field\n- Package-level constructors: `Now()`, `From(time.Time)`\n- Conversion methods: `Time()` returns underlying time.Time\n- Unix timestamp support: `Unix()` and `FromUnix(int64)`\n\n**Implementation Notes:**\n- Date must wrap time.Time, not reimplement it\n- All operations return new Date instances (immutability)\n- Support full Go time.Time range (year 0-9999+)\n- Support negative Unix timestamps (before 1970)\n\n## Acceptance Criteria\n- [ ] Date struct defined with time.Time and Lang fields\n- [ ] Now() returns current date\n- [ ] From(time.Time) converts to Date\n- [ ] Time() extracts underlying time.Time\n- [ ] Unix() returns Unix timestamp (int64)\n- [ ] FromUnix(int64) creates Date from timestamp\n- [ ] Unit tests with 95%+ coverage\n- [ ] Godoc comments for all exported types/functions\n- [ ] Example tests in example_test.go","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:29.134906992+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:31:25.561098309+01:00","closed_at":"2026-02-11T16:31:25.561098309+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-j2s","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.272420642+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":2,"issue_id":"quando-j2s","author":"Oliver Jakoubek","text":"Plan: 1) Create date.go with Date struct (t time.Time, lang Lang), 2) Implement package-level constructors Now() and From(time.Time), 3) Add conversion methods Time(), Unix(), FromUnix(), 4) Create quando.go for package-level exports, 5) Write comprehensive unit tests covering edge cases (negative Unix timestamps, full time.Time range), 6) Add example tests in example_test.go, 7) Ensure all exports have godoc comments","created_at":"2026-02-11T15:28:39Z"}]}
|
{"id":"quando-j2s","title":"Core infrastructure: Date type and conversions","description":"Implement the core Date type that wraps time.Time and provides the foundation for the fluent API.\n\n**Technical Details:**\n- `Date` struct with private `time.Time` field and optional `Lang` field\n- Package-level constructors: `Now()`, `From(time.Time)`\n- Conversion methods: `Time()` returns underlying time.Time\n- Unix timestamp support: `Unix()` and `FromUnix(int64)`\n\n**Implementation Notes:**\n- Date must wrap time.Time, not reimplement it\n- All operations return new Date instances (immutability)\n- Support full Go time.Time range (year 0-9999+)\n- Support negative Unix timestamps (before 1970)\n\n## Acceptance Criteria\n- [ ] Date struct defined with time.Time and Lang fields\n- [ ] Now() returns current date\n- [ ] From(time.Time) converts to Date\n- [ ] Time() extracts underlying time.Time\n- [ ] Unix() returns Unix timestamp (int64)\n- [ ] FromUnix(int64) creates Date from timestamp\n- [ ] Unit tests with 95%+ coverage\n- [ ] Godoc comments for all exported types/functions\n- [ ] Example tests in example_test.go","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:29.134906992+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:31:25.561098309+01:00","closed_at":"2026-02-11T16:31:25.561098309+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-j2s","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.272420642+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":2,"issue_id":"quando-j2s","author":"Oliver Jakoubek","text":"Plan: 1) Create date.go with Date struct (t time.Time, lang Lang), 2) Implement package-level constructors Now() and From(time.Time), 3) Add conversion methods Time(), Unix(), FromUnix(), 4) Create quando.go for package-level exports, 5) Write comprehensive unit tests covering edge cases (negative Unix timestamps, full time.Time range), 6) Add example tests in example_test.go, 7) Ensure all exports have godoc comments","created_at":"2026-02-11T15:28:39Z"}]}
|
||||||
|
|
|
||||||
120
arithmetic.go
Normal file
120
arithmetic.go
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
package quando
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Add adds the specified number of units to the date and returns a new Date.
|
||||||
|
// The original date is not modified (immutability).
|
||||||
|
//
|
||||||
|
// Supported units: Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years
|
||||||
|
//
|
||||||
|
// Month-End Overflow Behavior:
|
||||||
|
//
|
||||||
|
// When adding months (or quarters/years which add months internally), if the target
|
||||||
|
// day doesn't exist in the destination month, the date is snapped to the last day
|
||||||
|
// of that month instead of overflowing into the next month.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - 2026-01-31 + 1 month = 2026-02-28 (February has only 28 days in 2026)
|
||||||
|
// - 2026-01-24 + 1 month = 2026-02-24 (regular addition, day exists)
|
||||||
|
// - 2026-05-31 + 1 month = 2026-06-30 (June has only 30 days)
|
||||||
|
// - 2024-01-31 + 1 month = 2024-02-29 (leap year, February has 29 days)
|
||||||
|
//
|
||||||
|
// DST Handling:
|
||||||
|
//
|
||||||
|
// Adding Days means "same time on the next calendar day", not "24 hours later".
|
||||||
|
// This ensures that operations work intuitively across DST transitions.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// - 2026-03-31 02:00 CET + 1 Day = 2026-04-01 02:00 CEST (not 03:00)
|
||||||
|
//
|
||||||
|
// Negative Values:
|
||||||
|
//
|
||||||
|
// Negative values are supported and equivalent to subtraction:
|
||||||
|
// - Add(-1, Months) is the same as Sub(1, Months)
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// date := quando.From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC))
|
||||||
|
// result := date.Add(1, quando.Months) // 2026-02-28 12:00:00
|
||||||
|
func (d Date) Add(value int, unit Unit) Date {
|
||||||
|
t := d.t
|
||||||
|
|
||||||
|
switch unit {
|
||||||
|
case Seconds:
|
||||||
|
t = t.Add(time.Duration(value) * time.Second)
|
||||||
|
|
||||||
|
case Minutes:
|
||||||
|
t = t.Add(time.Duration(value) * time.Minute)
|
||||||
|
|
||||||
|
case Hours:
|
||||||
|
t = t.Add(time.Duration(value) * time.Hour)
|
||||||
|
|
||||||
|
case Days:
|
||||||
|
// Use AddDate for calendar days (DST-safe)
|
||||||
|
t = t.AddDate(0, 0, value)
|
||||||
|
|
||||||
|
case Weeks:
|
||||||
|
// 1 week = 7 days
|
||||||
|
t = t.AddDate(0, 0, value*7)
|
||||||
|
|
||||||
|
case Months:
|
||||||
|
// Add months with month-end overflow handling
|
||||||
|
t = addMonthsWithOverflow(t, value)
|
||||||
|
|
||||||
|
case Quarters:
|
||||||
|
// 1 quarter = 3 months
|
||||||
|
t = addMonthsWithOverflow(t, value*3)
|
||||||
|
|
||||||
|
case Years:
|
||||||
|
// 1 year = 12 months
|
||||||
|
t = addMonthsWithOverflow(t, value*12)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Date{t: t, lang: d.lang}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub subtracts the specified number of units from the date and returns a new Date.
|
||||||
|
// This is equivalent to Add with a negative value.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// date := quando.From(time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC))
|
||||||
|
// result := date.Sub(1, quando.Months) // 2026-02-28 12:00:00 (month-end snap)
|
||||||
|
func (d Date) Sub(value int, unit Unit) Date {
|
||||||
|
return d.Add(-value, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMonthsWithOverflow adds months to a time.Time with month-end overflow handling.
|
||||||
|
// If the target day doesn't exist in the destination month, it snaps to the last day.
|
||||||
|
func addMonthsWithOverflow(t time.Time, months int) time.Time {
|
||||||
|
// Calculate target year and month
|
||||||
|
year := t.Year()
|
||||||
|
month := int(t.Month()) + months
|
||||||
|
day := t.Day()
|
||||||
|
|
||||||
|
// Handle year overflow/underflow
|
||||||
|
for month > 12 {
|
||||||
|
year++
|
||||||
|
month -= 12
|
||||||
|
}
|
||||||
|
for month < 1 {
|
||||||
|
year--
|
||||||
|
month += 12
|
||||||
|
}
|
||||||
|
|
||||||
|
targetMonth := time.Month(month)
|
||||||
|
|
||||||
|
// Get the last day of the target month
|
||||||
|
// Strategy: First day of next month minus 1 day
|
||||||
|
firstOfNextMonth := time.Date(year, targetMonth+1, 1, 0, 0, 0, 0, t.Location())
|
||||||
|
lastDayOfTargetMonth := firstOfNextMonth.AddDate(0, 0, -1).Day()
|
||||||
|
|
||||||
|
// If target day exceeds last day of month, snap to last day
|
||||||
|
if day > lastDayOfTargetMonth {
|
||||||
|
day = lastDayOfTargetMonth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the result date with the same time of day
|
||||||
|
return time.Date(year, targetMonth, day,
|
||||||
|
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
|
||||||
|
}
|
||||||
427
arithmetic_test.go
Normal file
427
arithmetic_test.go
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
package quando
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddSeconds(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"add 1 second", 1, "2026-01-01 12:00:01"},
|
||||||
|
{"add 60 seconds", 60, "2026-01-01 12:01:00"},
|
||||||
|
{"add 3600 seconds", 3600, "2026-01-01 13:00:00"},
|
||||||
|
{"subtract 1 second", -1, "2026-01-01 11:59:59"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := date.Add(tt.value, Seconds)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Seconds) = %v, want %v", tt.value, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddMinutes(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"add 1 minute", 1, "2026-01-01 12:01:00"},
|
||||||
|
{"add 60 minutes", 60, "2026-01-01 13:00:00"},
|
||||||
|
{"subtract 1 minute", -1, "2026-01-01 11:59:00"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := date.Add(tt.value, Minutes)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Minutes) = %v, want %v", tt.value, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddHours(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"add 1 hour", 1, "2026-01-01 13:00:00"},
|
||||||
|
{"add 24 hours", 24, "2026-01-02 12:00:00"},
|
||||||
|
{"subtract 1 hour", -1, "2026-01-01 11:00:00"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := date.Add(tt.value, Hours)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Hours) = %v, want %v", tt.value, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddDays(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"add 1 day", 1, "2026-01-02 12:00:00"},
|
||||||
|
{"add 7 days", 7, "2026-01-08 12:00:00"},
|
||||||
|
{"add 365 days", 365, "2027-01-01 12:00:00"},
|
||||||
|
{"subtract 1 day", -1, "2025-12-31 12:00:00"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := date.Add(tt.value, Days)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Days) = %v, want %v", tt.value, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddWeeks(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"add 1 week", 1, "2026-01-08 12:00:00"},
|
||||||
|
{"add 4 weeks", 4, "2026-01-29 12:00:00"},
|
||||||
|
{"subtract 1 week", -1, "2025-12-25 12:00:00"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := date.Add(tt.value, Weeks)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Weeks) = %v, want %v", tt.value, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddMonths(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
date Date
|
||||||
|
months int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Regular month addition (no overflow)
|
||||||
|
{
|
||||||
|
name: "regular addition",
|
||||||
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2026-02-15 12:00:00",
|
||||||
|
},
|
||||||
|
// Month-end overflow cases
|
||||||
|
{
|
||||||
|
name: "Jan 31 + 1 month = Feb 28 (non-leap year)",
|
||||||
|
date: From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2026-02-28 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jan 31 + 1 month = Feb 29 (leap year)",
|
||||||
|
date: From(time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2024-02-29 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "May 31 + 1 month = Jun 30",
|
||||||
|
date: From(time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2026-06-30 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jul 31 + 1 month = Aug 31",
|
||||||
|
date: From(time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2026-08-31 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Aug 31 + 1 month = Sep 30",
|
||||||
|
date: From(time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2026-09-30 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Oct 31 + 1 month = Nov 30",
|
||||||
|
date: From(time.Date(2026, 10, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2026-11-30 12:00:00"},
|
||||||
|
// Cross year boundary
|
||||||
|
{
|
||||||
|
name: "Dec 15 + 1 month = Jan 15 next year",
|
||||||
|
date: From(time.Date(2026, 12, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2027-01-15 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dec 31 + 1 month = Jan 31 next year",
|
||||||
|
date: From(time.Date(2026, 12, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 1,
|
||||||
|
expected: "2027-01-31 12:00:00",
|
||||||
|
},
|
||||||
|
// Multiple months
|
||||||
|
{
|
||||||
|
name: "Jan 31 + 2 months = Mar 31",
|
||||||
|
date: From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 2,
|
||||||
|
expected: "2026-03-31 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jan 31 + 13 months = Feb 28 next year",
|
||||||
|
date: From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: 13,
|
||||||
|
expected: "2027-02-28 12:00:00",
|
||||||
|
},
|
||||||
|
// Negative values (subtraction)
|
||||||
|
{
|
||||||
|
name: "Mar 31 - 1 month = Feb 28",
|
||||||
|
date: From(time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: -1,
|
||||||
|
expected: "2026-02-28 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jan 15 - 1 month = Dec 15 prev year",
|
||||||
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
months: -1,
|
||||||
|
expected: "2025-12-15 12:00:00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.date.Add(tt.months, Months)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Months) = %v, want %v", tt.months, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddQuarters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
date Date
|
||||||
|
quarters int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add 1 quarter",
|
||||||
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
quarters: 1,
|
||||||
|
expected: "2026-04-15 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add 4 quarters (1 year)",
|
||||||
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
quarters: 4,
|
||||||
|
expected: "2027-01-15 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quarter with month-end overflow",
|
||||||
|
date: From(time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)),
|
||||||
|
quarters: 1,
|
||||||
|
expected: "2026-08-31 12:00:00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.date.Add(tt.quarters, Quarters)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Quarters) = %v, want %v", tt.quarters, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddYears(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
date Date
|
||||||
|
years int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add 1 year",
|
||||||
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
years: 1,
|
||||||
|
expected: "2027-01-15 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add 10 years",
|
||||||
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
years: 10,
|
||||||
|
expected: "2036-01-15 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leap year to non-leap year (Feb 29 -> Feb 28)",
|
||||||
|
date: From(time.Date(2024, 2, 29, 12, 0, 0, 0, time.UTC)),
|
||||||
|
years: 1,
|
||||||
|
expected: "2025-02-28 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subtract 1 year",
|
||||||
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||||||
|
years: -1,
|
||||||
|
expected: "2025-01-15 12:00:00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.date.Add(tt.years, Years)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Add(%d, Years) = %v, want %v", tt.years, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSub(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value int
|
||||||
|
unit Unit
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"subtract 1 day", 1, Days, "2026-03-30 12:00:00"},
|
||||||
|
{"subtract 1 month", 1, Months, "2026-02-28 12:00:00"},
|
||||||
|
{"subtract 1 year", 1, Years, "2025-03-31 12:00:00"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := date.Sub(tt.value, tt.unit)
|
||||||
|
if result.String() != tt.expected {
|
||||||
|
t.Errorf("Sub(%d, %v) = %v, want %v", tt.value, tt.unit, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddSubEquivalence(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
// Sub(n) should equal Add(-n)
|
||||||
|
units := []Unit{Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years}
|
||||||
|
|
||||||
|
for _, unit := range units {
|
||||||
|
result1 := date.Sub(5, unit)
|
||||||
|
result2 := date.Add(-5, unit)
|
||||||
|
|
||||||
|
if !result1.Time().Equal(result2.Time()) {
|
||||||
|
t.Errorf("Sub(5, %v) != Add(-5, %v): %v != %v", unit, unit, result1, result2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMethodChaining(t *testing.T) {
|
||||||
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
// Chain multiple operations
|
||||||
|
result := date.
|
||||||
|
Add(1, Months).
|
||||||
|
Add(15, Days).
|
||||||
|
Sub(2, Hours)
|
||||||
|
|
||||||
|
expected := "2026-02-16 10:00:00"
|
||||||
|
if result.String() != expected {
|
||||||
|
t.Errorf("Chained operations = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImmutability(t *testing.T) {
|
||||||
|
original := From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC))
|
||||||
|
originalTime := original.Time()
|
||||||
|
|
||||||
|
// Perform various operations
|
||||||
|
_ = original.Add(1, Days)
|
||||||
|
_ = original.Add(1, Months)
|
||||||
|
_ = original.Sub(1, Years)
|
||||||
|
|
||||||
|
// Verify original is unchanged
|
||||||
|
if !original.Time().Equal(originalTime) {
|
||||||
|
t.Error("Add/Sub operations modified the original date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTimezonePreservation verifies that arithmetic preserves timezone
|
||||||
|
func TestTimezonePreservation(t *testing.T) {
|
||||||
|
loc, err := time.LoadLocation("Europe/Berlin")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping timezone test: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
berlinTime := time.Date(2026, 1, 15, 12, 0, 0, 0, loc)
|
||||||
|
date := From(berlinTime)
|
||||||
|
|
||||||
|
result := date.Add(1, Months)
|
||||||
|
|
||||||
|
if result.Time().Location() != loc {
|
||||||
|
t.Errorf("Add() location = %v, want %v", result.Time().Location(), loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkAddDays benchmarks Add with Days
|
||||||
|
func BenchmarkAddDays(b *testing.B) {
|
||||||
|
date := Now()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = date.Add(1, Days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkAddMonths benchmarks Add with Months
|
||||||
|
func BenchmarkAddMonths(b *testing.B) {
|
||||||
|
date := Now()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = date.Add(1, Months)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkAddYears benchmarks Add with Years
|
||||||
|
func BenchmarkAddYears(b *testing.B) {
|
||||||
|
date := Now()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = date.Add(1, Years)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkMethodChaining benchmarks chained operations
|
||||||
|
func BenchmarkMethodChaining(b *testing.B) {
|
||||||
|
date := Now()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = date.Add(1, Months).Add(15, Days).Sub(2, Hours)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -312,3 +312,56 @@ func Example_errorTypes() {
|
||||||
// invalid timezone
|
// invalid timezone
|
||||||
// date overflow
|
// date overflow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExampleDate_Add demonstrates date arithmetic
|
||||||
|
func ExampleDate_Add() {
|
||||||
|
date := quando.From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
fmt.Println("Original:", date)
|
||||||
|
fmt.Println("+1 day:", date.Add(1, quando.Days))
|
||||||
|
fmt.Println("+1 month:", date.Add(1, quando.Months))
|
||||||
|
fmt.Println("+1 year:", date.Add(1, quando.Years))
|
||||||
|
// Output:
|
||||||
|
// Original: 2026-01-15 12:00:00
|
||||||
|
// +1 day: 2026-01-16 12:00:00
|
||||||
|
// +1 month: 2026-02-15 12:00:00
|
||||||
|
// +1 year: 2027-01-15 12:00:00
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleDate_Add_monthEndOverflow demonstrates month-end overflow behavior
|
||||||
|
func ExampleDate_Add_monthEndOverflow() {
|
||||||
|
// When adding months, if target day doesn't exist, snap to month end
|
||||||
|
date := quando.From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
fmt.Println("Jan 31 + 1 month:", date.Add(1, quando.Months))
|
||||||
|
fmt.Println("Jan 31 + 2 months:", date.Add(2, quando.Months))
|
||||||
|
// Output:
|
||||||
|
// Jan 31 + 1 month: 2026-02-28 12:00:00
|
||||||
|
// Jan 31 + 2 months: 2026-03-31 12:00:00
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleDate_Sub demonstrates date subtraction
|
||||||
|
func ExampleDate_Sub() {
|
||||||
|
date := quando.From(time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
fmt.Println("Original:", date)
|
||||||
|
fmt.Println("-1 day:", date.Sub(1, quando.Days))
|
||||||
|
fmt.Println("-1 month:", date.Sub(1, quando.Months))
|
||||||
|
// Output:
|
||||||
|
// Original: 2026-03-31 12:00:00
|
||||||
|
// -1 day: 2026-03-30 12:00:00
|
||||||
|
// -1 month: 2026-02-28 12:00:00
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleDate_Add_chaining demonstrates method chaining
|
||||||
|
func ExampleDate_Add_chaining() {
|
||||||
|
date := quando.From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
result := date.
|
||||||
|
Add(1, quando.Months).
|
||||||
|
Add(15, quando.Days).
|
||||||
|
Sub(2, quando.Hours)
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
// Output: 2026-02-16 10:00:00
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue