feat(quando-5ol): implement format presets and constants

Add Format type with preset constants (ISO, EU, US, Long, RFC2822) and
Format() method for convenient date formatting.

Key features:
- Type-safe Format enum with 5 preset formats
- ISO (2026-02-09), EU (09.02.2026), US (02/09/2026)
- Long format with i18n support (EN: "February 9, 2026", DE: "9. Februar 2026")
- RFC2822 email format
- Language-independent formats (ISO, EU, US, RFC2822) ignore Lang setting
- Language-dependent Long format respects WithLang()

Implementation:
- format.go: Format type, constants, Format() method, formatLong() helper
- format_test.go: 26 tests covering all formats, edge cases, immutability
- example_test.go: 8 example functions demonstrating usage

Performance (exceeds targets):
- ISO/EU/US: ~90 ns (target: <5 µs)
- RFC2822: ~207 ns (target: <5 µs)
- Long EN/DE: ~270 ns (target: <10 µs)

All tests pass with comprehensive coverage of leap years, month boundaries,
and language dependency.
This commit is contained in:
Oliver Jakoubek 2026-02-11 20:12:00 +01:00
commit 06ad5d67d3
4 changed files with 506 additions and 1 deletions

View file

@ -3,7 +3,7 @@
{"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":"closed","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-11T19:18:57.926412162+01:00","closed_at":"2026-02-11T19:18:57.926412162+01:00","close_reason":"Closed","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-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-5ol","title":"Format presets and constants","description":"Implement preset format constants and Format() method.\n\n**API:**\n```go\ntype Format int\n\nconst (\n ISO Format = iota // \"2026-02-09\"\n EU // \"09.02.2026\"\n US // \"02/09/2026\"\n Long // \"February 9, 2026\" (language-dependent)\n RFC2822 // \"Mon, 09 Feb 2026 00:00:00 +0000\"\n)\n\nfunc (d Date) Format(format Format) string\n```\n\n**Language Dependency:**\n- ISO, EU, US, RFC2822: Always language-independent\n- Long: Uses Lang setting\n - EN: \"February 9, 2026\"\n - DE: \"9. Februar 2026\"\n\n**Implementation:**\n- Map Format constants to Go layout strings\n- Use Lang() setting for Long format\n- Delegate to time.Format() internally\n\n## Acceptance Criteria\n- [ ] Format type and constants defined\n- [ ] Format() method implemented\n- [ ] ISO format outputs \"YYYY-MM-DD\"\n- [ ] EU format outputs \"DD.MM.YYYY\"\n- [ ] US format outputs \"MM/DD/YYYY\"\n- [ ] RFC2822 format correct\n- [ ] Long format respects Lang setting (EN and DE)\n- [ ] Non-Long formats ignore Lang setting\n- [ ] Unit tests for all formats\n- [ ] Unit tests for Long with both EN and DE\n- [ ] Benchmark meets \u003c5µs without i18n, \u003c10µs with i18n\n- [ ] Godoc for each format constant\n- [ ] Example tests showing each format","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:53.217816331+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:53.217816331+01:00","dependencies":[{"issue_id":"quando-5ol","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:12.164278602+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-5ol","depends_on_id":"quando-zbr","type":"blocks","created_at":"2026-02-11T16:23:12.20045666+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-5ol","title":"Format presets and constants","description":"Implement preset format constants and Format() method.\n\n**API:**\n```go\ntype Format int\n\nconst (\n ISO Format = iota // \"2026-02-09\"\n EU // \"09.02.2026\"\n US // \"02/09/2026\"\n Long // \"February 9, 2026\" (language-dependent)\n RFC2822 // \"Mon, 09 Feb 2026 00:00:00 +0000\"\n)\n\nfunc (d Date) Format(format Format) string\n```\n\n**Language Dependency:**\n- ISO, EU, US, RFC2822: Always language-independent\n- Long: Uses Lang setting\n - EN: \"February 9, 2026\"\n - DE: \"9. Februar 2026\"\n\n**Implementation:**\n- Map Format constants to Go layout strings\n- Use Lang() setting for Long format\n- Delegate to time.Format() internally\n\n## Acceptance Criteria\n- [ ] Format type and constants defined\n- [ ] Format() method implemented\n- [ ] ISO format outputs \"YYYY-MM-DD\"\n- [ ] EU format outputs \"DD.MM.YYYY\"\n- [ ] US format outputs \"MM/DD/YYYY\"\n- [ ] RFC2822 format correct\n- [ ] Long format respects Lang setting (EN and DE)\n- [ ] Non-Long formats ignore Lang setting\n- [ ] Unit tests for all formats\n- [ ] Unit tests for Long with both EN and DE\n- [ ] Benchmark meets \u003c5µs without i18n, \u003c10µs with i18n\n- [ ] Godoc for each format constant\n- [ ] Example tests showing each format","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:53.217816331+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T20:11:47.455205479+01:00","closed_at":"2026-02-11T20:11:47.455205479+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-5ol","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:12.164278602+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-5ol","depends_on_id":"quando-zbr","type":"blocks","created_at":"2026-02-11T16:23:12.20045666+01:00","created_by":"Oliver Jakoubek"}]}
{"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":"tombstone","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-11T19:21:16.246958875+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"}],"deleted_at":"2026-02-11T19:21:16.246958875+01:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
{"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"}]}

