feat(quando-tn3): implement relative date parsing

Add ParseRelative() and ParseRelativeWithClock() for parsing relative
date expressions like "today", "tomorrow", "+2 days", "-1 week", etc.

Features:
- Keywords: today, tomorrow, yesterday (case-insensitive)
- Relative offsets: +/-N <unit> format
- Supported units: day(s), week(s), month(s), quarter(s), year(s)
- All results return at 00:00:00 in local timezone
- Comprehensive error handling with ErrInvalidFormat

Implementation:
- parse.go: Added ParseRelative(), ParseRelativeWithClock(), parseUnitString()
- parse_test.go: Added 38 test cases covering all expressions and errors
- example_test.go: Added 4 example functions demonstrating usage

Test results:
- 100% code coverage for all ParseRelative functions
- Benchmarks: ~67ns (keywords), ~563ns (offsets) - well under <20µs target
- All existing tests pass

Complex expressions like "next monday" or "start of month" are out of
scope for Phase 1 and documented for future implementation.
This commit is contained in:
Oliver Jakoubek 2026-02-11 20:01:35 +01:00
commit 8c9e0e725a
4 changed files with 518 additions and 1 deletions

View file

@ -568,3 +568,64 @@ func ExampleParseWithLayout_error() {
}
// Output: Invalid date format detected
}
// ExampleParseRelative demonstrates parsing relative date expressions
func ExampleParseRelative() {
// Simple keywords (results depend on current date)
today, _ := quando.ParseRelative("today")
tomorrow, _ := quando.ParseRelative("tomorrow")
yesterday, _ := quando.ParseRelative("yesterday")
fmt.Printf("Type: %T\n", today)
fmt.Printf("Type: %T\n", tomorrow)
fmt.Printf("Type: %T\n", yesterday)
// Output:
// Type: quando.Date
// Type: quando.Date
// Type: quando.Date
}
// ExampleParseRelative_offsets demonstrates relative offset expressions
func ExampleParseRelative_offsets() {
// Note: Results depend on current date
// Using ParseRelativeWithClock for deterministic example
clock := quando.NewFixedClock(time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC))
twoDaysFromNow, _ := quando.ParseRelativeWithClock("+2 days", clock)
oneWeekAgo, _ := quando.ParseRelativeWithClock("-1 week", clock)
threeMonthsFromNow, _ := quando.ParseRelativeWithClock("+3 months", clock)
fmt.Println(twoDaysFromNow)
fmt.Println(oneWeekAgo)
fmt.Println(threeMonthsFromNow)
// Output:
// 2026-02-17 00:00:00
// 2026-02-08 00:00:00
// 2026-05-15 00:00:00
}
// ExampleParseRelative_caseInsensitive demonstrates case-insensitive parsing
func ExampleParseRelative_caseInsensitive() {
clock := quando.NewFixedClock(time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC))
// All of these work
date1, _ := quando.ParseRelativeWithClock("today", clock)
date2, _ := quando.ParseRelativeWithClock("TODAY", clock)
date3, _ := quando.ParseRelativeWithClock("Today", clock)
fmt.Println(date1.Unix() == date2.Unix())
fmt.Println(date2.Unix() == date3.Unix())
// Output:
// true
// true
}
// ExampleParseRelative_error demonstrates error handling
func ExampleParseRelative_error() {
_, err := quando.ParseRelative("next monday") // Not supported in Phase 1
if errors.Is(err, quando.ErrInvalidFormat) {
fmt.Println("Complex expressions not yet supported")
}
// Output: Complex expressions not yet supported
}