quando/example_test.go
Oliver Jakoubek 065b767b54 feat(quando-gr5): implement automatic date parsing with format detection
Implement Parse() function that automatically detects and parses common
date formats without requiring explicit layout strings. This provides an
intuitive API for parsing dates from various sources while maintaining
type safety through proper error handling.

Supported formats:
- ISO format (YYYY-MM-DD): "2026-02-09"
- ISO with slash (YYYY/MM/DD): "2026/02/09"
- EU format (DD.MM.YYYY): "09.02.2026"
- RFC2822/RFC1123: "Mon, 09 Feb 2026 00:00:00 +0000"

Key features:
- Detects and rejects ambiguous slash formats (e.g., "01/02/2026")
- Returns clear, contextual errors for invalid or ambiguous inputs
- Never panics - all errors via return values
- Zero allocations for successful parses
- Comprehensive test coverage (98%)

Performance results:
- ISO format: 105.5 ns/op (94x faster than 10µs target)
- ISO slash: 118.3 ns/op
- EU format: 117.4 ns/op
- RFC2822: 257.8 ns/op

Test coverage:
- 42 unit tests covering valid formats, error cases, edge cases
- 3 example tests demonstrating usage patterns
- Benchmarks for all format types
- Parse() function: 100% coverage

Files added:
- parse.go: Main implementation with Parse() and helper functions
- parse_test.go: Comprehensive test suite with table-driven tests

Files modified:
- example_test.go: Added ExampleParse examples
2026-02-11 18:53:16 +01:00

412 lines
12 KiB
Go