View file

@ -629,3 +629,77 @@ func ExampleParseRelative_error() {
}
// Output: Complex expressions not yet supported
}
// ExampleDate_Format demonstrates basic date formatting
func ExampleDate_Format() {
date := quando.From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
fmt.Println(date.Format(quando.ISO))
fmt.Println(date.Format(quando.EU))
fmt.Println(date.Format(quando.US))
fmt.Println(date.Format(quando.Long))
// Output:
// 2026-02-09
// 09.02.2026
// 02/09/2026
// February 9, 2026
}
// ExampleDate_Format_isoFormat demonstrates ISO 8601 format
func ExampleDate_Format_isoFormat() {
date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
fmt.Println(date.Format(quando.ISO))
// Output: 2026-02-09
}
// ExampleDate_Format_euFormat demonstrates European format
func ExampleDate_Format_euFormat() {
date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
fmt.Println(date.Format(quando.EU))
// Output: 09.02.2026
}
// ExampleDate_Format_usFormat demonstrates US format
func ExampleDate_Format_usFormat() {
date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
fmt.Println(date.Format(quando.US))
// Output: 02/09/2026
}
// ExampleDate_Format_longFormat demonstrates long format in English
func ExampleDate_Format_longFormat() {
date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
fmt.Println(date.Format(quando.Long))
// Output: February 9, 2026
}
// ExampleDate_Format_longFormatGerman demonstrates long format in German
func ExampleDate_Format_longFormatGerman() {
date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)).WithLang(quando.DE)
fmt.Println(date.Format(quando.Long))
// Output: 9. Februar 2026
}
// ExampleDate_Format_rfc2822Format demonstrates RFC 2822 email format
func ExampleDate_Format_rfc2822Format() {
date := quando.From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
fmt.Println(date.Format(quando.RFC2822))
// Output: Mon, 09 Feb 2026 12:30:45 +0000
}
// ExampleDate_Format_languageIndependence demonstrates that most formats ignore language
func ExampleDate_Format_languageIndependence() {
date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
// ISO format is always the same regardless of language
dateEN := date.WithLang(quando.EN)
dateDE := date.WithLang(quando.DE)
fmt.Println("ISO (EN):", dateEN.Format(quando.ISO))
fmt.Println("ISO (DE):", dateDE.Format(quando.ISO))
fmt.Println("Same:", dateEN.Format(quando.ISO) == dateDE.Format(quando.ISO))
// Output:
// ISO (EN): 2026-02-09
// ISO (DE): 2026-02-09
// Same: true
}

152
format.go Normal file
View file

