feat(quando-95w): implement custom layout formatting with i18n support

Add FormatLayout() method enabling custom date formatting using Go's
standard layout format (Mon Jan 2 15:04:05 MST 2006) with full
internationalization support for month and weekday names.

Key features:
- Custom layout strings using Go's time.Format() reference date
- i18n support for EN and DE languages
- Translates both full and abbreviated month/weekday names
- Fast path for EN (direct passthrough, ~144-173 ns/op)
- Optimized replacements using strings.NewReplacer (~7.3-7.5 µs/op for DE)
- Comprehensive test coverage (97.6%)

Implementation:
- Added FormatLayout() method to format.go
- Uses strings.NewReplacer for atomic replacements to avoid substring collisions
- Added 280+ lines of tests covering all months, weekdays, and edge cases
- Added 4 benchmark tests (all meet <10µs target)
- Added 4 example tests demonstrating usage

Performance:
- EN (fast path): ~144-173 ns/op
- DE (with i18n): ~7.3-7.5 µs/op
- Both well under <10µs requirement

Files changed:
- format.go: Added FormatLayout() method with full godoc
- format_test.go: Added comprehensive tests and benchmarks
- example_test.go: Added 4 example functions
This commit is contained in:
Oliver Jakoubek 2026-02-11 20:34:57 +01:00
commit 39397ea6df
4 changed files with 413 additions and 1 deletions

View file

@ -2,6 +2,7 @@ package quando
import (
"fmt"
"strings"
"time"
)
@ -132,6 +133,92 @@ func (d Date) formatLong() string {
}
}
// FormatLayout formats the date using a custom layout string with localized month/weekday names.
//
// The layout parameter uses Go's standard time layout format (Mon Jan 2 15:04:05 MST 2006).
// Month and weekday names in the output are translated according to the Date's Lang setting.
//
// Supported layout components (see time.Format documentation for full details):
// - "January" - Full month name (localized)
// - "Jan" - Short month name (localized)
// - "Monday" - Full weekday name (localized)
// - "Mon" - Short weekday name (localized)
// - All numeric components (year, day, hour, etc.) - not localized
//
// Language Support:
// - EN (English) - Default, no translation
// - DE (German) - Translates month/weekday names
//
// Performance: < 10 µs for typical layouts with i18n
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 14, 30, 0, 0, time.UTC))
//
// // English (default)
// date.FormatLayout("Monday, 2. January 2006") // "Monday, 9. February 2026"
//
// // German
// date.WithLang(quando.DE).FormatLayout("Monday, 2. January 2006") // "Montag, 9. Februar 2026"
// date.WithLang(quando.DE).FormatLayout("Mon, 02 Jan 2006") // "Mo, 09 Feb 2026"
func (d Date) FormatLayout(layout string) string {
// Fast path: if lang is EN (or not set), just use Go's format directly
lang := d.lang
if lang == "" || lang == EN {
return d.t.Format(layout)
}
// Format using Go's time.Format (always in English)
formatted := d.t.Format(layout)
// Build replacement pairs: old (English) -> new (localized)
// Order matters: longest strings first to avoid partial matches
// We use strings.Replacer which processes all replacements in a single pass
var replacementPairs []string
// 1. Full month names first (e.g., "September" before "Sep")
for m := time.January; m <= time.December; m++ {
enFull := monthNames[EN][m-1]
localFull := lang.MonthName(m)
if enFull != localFull {
replacementPairs = append(replacementPairs, enFull, localFull)
}
}
// 2. Full weekday names (e.g., "Wednesday" before "Wed")
for wd := time.Sunday; wd <= time.Saturday; wd++ {
enFull := weekdayNames[EN][wd]
localFull := lang.WeekdayName(wd)
if enFull != localFull {
replacementPairs = append(replacementPairs, enFull, localFull)
}
}
// 3. Short month names
for m := time.January; m <= time.December; m++ {
enShort := monthNamesShort[EN][m-1]
localShort := lang.MonthNameShort(m)
if enShort != localShort {
replacementPairs = append(replacementPairs, enShort, localShort)
}
}
// 4. Short weekday names
for wd := time.Sunday; wd <= time.Saturday; wd++ {
enShort := weekdayNamesShort[EN][wd]
localShort := lang.WeekdayNameShort(wd)
if enShort != localShort {
replacementPairs = append(replacementPairs, enShort, localShort)
}
}
// Create a replacer and apply all replacements in a single pass
// This ensures that once a full name is replaced, the short name in the
// replacement won't be affected (e.g., "Monday" -> "Montag", and "Mon" in "Montag" won't become "Mo")
replacer := strings.NewReplacer(replacementPairs...)
return replacer.Replace(formatted)
}
// String returns the string representation of the Format type.
// This is used for better test output and debugging.
func (f Format) String() string {