From d0cbff9ff807f98f3e19030de8bfac6295b5d049 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Wed, 11 Feb 2026 16:33:23 +0100 Subject: [PATCH] feat(quando-vih): implement Clock abstraction for testability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Clock interface with Now() and From(t time.Time) methods - Implement DefaultClock using time.Now() for production code - Implement FixedClock with fixed time for deterministic testing - Add factory functions NewClock() and NewFixedClock(time.Time) - Comprehensive unit tests demonstrating deterministic test patterns - Edge case testing (epoch, year 0001, year 9999, nanoseconds) - Timezone preservation tests - Example tests showing test usage patterns - Performance benchmarks for both clock implementations - 100% test coverage (exceeds 95% requirement) All acceptance criteria met: ✓ Clock interface defined ✓ DefaultClock implementation using time.Now() ✓ FixedClock implementation with fixed time ✓ NewClock() factory function ✓ NewFixedClock(time.Time) factory function ✓ Unit tests demonstrating deterministic test patterns ✓ Godoc comments ✓ Example test showing test usage pattern --- .beads/issues.jsonl | 4 +- clock.go | 79 +++++++++++++++ clock_test.go | 230 ++++++++++++++++++++++++++++++++++++++++++++ example_test.go | 39 ++++++++ 4 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 clock.go create mode 100644 clock_test.go diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b8e9759..34ddb55 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -13,10 +13,10 @@ {"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":"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-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-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"}]} -{"id":"quando-vih","title":"Clock abstraction for testability","description":"Implement Clock interface to enable dependency injection and deterministic testing.\n\n**Technical Details:**\n```go\ntype Clock interface {\n Now() Date\n From(t time.Time) Date\n}\n```\n\n**Implementations:**\n- DefaultClock: Uses time.Now()\n- FixedClock: Returns fixed time for tests\n\n**API:**\n- `NewClock()` - returns DefaultClock\n- `NewFixedClock(time.Time)` - returns FixedClock for tests\n\n## Acceptance Criteria\n- [ ] Clock interface defined\n- [ ] DefaultClock implementation using time.Now()\n- [ ] FixedClock implementation with fixed time\n- [ ] NewClock() factory function\n- [ ] NewFixedClock(time.Time) factory function\n- [ ] Unit tests demonstrating deterministic test patterns\n- [ ] Godoc comments\n- [ ] Example test showing test usage pattern","status":"open","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:33.357927572+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:20:33.357927572+01:00","dependencies":[{"issue_id":"quando-vih","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.308383809+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"quando-vih","title":"Clock abstraction for testability","description":"Implement Clock interface to enable dependency injection and deterministic testing.\n\n**Technical Details:**\n```go\ntype Clock interface {\n Now() Date\n From(t time.Time) Date\n}\n```\n\n**Implementations:**\n- DefaultClock: Uses time.Now()\n- FixedClock: Returns fixed time for tests\n\n**API:**\n- `NewClock()` - returns DefaultClock\n- `NewFixedClock(time.Time)` - returns FixedClock for tests\n\n## Acceptance Criteria\n- [ ] Clock interface defined\n- [ ] DefaultClock implementation using time.Now()\n- [ ] FixedClock implementation with fixed time\n- [ ] NewClock() factory function\n- [ ] NewFixedClock(time.Time) factory function\n- [ ] Unit tests demonstrating deterministic test patterns\n- [ ] Godoc comments\n- [ ] Example test showing test usage pattern","status":"in_progress","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:33.357927572+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:31:38.756484443+01:00","dependencies":[{"issue_id":"quando-vih","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.308383809+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":3,"issue_id":"quando-vih","author":"Oliver Jakoubek","text":"Plan: 1) Create clock.go with Clock interface (Now, From methods), 2) Implement DefaultClock using time.Now(), 3) Implement FixedClock with fixed time for deterministic tests, 4) Add factory functions NewClock() and NewFixedClock(time.Time), 5) Write comprehensive unit tests demonstrating deterministic test patterns, 6) Add example tests showing test usage, 7) Ensure all exports have godoc comments","created_at":"2026-02-11T15:31:45Z"}]} {"id":"quando-wny","title":"Explicit parsing with layout","description":"Implement explicit parsing using Go's standard layout format.\n\n**API:**\n```go\nfunc ParseWithLayout(s, layout string) (Date, error)\n```\n\n**Purpose:**\nHandle ambiguous or custom formats by providing explicit layout\n\n**Examples:**\n```go\nParseWithLayout(\"01/02/2026\", \"02/01/2006\") // EU format\nParseWithLayout(\"01/02/2026\", \"01/02/2006\") // US format\nParseWithLayout(\"9. Februar 2026\", \"2. January 2006\") // German custom\n```\n\n**Implementation:**\n- Delegate to time.Parse() with layout\n- Wrap result in quando.Date\n- Return clear errors for invalid inputs\n\n## Acceptance Criteria\n- [ ] ParseWithLayout() implemented\n- [ ] Uses Go's standard layout format (reference date)\n- [ ] Wraps time.Parse() correctly\n- [ ] Returns Date on success\n- [ ] Returns error on parse failure\n- [ ] Never panics\n- [ ] Unit tests for various layouts\n- [ ] Unit tests for invalid inputs\n- [ ] Benchmark meets \u003c10µs target\n- [ ] Godoc with Go layout format explanation\n- [ ] Example tests showing EU vs US disambiguation","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:33.246073999+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:33.246073999+01:00","dependencies":[{"issue_id":"quando-wny","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:11.182490834+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-wny","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:11.216169487+01:00","created_by":"Oliver Jakoubek"}]} {"id":"quando-zbr","title":"Language constants and i18n infrastructure","description":"Define language constants and i18n infrastructure for multi-language support.\n\n**API:**\n```go\ntype Lang string\n\nconst (\n LangEN Lang = \"en\" // English (Default)\n LangDE Lang = \"de\" // Deutsch\n)\n\nfunc (d Date) Lang(lang Lang) Date // Fluent API\n```\n\n**Phase 1 Languages:**\n- EN (English) - default\n- DE (Deutsch) - must-have\n\n**i18n Applies To:**\n- Format(Long) - \"February 9, 2026\" vs \"9. Februar 2026\"\n- Custom layouts with month/weekday names\n- Human() duration format\n\n**i18n Does NOT Apply To:**\n- ISO, EU, US, RFC2822 formats (always language-independent)\n- Numeric outputs (WeekNumber, Quarter, etc.)\n\n## Acceptance Criteria\n- [ ] Lang type defined as string\n- [ ] LangEN and LangDE constants defined\n- [ ] Lang() method on Date for fluent API\n- [ ] Month names in EN and DE\n- [ ] Weekday names in EN and DE\n- [ ] Time unit names for Human() in EN and DE\n- [ ] Unit tests for language switching\n- [ ] Godoc comments\n- [ ] Documentation noting future language expansion (21 more)","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:19.72536676+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:19.72536676+01:00","dependencies":[{"issue_id":"quando-zbr","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:09.056424359+01:00","created_by":"Oliver Jakoubek"}]} diff --git a/clock.go b/clock.go new file mode 100644 index 0000000..3a8ff9a --- /dev/null +++ b/clock.go @@ -0,0 +1,79 @@ +package quando + +import "time" + +// Clock provides an abstraction for time operations to enable deterministic testing. +// Use DefaultClock in production and FixedClock in tests. +// +// Example production code: +// +// clock := quando.NewClock() +// date := clock.Now() +// +// Example test code: +// +// fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) +// clock := quando.NewFixedClock(fixedTime) +// date := clock.Now() // Always returns Feb 9, 2026 +type Clock interface { + // Now returns the current date according to this clock. + Now() Date + + // From converts a time.Time to a Date using this clock's configuration. + From(t time.Time) Date +} + +// DefaultClock is the standard clock implementation that uses the system time. +// It returns the actual current time when Now() is called. +type DefaultClock struct{} + +// NewClock returns a new DefaultClock that uses the system time. +// This is the clock to use in production code. +// +// Example: +// +// clock := quando.NewClock() +// now := clock.Now() +func NewClock() Clock { + return &DefaultClock{} +} + +// Now returns the current date using the system time. +func (c *DefaultClock) Now() Date { + return Now() +} + +// From converts a time.Time to a Date. +func (c *DefaultClock) From(t time.Time) Date { + return From(t) +} + +// FixedClock is a clock implementation that always returns the same time. +// This is useful for deterministic testing. +type FixedClock struct { + fixedTime time.Time +} + +// NewFixedClock returns a new FixedClock that always returns the specified time. +// This is primarily intended for testing. +// +// Example: +// +// // In tests +// fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) +// clock := quando.NewFixedClock(fixedTime) +// date := clock.Now() // Always returns Feb 9, 2026 12:00:00 +func NewFixedClock(t time.Time) Clock { + return &FixedClock{fixedTime: t} +} + +// Now returns the fixed time configured for this clock. +func (c *FixedClock) Now() Date { + return From(c.fixedTime) +} + +// From converts a time.Time to a Date. +// For FixedClock, this behaves the same as the DefaultClock. +func (c *FixedClock) From(t time.Time) Date { + return From(t) +} diff --git a/clock_test.go b/clock_test.go new file mode 100644 index 0000000..471ff27 --- /dev/null +++ b/clock_test.go @@ -0,0 +1,230 @@ +package quando + +import ( + "testing" + "time" +) + +func TestNewClock(t *testing.T) { + clock := NewClock() + + if clock == nil { + t.Fatal("NewClock() returned nil") + } + + // Verify it's a DefaultClock + if _, ok := clock.(*DefaultClock); !ok { + t.Errorf("NewClock() returned %T, want *DefaultClock", clock) + } +} + +func TestDefaultClock_Now(t *testing.T) { + clock := NewClock() + + before := time.Now() + date := clock.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("DefaultClock.Now() returned time outside expected range") + } +} + +func TestDefaultClock_From(t *testing.T) { + clock := NewClock() + testTime := time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC) + + date := clock.From(testTime) + + if !date.Time().Equal(testTime) { + t.Errorf("DefaultClock.From() = %v, want %v", date.Time(), testTime) + } +} + +func TestNewFixedClock(t *testing.T) { + fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := NewFixedClock(fixedTime) + + if clock == nil { + t.Fatal("NewFixedClock() returned nil") + } + + // Verify it's a FixedClock + if _, ok := clock.(*FixedClock); !ok { + t.Errorf("NewFixedClock() returned %T, want *FixedClock", clock) + } +} + +func TestFixedClock_Now(t *testing.T) { + fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := NewFixedClock(fixedTime) + + // Call Now() multiple times to verify it always returns the same time + date1 := clock.Now() + time.Sleep(1 * time.Millisecond) + date2 := clock.Now() + time.Sleep(1 * time.Millisecond) + date3 := clock.Now() + + // All should return the same fixed time + if !date1.Time().Equal(fixedTime) { + t.Errorf("FixedClock.Now() (call 1) = %v, want %v", date1.Time(), fixedTime) + } + if !date2.Time().Equal(fixedTime) { + t.Errorf("FixedClock.Now() (call 2) = %v, want %v", date2.Time(), fixedTime) + } + if !date3.Time().Equal(fixedTime) { + t.Errorf("FixedClock.Now() (call 3) = %v, want %v", date3.Time(), fixedTime) + } + + // Verify all three are equal to each other + if !date1.Time().Equal(date2.Time()) || !date2.Time().Equal(date3.Time()) { + t.Error("FixedClock.Now() returned different times on successive calls") + } +} + +func TestFixedClock_From(t *testing.T) { + fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := NewFixedClock(fixedTime) + + // Test with different time + testTime := time.Date(2025, 5, 15, 8, 30, 0, 0, time.UTC) + date := clock.From(testTime) + + // From() should use the provided time, not the fixed time + if !date.Time().Equal(testTime) { + t.Errorf("FixedClock.From() = %v, want %v", date.Time(), testTime) + } +} + +// TestFixedClock_DeterministicTesting demonstrates how FixedClock enables deterministic tests +func TestFixedClock_DeterministicTesting(t *testing.T) { + // This test demonstrates a deterministic test pattern using FixedClock + + // Create a fixed clock for a specific test scenario + testTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := NewFixedClock(testTime) + + // Use the clock in test code + now := clock.Now() + + // We can now make deterministic assertions + expected := "2026-02-09 12:00:00" + if now.String() != expected { + t.Errorf("String() = %v, want %v", now.String(), expected) + } + + expectedUnix := int64(1770638400) + if now.Unix() != expectedUnix { + t.Errorf("Unix() = %d, want %d", now.Unix(), expectedUnix) + } +} + +// TestClock_Interface verifies that both implementations satisfy the Clock interface +func TestClock_Interface(t *testing.T) { + var _ Clock = &DefaultClock{} + var _ Clock = &FixedClock{} + + // This test will fail at compile time if either type doesn't implement Clock +} + +// TestClock_EdgeCases tests edge cases for clock implementations +func TestClock_EdgeCases(t *testing.T) { + tests := []struct { + name string + fixedTime time.Time + }{ + { + name: "epoch", + fixedTime: time.Unix(0, 0), + }, + { + name: "year 0001", + fixedTime: time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "year 9999", + fixedTime: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + }, + { + name: "with nanoseconds", + fixedTime: time.Date(2026, 2, 9, 12, 30, 45, 123456789, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clock := NewFixedClock(tt.fixedTime) + date := clock.Now() + + if !date.Time().Equal(tt.fixedTime) { + t.Errorf("FixedClock.Now() = %v, want %v", date.Time(), tt.fixedTime) + } + }) + } +} + +// TestClock_Timezones verifies that clocks preserve timezone information +func TestClock_Timezones(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) + + // Test DefaultClock + defaultClock := NewClock() + date1 := defaultClock.From(berlinTime) + if date1.Time().Location() != loc { + t.Errorf("DefaultClock.From() location = %v, want %v", date1.Time().Location(), loc) + } + + // Test FixedClock + fixedClock := NewFixedClock(berlinTime) + date2 := fixedClock.Now() + if date2.Time().Location() != loc { + t.Errorf("FixedClock.Now() location = %v, want %v", date2.Time().Location(), loc) + } +} + +// BenchmarkDefaultClock_Now benchmarks DefaultClock.Now() +func BenchmarkDefaultClock_Now(b *testing.B) { + clock := NewClock() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = clock.Now() + } +} + +// BenchmarkFixedClock_Now benchmarks FixedClock.Now() +func BenchmarkFixedClock_Now(b *testing.B) { + fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := NewFixedClock(fixedTime) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = clock.Now() + } +} + +// BenchmarkDefaultClock_From benchmarks DefaultClock.From() +func BenchmarkDefaultClock_From(b *testing.B) { + clock := NewClock() + t := time.Now() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = clock.From(t) + } +} + +// BenchmarkFixedClock_From benchmarks FixedClock.From() +func BenchmarkFixedClock_From(b *testing.B) { + fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := NewFixedClock(fixedTime) + t := time.Now() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = clock.From(t) + } +} diff --git a/example_test.go b/example_test.go index 5979465..4581a81 100644 --- a/example_test.go +++ b/example_test.go @@ -73,3 +73,42 @@ func ExampleDate_immutability() { // Modified: 2026-02-09 12:00:00 // Original unchanged: true } + +// ExampleNewClock demonstrates creating a default clock +func ExampleNewClock() { + clock := quando.NewClock() + _ = clock.Now() + fmt.Println("Clock created") + // Output: Clock created +} + +// ExampleNewFixedClock demonstrates creating a fixed clock for testing +func ExampleNewFixedClock() { + // Create a fixed clock that always returns the same time + fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := quando.NewFixedClock(fixedTime) + + // Now() always returns the fixed time + date := clock.Now() + fmt.Println(date) + // Output: 2026-02-09 12:00:00 +} + +// ExampleFixedClock_deterministic demonstrates deterministic testing with FixedClock +func ExampleFixedClock_deterministic() { + // In tests, use a fixed clock for deterministic behavior + testTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC) + clock := quando.NewFixedClock(testTime) + + // Call multiple times - always returns same time + date1 := clock.Now() + date2 := clock.Now() + + fmt.Printf("Date 1: %v\n", date1) + fmt.Printf("Date 2: %v\n", date2) + fmt.Printf("Same: %v\n", date1.Unix() == date2.Unix()) + // Output: + // Date 1: 2026-02-09 12:00:00 + // Date 2: 2026-02-09 12:00:00 + // Same: true +}