@ -0,0 +1,152 @@
package quando
import (
"fmt"
"time"
)
// Format represents a preset date format for use with the Format method.
// Each format produces a different string representation of a date.
//
// Language Dependency:
// - ISO, EU, US, RFC2822: Always language-independent
// - Long: Uses the Date's Lang setting for month and weekday names
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
// iso := date.Format(quando.ISO) // "2026-02-09"
// long := date.Format(quando.Long) // "February 9, 2026" (EN)
// longDE := date.WithLang(quando.DE).Format(quando.Long) // "9. Februar 2026"
type Format int
const (
// ISO represents the ISO 8601 date format: "2026-02-09" (YYYY-MM-DD).
// This format is always language-independent and is the standard international format.
ISO Format = iota
// EU represents the European date format: "09.02.2026" (DD.MM.YYYY).
// This format is always language-independent and uses dots as separators.
// Common in Germany, Austria, Switzerland, and many other European countries.
EU
// US represents the US date format: "02/09/2026" (MM/DD/YYYY).
// This format is always language-independent and uses slashes as separators.
// Common in the United States and some other countries.
US
// Long represents a human-readable long format with full month name.
// This format is language-dependent and uses the Date's Lang setting.
//
// Examples:
// - EN: "February 9, 2026"
// - DE: "9. Februar 2026"
//
// The format varies by language to match local conventions.
Long
// RFC2822 represents the RFC 2822 email date format.
// Example: "Mon, 09 Feb 2026 12:30:45 +0000"
// This format is always language-independent and includes time and timezone.
RFC2822
)
// Format formats the date using the specified preset format.
//
// Supported formats:
// - ISO: "2026-02-09" (YYYY-MM-DD)
// - EU: "09.02.2026" (DD.MM.YYYY)
// - US: "02/09/2026" (MM/DD/YYYY)
// - Long: "February 9, 2026" (language-dependent)
// - RFC2822: "Mon, 09 Feb 2026 12:30:45 +0000"
//
// The Long format respects the Date's Lang setting:
// - EN: "February 9, 2026"
// - DE: "9. Februar 2026"
//
// All other formats are language-independent.
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
// fmt.Println(date.Format(quando.ISO)) // "2026-02-09"
// fmt.Println(date.Format(quando.EU)) // "09.02.2026"
// fmt.Println(date.Format(quando.US)) // "02/09/2026"
// fmt.Println(date.Format(quando.Long)) // "February 9, 2026"
// fmt.Println(date.Format(quando.RFC2822)) // "Mon, 09 Feb 2026 12:30:45 +0000"
func (d Date) Format(format Format) string {
t := d.t
switch format {
case ISO:
// ISO 8601 format: YYYY-MM-DD
return t.Format("2006-01-02")
case EU:
// European format: DD.MM.YYYY
return t.Format("02.01.2006")
case US:
// US format: MM/DD/YYYY
return t.Format("01/02/2006")
case Long:
// Long format with full month name (language-dependent)
// EN: "February 9, 2026"
// DE: "9. Februar 2026"
return d.formatLong()
case RFC2822:
// RFC 2822 email format
return t.Format(time.RFC1123Z)
default:
// Fallback to ISO format for unknown formats
return t.Format("2006-01-02")
}
}
// formatLong formats the date in long format with language-specific conventions.
// This is a helper method for Format(Long).
func (d Date) formatLong() string {
t := d.t
lang := d.lang
if lang == "" {
lang = EN // Default to English if no language set
}
// Get localized month name
monthName := lang.MonthName(t.Month())
// Different formats for different languages
switch lang {
case DE:
// German format: "9. Februar 2026"
// Pattern: day without leading zero + ". " + month + " " + year
return fmt.Sprintf("%d. %s %d", t.Day(), monthName, t.Year())
default:
// English format (and fallback): "February 9, 2026"
// Pattern: month + " " + day + ", " + year
return fmt.Sprintf("%s %d, %d", monthName, t.Day(), t.Year())
}
}
// String returns the string representation of the Format type.
// This is used for better test output and debugging.
func (f Format) String() string {
switch f {
case ISO:
return "ISO"
case EU:
return "EU"
case US:
return "US"
case Long:
return "Long"
case RFC2822:
return "RFC2822"
default:
return "Unknown"
}
}

279
format_test.go Normal file
View file

