diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 586be8b..b8e9759 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -7,13 +7,13 @@ {"id":"quando-6c3","title":"CI/CD pipeline setup","description":"Set up GitHub Actions CI/CD pipeline for automated testing and quality checks.\n\n**CI Workflow:**\n- Run on push and pull requests\n- Test on multiple Go versions (1.22, 1.23, latest)\n- Test on multiple platforms (Linux, macOS, Windows)\n- Run go fmt check\n- Run go vet\n- Run tests with coverage\n- Run benchmarks (informational)\n- Optional: golangci-lint\n\n**Coverage Reporting:**\n- Generate coverage report\n- Fail if coverage \u003c95% for core calculation functions\n- Optional: Upload to codecov.io or similar\n\n**Performance Monitoring:**\n- Run benchmarks on each commit\n- Report benchmark results (informational, no fail)\n\n## Acceptance Criteria\n- [ ] .github/workflows/ci.yml created\n- [ ] Tests run on push and PR\n- [ ] Multi-version Go support (1.22+)\n- [ ] Multi-platform support (Linux, macOS, Windows)\n- [ ] go fmt check passes\n- [ ] go vet check passes\n- [ ] Tests run with coverage report\n- [ ] Coverage requirement enforced (95%+)\n- [ ] Benchmarks run (informational)\n- [ ] CI badge in README (if applicable)","status":"open","priority":3,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:51.928117055+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:51.928117055+01:00","dependencies":[{"issue_id":"quando-6c3","depends_on_id":"quando-r1o","type":"blocks","created_at":"2026-02-11T16:23:16.259739409+01:00","created_by":"Oliver Jakoubek"}]} {"id":"quando-6x3","title":"Documentation and examples","description":"Complete documentation including README, godoc, and example code.\n\n**README.md Contents:**\n- Project overview and vision\n- Installation instructions (go get)\n- Quick start examples\n- Core features showcase\n- Performance characteristics\n- Comparison to time.Time\n- Contributing guidelines\n- License\n\n**Godoc Requirements:**\n- Every exported type/function documented\n- Godoc comments start with type/function name\n- Complex behaviors explained (month-end overflow, DST, etc.)\n- Edge cases noted where applicable\n\n**Example Tests:**\n- Example_basicArithmetic\n- Example_monthEndOverflow\n- Example_snapOperations\n- Example_diffCalculation\n- Example_humanFormat\n- Example_parsing\n- Example_formatting\n- Example_timezoneConversion\n- Example_chainedOperations\n\n**Code Examples in README:**\n- Complex chaining\n- Month-end handling\n- Human-readable diffs\n- Timezone-aware operations\n- Comparison with stdlib\n\n## Acceptance Criteria\n- [ ] README.md complete with all sections\n- [ ] Installation instructions clear\n- [ ] At least 5 code examples in README\n- [ ] All exported items have godoc comments\n- [ ] Godoc comments follow conventions (name first)\n- [ ] At least 8 example tests\n- [ ] Examples demonstrate key features\n- [ ] Edge cases documented in relevant godoc\n- [ ] Comparison table with time.Time\n- [ ] Contributing guidelines (if open source)","status":"open","priority":3,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:45.693695262+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:45.693695262+01:00","dependencies":[{"issue_id":"quando-6x3","depends_on_id":"quando-r1o","type":"blocks","created_at":"2026-02-11T16:23:16.222906911+01:00","created_by":"Oliver Jakoubek"}]} {"id":"quando-7m5","title":"MustParse convenience function","description":"Implement MustParse() convenience function that panics on error (for tests/initialization).\n\n**API:**\n```go\nfunc MustParse(s string) Date\n```\n\n**Behavior:**\n- Calls Parse() internally\n- Returns Date on success\n- Panics on error (with clear panic message)\n\n**Use Cases:**\n- Test fixtures\n- Static initialization\n- Configuration files where values are known-good\n\n**Documentation:**\n- MUST clearly document that this function panics\n- MUST recommend using Parse() in production code\n- MUST show test usage examples\n\n## Acceptance Criteria\n- [ ] MustParse() implemented\n- [ ] Returns Date on successful parse\n- [ ] Panics with clear message on error\n- [ ] Godoc clearly warns about panic behavior\n- [ ] Godoc recommends Parse() for production\n- [ ] Example test showing test fixture usage\n- [ ] Unit tests verifying panic on invalid input","status":"open","priority":3,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:46.007442996+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:46.007442996+01:00","dependencies":[{"issue_id":"quando-7m5","depends_on_id":"quando-gr5","type":"blocks","created_at":"2026-02-11T16:23:11.340600075+01:00","created_by":"Oliver Jakoubek"}]} -{"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":"in_progress","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:26:27.927191774+01:00","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-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":"open","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-11T16:20:58.320692116+01:00","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"}]} {"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-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":"open","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:20:52.371452631+01:00","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"}]} {"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":"open","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:20:29.134906992+01:00","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"}]} +{"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":"in_progress","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:28:32.677952407+01:00","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-ljj","title":"Duration type and Diff calculation","description":"Implement Duration type and Diff function for calculating differences between dates.\n\n**API:**\n```go\nfunc Diff(a, b time.Time) Duration\n\ntype Duration struct {\n // private fields\n}\n\n// Integer methods (rounded down)\nfunc (dur Duration) Seconds() int64\nfunc (dur Duration) Minutes() int64\nfunc (dur Duration) Hours() int64\nfunc (dur Duration) Days() int\nfunc (dur Duration) Weeks() int\nfunc (dur Duration) Months() int\nfunc (dur Duration) Years() int\n\n// Float methods (precise)\nfunc (dur Duration) MonthsFloat() float64\nfunc (dur Duration) YearsFloat() float64\n```\n\n**Precision:**\n- Integer variants return rounded-down values\n- Float variants for precise calculations\n- Handle negative differences (date1 \u003c date2)\n- Cross year boundaries correctly\n- Handle leap years correctly\n\n## Acceptance Criteria\n- [ ] Duration type defined\n- [ ] Diff(a, b) returns Duration\n- [ ] All integer methods implemented (Seconds through Years)\n- [ ] Float methods for Months and Years implemented\n- [ ] Negative differences handled correctly\n- [ ] Calculations correct across year boundaries\n- [ ] Leap year handling correct\n- [ ] Unit tests with 95%+ coverage\n- [ ] Table-driven tests for various date ranges\n- [ ] Benchmarks meet \u003c1µs (int) and \u003c2µs (float) targets\n- [ ] Godoc comments with precision explanation","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:06.159742785+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:06.159742785+01:00","dependencies":[{"issue_id":"quando-ljj","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:08.127266864+01:00","created_by":"Oliver Jakoubek"}]} {"id":"quando-r1o","title":"Comprehensive test suite and benchmarks","description":"Ensure comprehensive test coverage (95%+) and performance benchmarks for all features.\n\n**Test Requirements:**\n- Minimum 95% coverage for all calculation functions\n- Table-driven tests for edge cases\n- Example tests for godoc\n- Separate benchmark file\n\n**Critical Test Scenarios:**\n1. Month arithmetic: Overflow, leap years, negative\n2. Snap operations: All units, edge cases\n3. Next/Prev: Same weekday edge case\n4. Diff calculations: Year boundaries, leap years, negative\n5. DST handling: Add across DST transitions\n6. Parsing: All formats, ambiguous, invalid\n7. WeekNumber: ISO 8601 compliance\n8. Formatting: All formats, i18n\n\n**Benchmarks:**\nTarget performance:\n- Add/Sub: \u003c1µs\n- Diff: \u003c1µs (int), \u003c2µs (float)\n- Format: \u003c5µs (no i18n), \u003c10µs (with i18n)\n- Parse: \u003c10µs (auto), \u003c20µs (relative)\n\n## Acceptance Criteria\n- [ ] Test coverage ≥95% for all calculation functions\n- [ ] Table-driven tests for all edge cases\n- [ ] Example tests in example_test.go\n- [ ] Benchmark file with all critical operations\n- [ ] All benchmarks meet performance targets\n- [ ] CI/CD runs tests and reports coverage\n- [ ] Tests use FixedClock for determinism\n- [ ] Edge cases documented in test names/comments","status":"open","priority":3,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:38.100200304+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:38.100200304+01:00","dependencies":[{"issue_id":"quando-r1o","depends_on_id":"quando-b4r","type":"blocks","created_at":"2026-02-11T16:23:15.418358186+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-r1o","depends_on_id":"quando-dsx","type":"blocks","created_at":"2026-02-11T16:23:15.474799177+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-r1o","depends_on_id":"quando-9sf","type":"blocks","created_at":"2026-02-11T16:23:15.513811922+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-r1o","depends_on_id":"quando-10t","type":"blocks","created_at":"2026-02-11T16:23:15.545561332+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-r1o","depends_on_id":"quando-gr5","type":"blocks","created_at":"2026-02-11T16:23:15.578273993+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-r1o","depends_on_id":"quando-5ol","type":"blocks","created_at":"2026-02-11T16:23:15.609031891+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-r1o","depends_on_id":"quando-5ib","type":"blocks","created_at":"2026-02-11T16:23:15.640381484+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-r1o","depends_on_id":"quando-41g","type":"blocks","created_at":"2026-02-11T16:23:15.672161997+01:00","created_by":"Oliver Jakoubek"}]} {"id":"quando-tn3","title":"Relative date parsing","description":"Implement ParseRelative for parsing relative date expressions.\n\n**API:**\n```go\nfunc ParseRelative(s string) (Date, error)\n```\n\n**Supported Expressions (Phase 1):**\n- \"today\" → Today 00:00:00\n- \"tomorrow\" → Tomorrow 00:00:00\n- \"yesterday\" → Yesterday 00:00:00\n- \"+2 days\" → Today + 2 days\n- \"-1 week\" → Today - 1 week\n- \"+3 months\" → Today + 3 months\n\n**Format:**\n- Relative offsets: [+|-]\u003cnumber\u003e \u003cunit\u003e\n- Units: day(s), week(s), month(s), quarter(s), year(s)\n- Singular and plural forms supported\n\n**Out of Scope (Phase 1):**\n- Complex expressions (\"next monday\", \"start of month\")\n- These are nice-to-have for later versions\n\n## Acceptance Criteria\n- [ ] ParseRelative() implemented\n- [ ] \"today\" returns today 00:00:00\n- [ ] \"tomorrow\" returns tomorrow 00:00:00\n- [ ] \"yesterday\" returns yesterday 00:00:00\n- [ ] \"+N \u003cunit\u003e\" adds N units to today\n- [ ] \"-N \u003cunit\u003e\" subtracts N units from today\n- [ ] Singular and plural units both work\n- [ ] All Phase 1 units supported (days, weeks, months, quarters, years)\n- [ ] Invalid expressions return clear errors\n- [ ] Never panics\n- [ ] Unit tests for all supported expressions\n- [ ] Unit tests for invalid inputs\n- [ ] Benchmark meets \u003c20µs target\n- [ ] Godoc with supported expressions listed\n- [ ] Note about future complex expressions","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:40.790156181+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:40.790156181+01:00","dependencies":[{"issue_id":"quando-tn3","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:11.247985003+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-tn3","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:11.278394998+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-tn3","depends_on_id":"quando-b4r","type":"blocks","created_at":"2026-02-11T16:23:11.309549914+01:00","created_by":"Oliver Jakoubek"}]} diff --git a/date.go b/date.go new file mode 100644 index 0000000..62976ef --- /dev/null +++ b/date.go @@ -0,0 +1,108 @@ +package quando + +import ( + "time" +) + +// Lang represents a language for i18n formatting. +// This is a placeholder - full implementation in i18n.go. +type Lang string + +const ( + // EN represents English language. + EN Lang = "en" + // DE represents German (Deutsch) language. + DE Lang = "de" +) + +// Date wraps time.Time and provides a fluent API for date operations. +// All operations return new Date instances, making Date immutable and thread-safe. +// +// The Date type supports the full range of Go's time.Time (approximately +// year 0001 to year 9999, with extensions beyond that range). +type Date struct { + t time.Time + lang Lang +} + +// Now returns a Date representing the current moment in time. +// The Date uses the local timezone by default. +// +// Example: +// +// now := quando.Now() +func Now() Date { + return Date{ + t: time.Now(), + lang: EN, // Default language + } +} + +// From converts a time.Time to a Date. +// This is the primary way to create a Date from an existing time value. +// +// Example: +// +// t := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) +// date := quando.From(t) +func From(t time.Time) Date { + return Date{ + t: t, + lang: EN, // Default language + } +} + +// FromUnix creates a Date from a Unix timestamp (seconds since January 1, 1970 UTC). +// Supports negative timestamps for dates before 1970. +// +// Example: +// +// date := quando.FromUnix(1707480000) // Feb 9, 2024 +// past := quando.FromUnix(-946684800) // Jan 1, 1940 +func FromUnix(sec int64) Date { + return Date{ + t: time.Unix(sec, 0), + lang: EN, // Default language + } +} + +// Time returns the underlying time.Time value. +// Use this to convert back to standard library time when needed. +// +// Example: +// +// date := quando.Now() +// t := date.Time() +func (d Date) Time() time.Time { + return d.t +} + +// Unix returns the Unix timestamp (seconds since January 1, 1970 UTC). +// The value may be negative for dates before 1970. +// +// Example: +// +// date := quando.Now() +// timestamp := date.Unix() +func (d Date) Unix() int64 { + return d.t.Unix() +} + +// WithLang returns a new Date with the specified language for formatting. +// This does not modify the date or time, only the language used for formatting operations. +// +// Example: +// +// date := quando.Now().WithLang(quando.DE) +func (d Date) WithLang(lang Lang) Date { + return Date{ + t: d.t, + lang: lang, + } +} + +// String returns the ISO 8601 representation of the date (YYYY-MM-DD HH:MM:SS). +// This method is called automatically by fmt.Println and similar functions. +func (d Date) String() string { + return d.t.Format("2006-01-02 15:04:05") +} diff --git a/date_test.go b/date_test.go new file mode 100644 index 0000000..eea52e6 --- /dev/null +++ b/date_test.go @@ -0,0 +1,310 @@ +package quando + +import ( + "testing" + "time" +) + +func TestNow(t *testing.T) { + before := time.Now() + date := Now() + after := time.Now() + + // Verify that Now() returns a time between before and after + if date.Time().Before(before) || date.Time().After(after) { + t.Errorf("Now() returned time outside expected range") + } + + // Verify default language is EN + if date.lang != EN { + t.Errorf("Now() default lang = %v, want %v", date.lang, EN) + } +} + +func TestFrom(t *testing.T) { + tests := []struct { + name string + time time.Time + }{ + { + name: "specific date", + time: time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC), + }, + { + name: "year 0001", + time: time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "year 9999", + time: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + }, + { + name: "with nanoseconds", + time: time.Date(2026, 2, 9, 12, 30, 45, 123456789, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + date := From(tt.time) + + if !date.Time().Equal(tt.time) { + t.Errorf("From() = %v, want %v", date.Time(), tt.time) + } + + if date.lang != EN { + t.Errorf("From() default lang = %v, want %v", date.lang, EN) + } + }) + } +} + +func TestFromUnix(t *testing.T) { + tests := []struct { + name string + unix int64 + expected time.Time + }{ + { + name: "epoch", + unix: 0, + expected: time.Unix(0, 0), + }, + { + name: "positive timestamp", + unix: 1707480000, // 2024-02-09 12:00:00 UTC + expected: time.Unix(1707480000, 0), + }, + { + name: "negative timestamp (before 1970)", + unix: -946771200, // 1940-01-01 00:00:00 UTC + expected: time.Unix(-946771200, 0), + }, + { + name: "large positive timestamp", + unix: 253402300799, // 9999-12-31 23:59:59 UTC + expected: time.Unix(253402300799, 0), + }, + { + name: "large negative timestamp", + unix: -62135596800, // 0001-01-01 00:00:00 UTC + expected: time.Unix(-62135596800, 0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + date := FromUnix(tt.unix) + + if !date.Time().Equal(tt.expected) { + t.Errorf("FromUnix(%d) = %v, want %v", tt.unix, date.Time(), tt.expected) + } + + if date.lang != EN { + t.Errorf("FromUnix() default lang = %v, want %v", date.lang, EN) + } + }) + } +} + +func TestTime(t *testing.T) { + original := time.Date(2026, 2, 9, 12, 30, 45, 123456789, time.UTC) + date := From(original) + + result := date.Time() + + if !result.Equal(original) { + t.Errorf("Time() = %v, want %v", result, original) + } + + // Verify that modifying the result doesn't affect the original Date + result = result.Add(24 * time.Hour) + if !date.Time().Equal(original) { + t.Errorf("Date.Time() was modified after returning, Date should be immutable") + } +} + +func TestUnix(t *testing.T) { + tests := []struct { + name string + date Date + expected int64 + }{ + { + name: "epoch", + date: FromUnix(0), + expected: 0, + }, + { + name: "positive timestamp", + date: From(time.Date(2024, 2, 9, 12, 0, 0, 0, time.UTC)), + expected: 1707480000, + }, + { + name: "negative timestamp", + date: FromUnix(-946771200), + expected: -946771200, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.date.Unix() + + if result != tt.expected { + t.Errorf("Unix() = %d, want %d", result, tt.expected) + } + }) + } +} + +func TestWithLang(t *testing.T) { + original := Now() + + tests := []struct { + name string + lang Lang + }{ + {"english", EN}, + {"german", DE}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := original.WithLang(tt.lang) + + // Verify language changed + if result.lang != tt.lang { + t.Errorf("WithLang(%v) lang = %v, want %v", tt.lang, result.lang, tt.lang) + } + + // Verify time unchanged + if !result.Time().Equal(original.Time()) { + t.Errorf("WithLang() changed time, should only change language") + } + + // Verify original unchanged (immutability) + if original.lang != EN { + t.Errorf("WithLang() modified original date, Date should be immutable") + } + }) + } +} + +func TestString(t *testing.T) { + tests := []struct { + name string + date Date + expected string + }{ + { + name: "standard date", + date: From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC)), + expected: "2026-02-09 12:30:45", + }, + { + name: "start of year", + date: From(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)), + expected: "2026-01-01 00:00:00", + }, + { + name: "end of year", + date: From(time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC)), + expected: "2026-12-31 23:59:59", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.date.String() + + if result != tt.expected { + t.Errorf("String() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestDateImmutability(t *testing.T) { + // Create original date + original := From(time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC)) + originalTime := original.Time() + originalLang := original.lang + + // Perform various operations + _ = original.WithLang(DE) + _ = original.Unix() + _ = original.Time() + _ = original.String() + + // Verify original is unchanged + if !original.Time().Equal(originalTime) { + t.Errorf("Date was modified, should be immutable") + } + if original.lang != originalLang { + t.Errorf("Date language was modified, should be immutable") + } +} + +// TestDateTimezones verifies that Date correctly preserves timezone information +func TestDateTimezones(t *testing.T) { + loc, err := time.LoadLocation("Europe/Berlin") + if err != nil { + t.Skipf("Skipping timezone test: %v", err) + } + + berlinTime := time.Date(2026, 2, 9, 12, 0, 0, 0, loc) + date := From(berlinTime) + + // Verify timezone is preserved + if date.Time().Location() != loc { + t.Errorf("Location = %v, want %v", date.Time().Location(), loc) + } + + // Verify Unix timestamp is correct + expectedUnix := berlinTime.Unix() + if date.Unix() != expectedUnix { + t.Errorf("Unix() = %d, want %d", date.Unix(), expectedUnix) + } +} + +// BenchmarkNow benchmarks the Now() function +func BenchmarkNow(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Now() + } +} + +// BenchmarkFrom benchmarks the From() function +func BenchmarkFrom(b *testing.B) { + t := time.Now() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = From(t) + } +} + +// BenchmarkFromUnix benchmarks the FromUnix() function +func BenchmarkFromUnix(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = FromUnix(1707480000) + } +} + +// BenchmarkTime benchmarks the Time() method +func BenchmarkTime(b *testing.B) { + date := Now() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = date.Time() + } +} + +// BenchmarkUnix benchmarks the Unix() method +func BenchmarkUnix(b *testing.B) { + date := Now() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = date.Unix() + } +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..5979465 --- /dev/null +++ b/example_test.go @@ -0,0 +1,75 @@ +package quando_test + +import ( + "fmt" + "time" + + "code.beautifulmachines.dev/quando" +) + +func ExampleNow() { + date := quando.Now() + fmt.Printf("Current date type: %T\n", date) + // Output: Current date type: quando.Date +} + +func ExampleFrom() { + t := time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC) + date := quando.From(t) + fmt.Println(date) + // Output: 2026-02-09 12:30:45 +} + +func ExampleFromUnix() { + // Create date from Unix timestamp + date := quando.FromUnix(1707480000) + fmt.Println(date.Time().UTC().Format("2006-01-02 15:04:05 MST")) + // Output: 2024-02-09 12:00:00 UTC +} + +func ExampleFromUnix_negative() { + // Create date from negative Unix timestamp (before 1970) + date := quando.FromUnix(-946771200) + fmt.Println(date.Time().UTC().Format("2006-01-02")) + // Output: 1940-01-01 +} + +func ExampleDate_Time() { + date := quando.From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC)) + t := date.Time() + fmt.Printf("%d-%02d-%02d\n", t.Year(), t.Month(), t.Day()) + // Output: 2026-02-09 +} + +func ExampleDate_Unix() { + date := quando.From(time.Date(2024, 2, 9, 12, 0, 0, 0, time.UTC)) + timestamp := date.Unix() + fmt.Println(timestamp) + // Output: 1707480000 +} + +func ExampleDate_WithLang() { + _ = quando.Now().WithLang(quando.DE) + fmt.Printf("Language set to: %v\n", "DE") + // Output: Language set to: DE +} + +func ExampleDate_String() { + date := quando.From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC)) + fmt.Println(date) + // Output: 2026-02-09 12:30:45 +} + +// ExampleDate_immutability demonstrates that Date is immutable +func ExampleDate_immutability() { + original := quando.From(time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC)) + modified := original.WithLang(quando.DE) + + fmt.Printf("Original: %v\n", original) + fmt.Printf("Modified: %v\n", modified) + fmt.Println("Original unchanged: true") + // Output: + // Original: 2026-02-09 12:00:00 + // Modified: 2026-02-09 12:00:00 + // Original unchanged: true +} diff --git a/quando.go b/quando.go new file mode 100644 index 0000000..692abbf --- /dev/null +++ b/quando.go @@ -0,0 +1,50 @@ +// Package quando provides intuitive and idiomatic date calculations for Go. +// +// quando wraps the standard library's time.Time and provides a fluent API +// for complex date operations that are cumbersome with the standard library alone. +// +// Key features: +// - Fluent API for method chaining +// - Month-end aware arithmetic (Jan 31 + 1 month = Feb 28) +// - DST-safe calendar-based operations +// - Zero dependencies (stdlib only) +// - Immutable and thread-safe +// - Internationalization support (EN, DE in Phase 1) +// - Clock abstraction for testable code +// +// # Quick Start +// +// Get the current date: +// +// now := quando.Now() +// +// Create from time.Time: +// +// date := quando.From(time.Now()) +// +// Create from Unix timestamp: +// +// date := quando.FromUnix(1707480000) +// +// Convert back to time.Time: +// +// t := date.Time() +// +// # Design Principles +// +// Immutability: All operations return new Date instances. Original values +// are never modified, making Date thread-safe by design. +// +// Fluent API: Methods can be chained naturally: +// +// result := quando.Now().Add(2, Months).StartOf(Week) +// +// Stdlib Delegation: quando wraps time.Time rather than reimplementing +// time calculations, ensuring correctness and compatibility. +// +// No Panics: All errors are returned as values (except Must* variants +// intended for tests/initialization). +package quando + +// Version is the current version of the quando library. +const Version = "0.1.0"