feat(quando-wny): implement explicit parsing with layout format

Add ParseWithLayout() function to handle ambiguous and custom date formats
by providing an explicit Go layout string.

Implementation:
- ParseWithLayout(s, layout string) delegates to time.Parse()
- Wraps result in quando.Date with default EN language
- Returns ErrInvalidFormat on parse failure
- Trims whitespace and validates empty input
- Never panics, always returns errors as values

Features:
- Disambiguate US vs EU slash formats (01/02/2026)
- Support custom formats with month names (9. February 2026)
- Full Go layout format support (reference date: Mon Jan 2 15:04:05 MST 2006)
- Thread-safe and immutable

Testing:
- 13 success test cases (US/EU, custom formats, edge cases)
- 8 error test cases (invalid inputs, validation)
- Immutability test
- 2 benchmarks: ~87-104 ns/op (100x faster than 10µs target)
- Zero allocations
- 100% test coverage for new code
- 3 example tests demonstrating key use cases

Files modified:
- parse.go: Added ParseWithLayout() with comprehensive godoc
- parse_test.go: Added 21 test cases + 2 benchmarks
- example_test.go: Added 3 example functions
This commit is contained in:
Oliver Jakoubek 2026-02-11 19:51:09 +01:00
commit 00353c2d4b
4 changed files with 346 additions and 1 deletions

View file

@ -150,3 +150,58 @@ func isYearPrefix(s string) bool {
return true
}
// ParseWithLayout parses a date string using an explicit Go layout format.
// This is useful for disambiguating ambiguous formats or parsing custom formats
// that cannot be automatically detected.
//
// Layout Format:
// Go uses a reference date approach. The layout string must use the reference date:
// Mon Jan 2 15:04:05 MST 2006
//
// Components you can use in your layout:
// Year: 2006 (4-digit), 06 (2-digit)
// Month: 01 (2-digit), 1 (1-digit), Jan (short), January (long)
// Day: 02 (2-digit), 2 (1-digit), _2 (space-padded)
// Weekday: Mon (short), Monday (long)
// Hour: 15 (24-hour), 03 (12-hour), 3 (1-digit 12-hour)
// Minute: 04 (2-digit), 4 (1-digit)
// Second: 05 (2-digit), 5 (1-digit)
// AM/PM: PM
// Timezone: MST (abbrev), -0700 (offset), Z07:00 (ISO 8601)
//
// Note: Month and weekday names must be in English (Go limitation).
//
// Examples:
//
// // Disambiguate US vs EU slash format
// ParseWithLayout("01/02/2026", "01/02/2006") // US: January 2, 2026
// ParseWithLayout("01/02/2026", "02/01/2006") // EU: February 1, 2026
//
// // Custom format with text month
// ParseWithLayout("9. February 2026", "2. January 2006") // February 9, 2026
//
// // ISO 8601 with time
// ParseWithLayout("2026-02-09T14:30:00", "2006-01-02T15:04:05")
//
// If the string cannot be parsed with the given layout, returns an error
// wrapping ErrInvalidFormat. The returned Date uses UTC timezone unless
// the layout and input include timezone information.
func ParseWithLayout(s, layout string) (Date, error) {
// Trim whitespace from input
s = strings.TrimSpace(s)
// Empty input check
if s == "" {
return Date{}, fmt.Errorf("parsing date with layout %q: empty input: %w", layout, ErrInvalidFormat)
}
// Parse using time.Parse with the provided layout
t, err := time.Parse(layout, s)
if err != nil {
return Date{}, fmt.Errorf("parsing date %q with layout %q: %w", s, layout, ErrInvalidFormat)
}
// Wrap in quando.Date with default language
return Date{t: t, lang: EN}, nil
}