@ -0,0 +1,279 @@
package quando
import (
"testing"
"time"
)
func TestFormat(t *testing.T) {
// Fixed date for testing: Feb 9, 2026 at 12:30:45 UTC
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
tests := []struct {
name string
format Format
expected string
}{
// ISO format
{
name: "ISO format",
format: ISO,
expected: "2026-02-09",
},
// EU format
{
name: "EU format",
format: EU,
expected: "09.02.2026",
},
// US format
{
name: "US format",
format: US,
expected: "02/09/2026",
},
// Long format (default EN)
{
name: "Long format (EN)",
format: Long,
expected: "February 9, 2026",
},
// RFC2822 format
{
name: "RFC2822 format",
format: RFC2822,
expected: "Mon, 09 Feb 2026 12:30:45 +0000",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := date.Format(tt.format)
if result != tt.expected {
t.Errorf("Format(%v) = %q, want %q", tt.format, result, tt.expected)
}
})
}
}
func TestFormatLong_LanguageDependency(t *testing.T) {
// Fixed date for testing: Feb 9, 2026
baseDate := From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
tests := []struct {
name string
lang Lang
expected string
}{
{
name: "Long format EN",
lang: EN,
expected: "February 9, 2026",
},
{
name: "Long format DE",
lang: DE,
expected: "9. Februar 2026",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
date := baseDate.WithLang(tt.lang)
result := date.Format(Long)
if result != tt.expected {
t.Errorf("Format(Long) with lang=%v = %q, want %q", tt.lang, result, tt.expected)
}
})
}
}
func TestFormat_LanguageIndependence(t *testing.T) {
// Verify that ISO, EU, US, RFC2822 formats ignore Lang setting
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
formats := []struct {
format Format
expected string
}{
{ISO, "2026-02-09"},
{EU, "09.02.2026"},
{US, "02/09/2026"},
{RFC2822, "Mon, 09 Feb 2026 12:30:45 +0000"},
}
for _, tc := range formats {
t.Run(tc.format.String(), func(t *testing.T) {
// Test with EN
resultEN := date.WithLang(EN).Format(tc.format)
// Test with DE
resultDE := date.WithLang(DE).Format(tc.format)
// Both should be identical (language-independent)
if resultEN != tc.expected {
t.Errorf("Format(%v) with EN = %q, want %q", tc.format, resultEN, tc.expected)
}
if resultDE != tc.expected {
t.Errorf("Format(%v) with DE = %q, want %q", tc.format, resultDE, tc.expected)
}
if resultEN != resultDE {
t.Errorf("Format(%v) should be language-independent: EN=%q, DE=%q", tc.format, resultEN, resultDE)
}
})
}
}
func TestFormat_EdgeCases(t *testing.T) {
tests := []struct {
name string
date Date
format Format
expected string
}{
// Leap year
{
name: "Leap year Feb 29",
date: From(time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)),
format: ISO,
expected: "2024-02-29",
},
{
name: "Leap year Feb 29 (Long EN)",
date: From(time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)),
format: Long,
expected: "February 29, 2024",
},
{
name: "Leap year Feb 29 (Long DE)",
date: From(time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)).WithLang(DE),
format: Long,
expected: "29. Februar 2024",
},
// Year boundaries
{
name: "New Year's Day",
date: From(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)),
format: ISO,
expected: "2026-01-01",
},
{
name: "Year end",
date: From(time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC)),
format: ISO,
expected: "2026-12-31",
},
// Month boundaries
{
name: "Start of month",
date: From(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)),
format: EU,
expected: "01.05.2026",
},
{
name: "End of month (30 days)",
date: From(time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC)),
format: EU,
expected: "30.06.2026",
},
{
name: "End of month (31 days)",
date: From(time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC)),
format: EU,
expected: "31.07.2026",
},
// Different months (test all months)
{
name: "January (Long EN)",
date: From(time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC)),
format: Long,
expected: "January 15, 2026",
},
{
name: "December (Long DE)",
date: From(time.Date(2026, 12, 15, 0, 0, 0, 0, time.UTC)).WithLang(DE),
format: Long,
expected: "15. Dezember 2026",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.date.Format(tt.format)
if result != tt.expected {
t.Errorf("Format(%v) = %q, want %q", tt.format, result, tt.expected)
}
})
}
}
func TestFormat_Immutability(t *testing.T) {
// Format should not modify the original Date
original := From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
// Format multiple times
_ = original.Format(ISO)
_ = original.Format(EU)
_ = original.Format(Long)
// Verify original is unchanged
expected := time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)
if !original.Time().Equal(expected) {
t.Errorf("Format modified the original date: got %v, want %v", original.Time(), expected)
}
}
// Benchmarks
func BenchmarkFormat_ISO(b *testing.B) {
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = date.Format(ISO)
}
}
func BenchmarkFormat_EU(b *testing.B) {
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = date.Format(EU)
}
}
func BenchmarkFormat_US(b *testing.B) {
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = date.Format(US)
}
}
func BenchmarkFormat_Long_EN(b *testing.B) {
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC)).WithLang(EN)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = date.Format(Long)
}
}
func BenchmarkFormat_Long_DE(b *testing.B) {
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC)).WithLang(DE)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = date.Format(Long)
}
}
func BenchmarkFormat_RFC2822(b *testing.B) {
date := From(time.Date(2026, 2, 9, 12, 30, 45, 0, time.UTC))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = date.Format(RFC2822)
}
}