package quando_test
import (
"errors"
"fmt"
"time"
"code.beautifulmachines.dev/jakoubek/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
}
// 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
}
// ExampleUnit demonstrates the Unit type
func ExampleUnit() {
// Units are used with Add, Sub, StartOf, EndOf operations
units := []quando.Unit{
quando.Seconds,
quando.Minutes,
quando.Hours,
quando.Days,
quando.Weeks,
quando.Months,
quando.Quarters,
quando.Years,
}
for _, u := range units {
fmt.Println(u.String())
}
// Output:
// seconds
// minutes
// hours
// days
// weeks
// months
// quarters
// years
}
// ExampleUnit_String demonstrates the String method
func ExampleUnit_String() {
unit := quando.Days
fmt.Printf("Unit: %s\n", unit)
// Output: Unit: days
}
// ExampleDate_StartOf demonstrates snapping to the beginning of time units
func ExampleDate_StartOf() {
date := quando.From(time.Date(2026, 2, 15, 15, 30, 45, 0, time.UTC)) // Sunday, Feb 15
fmt.Println("Original:", date)
fmt.Println("StartOf(Week):", date.StartOf(quando.Weeks)) // Monday
fmt.Println("StartOf(Month):", date.StartOf(quando.Months)) // Feb 1
fmt.Println("StartOf(Quarter):", date.StartOf(quando.Quarters)) // Jan 1 (Q1)
fmt.Println("StartOf(Year):", date.StartOf(quando.Years)) // Jan 1
// Output:
// Original: 2026-02-15 15:30:45
// StartOf(Week): 2026-02-09 00:00:00
// StartOf(Month): 2026-02-01 00:00:00
// StartOf(Quarter): 2026-01-01 00:00:00
// StartOf(Year): 2026-01-01 00:00:00
}
// ExampleDate_EndOf demonstrates snapping to the end of time units
func ExampleDate_EndOf() {
date := quando.From(time.Date(2026, 2, 9, 15, 30, 45, 0, time.UTC)) // Monday, Feb 9
fmt.Println("Original:", date)
fmt.Println("EndOf(Week):", date.EndOf(quando.Weeks)) // Sunday
fmt.Println("EndOf(Month):", date.EndOf(quando.Months)) // Feb 28
fmt.Println("EndOf(Quarter):", date.EndOf(quando.Quarters)) // Mar 31 (Q1)
fmt.Println("EndOf(Year):", date.EndOf(quando.Years)) // Dec 31
// Output:
// Original: 2026-02-09 15:30:45
// EndOf(Week): 2026-02-15 23:59:59
// EndOf(Month): 2026-02-28 23:59:59
// EndOf(Quarter): 2026-03-31 23:59:59
// EndOf(Year): 2026-12-31 23:59:59
}
// ExampleDate_StartOf_chaining demonstrates chaining snap operations
func ExampleDate_StartOf_chaining() {
// Get the first Monday of the current quarter
date := quando.Now()
firstMondayOfQuarter := date.StartOf(quando.Quarters).StartOf(quando.Weeks)
fmt.Printf("Type: %T\n", firstMondayOfQuarter)
// Output: Type: quando.Date
}
// ExampleDate_Next demonstrates finding the next occurrence of a weekday
func ExampleDate_Next() {
// On Monday, Feb 9, 2026
date := quando.From(time.Date(2026, 2, 9, 15, 30, 0, 0, time.UTC))
fmt.Println("Today:", date.Time().Weekday())
fmt.Println("Next Monday:", date.Next(time.Monday).Time().Weekday(), "-", date.Next(time.Monday))
fmt.Println("Next Friday:", date.Next(time.Friday).Time().Weekday(), "-", date.Next(time.Friday))
// Output:
// Today: Monday
// Next Monday: Monday - 2026-02-16 15:30:00
// Next Friday: Friday - 2026-02-13 15:30:00
}
// ExampleDate_Prev demonstrates finding the previous occurrence of a weekday
func ExampleDate_Prev() {
// On Monday, Feb 9, 2026
date := quando.From(time.Date(2026, 2, 9, 15, 30, 0, 0, time.UTC))
fmt.Println("Today:", date.Time().Weekday())
fmt.Println("Prev Monday:", date.Prev(time.Monday).Time().Weekday(), "-", date.Prev(time.Monday))
fmt.Println("Prev Friday:", date.Prev(time.Friday).Time().Weekday(), "-", date.Prev(time.Friday))
// Output:
// Today: Monday
// Prev Monday: Monday - 2026-02-02 15:30:00
// Prev Friday: Friday - 2026-02-06 15:30:00
}
// ExampleDate_Next_sameWeekday demonstrates the same-weekday edge case
func ExampleDate_Next_sameWeekday() {
// Next ALWAYS returns future, never today (even if same weekday)
monday := quando.From(time.Date(2026, 2, 9, 15, 30, 0, 0, time.UTC)) // Monday
nextMonday := monday.Next(time.Monday)
fmt.Printf("Days later: %d\n", int(nextMonday.Time().Sub(monday.Time()).Hours()/24))
// Output: Days later: 7
}
// ExampleDiff demonstrates calculating the duration between two dates
func ExampleDiff() {
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC)
dur := quando.Diff(start, end)
fmt.Printf("Days: %d\n", dur.Days())
fmt.Printf("Months: %d\n", dur.Months())
fmt.Printf("Years: %d\n", dur.Years())
// Output:
// Days: 364
// Months: 11
// Years: 0
}
// ExampleDuration_MonthsFloat demonstrates precise month calculations
func ExampleDuration_MonthsFloat() {
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 2, 16, 0, 0, 0, 0, time.UTC)
dur := quando.Diff(start, end)
intMonths := dur.Months()
floatMonths := dur.MonthsFloat()
fmt.Printf("Integer months: %d\n", intMonths)
fmt.Printf("Float months: %.2f\n", floatMonths)
// Output:
// Integer months: 1
// Float months: 1.54
}
// ExampleDuration_negative demonstrates negative durations
func ExampleDuration_negative() {
// When start is after end, duration is negative
start := time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
dur := quando.Diff(start, end)
fmt.Printf("Months: %d\n", dur.Months())
fmt.Printf("Years: %d\n", dur.Years())
// Output:
// Months: -12
// Years: -1
}
// ExampleErrInvalidFormat demonstrates handling invalid date formats
func Example_errorHandling() {
// Note: Parse doesn't exist yet, so this is a conceptual example
// showing the error handling pattern that will be used
// Simulate an error by directly using the sentinel error
err := quando.ErrInvalidFormat
// Check for specific error type
if errors.Is(err, quando.ErrInvalidFormat) {
fmt.Println("Invalid format detected")
}
// Output: Invalid format detected
}
// Example_errorTypes demonstrates all error types
func Example_errorTypes() {
// Show all defined error types
errors := []error{
quando.ErrInvalidFormat,
quando.ErrInvalidTimezone,
quando.ErrOverflow,
}
for _, err := range errors {
fmt.Println(err.Error())
}
// Output:
// invalid date format
// invalid timezone
// date overflow
}
// ExampleDate_Add demonstrates date arithmetic
func ExampleDate_Add() {
date := quando.From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC))
fmt.Println("Original:", date)
fmt.Println("+1 day:", date.Add(1, quando.Days))
fmt.Println("+1 month:", date.Add(1, quando.Months))
fmt.Println("+1 year:", date.Add(1, quando.Years))
// Output:
// Original: 2026-01-15 12:00:00
// +1 day: 2026-01-16 12:00:00
// +1 month: 2026-02-15 12:00:00
// +1 year: 2027-01-15 12:00:00
}
// ExampleDate_Add_monthEndOverflow demonstrates month-end overflow behavior
func ExampleDate_Add_monthEndOverflow() {
// When adding months, if target day doesn't exist, snap to month end
date := quando.From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC))
fmt.Println("Jan 31 + 1 month:", date.Add(1, quando.Months))
fmt.Println("Jan 31 + 2 months:", date.Add(2, quando.Months))
// Output:
// Jan 31 + 1 month: 2026-02-28 12:00:00
// Jan 31 + 2 months: 2026-03-31 12:00:00
}
// ExampleDate_Sub demonstrates date subtraction
func ExampleDate_Sub() {
date := quando.From(time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC))
fmt.Println("Original:", date)
fmt.Println("-1 day:", date.Sub(1, quando.Days))
fmt.Println("-1 month:", date.Sub(1, quando.Months))
// Output:
// Original: 2026-03-31 12:00:00
// -1 day: 2026-03-30 12:00:00
// -1 month: 2026-02-28 12:00:00
}
// ExampleDate_Add_chaining demonstrates method chaining
func ExampleDate_Add_chaining() {
date := quando.From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
result := date.
Add(1, quando.Months).
Add(15, quando.Days).
Sub(2, quando.Hours)
fmt.Println(result)
// Output: 2026-02-16 10:00:00
}
// ExampleParse demonstrates basic date parsing with automatic format detection
func ExampleParse() {
date, err := quando.Parse("2026-02-09")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(date)
// Output: 2026-02-09 00:00:00
}
// ExampleParse_formats demonstrates parsing various supported date formats
func ExampleParse_formats() {
formats := []string{
"2026-02-09", // ISO format (YYYY-MM-DD)
"2026/02/09", // ISO with slash (YYYY/MM/DD)
"09.02.2026", // EU format (DD.MM.YYYY)
}
for _, f := range formats {
date, err := quando.Parse(f)
if err != nil {
fmt.Printf("Error parsing %s: %v\n", f, err)
continue
}
fmt.Println(date)
}
// Output:
// 2026-02-09 00:00:00
// 2026-02-09 00:00:00
// 2026-02-09 00:00:00
}
// ExampleParse_error demonstrates error handling for ambiguous formats
func ExampleParse_error() {
// Slash format without year prefix is ambiguous
// (could be US: MM/DD/YYYY or EU: DD/MM/YYYY)
_, err := quando.Parse("01/02/2026")
if errors.Is(err, quando.ErrInvalidFormat) {
fmt.Println("Ambiguous format detected")
}
// Output: Ambiguous format detected
}