feat(quando-41g): implement timezone conversion with DST support

Add In() method to Date type for converting between IANA timezones.
Implements comprehensive DST handling with proper wall-clock time
preservation across daylight saving transitions.

Features:
- In(location) converts to specified IANA timezone
- Returns ErrInvalidTimezone for invalid timezone names
- Never panics on invalid input
- Preserves language settings across conversions
- Maintains immutability pattern

DST Handling:
- Add(1, Days) preserves wall clock time, not duration
- Tested across spring forward (Mar 29, 2026)
- Tested across fall back (Oct 25, 2026)

Testing:
- 100% coverage for In() method
- 6 comprehensive test functions (228 lines)
- Tests for Europe/Berlin, America/New_York, Asia/Tokyo, UTC
- Error handling tests (empty string, invalid timezones)
- Immutability and language preservation tests
- 3 example tests demonstrating usage

Overall coverage: 98.1%
This commit is contained in:
Oliver Jakoubek 2026-02-11 19:19:07 +01:00
commit 57f9f689d9
4 changed files with 319 additions and 4 deletions

39
date.go
View file

@ -1,6 +1,7 @@
package quando
import (
"fmt"
"time"
)
@ -101,6 +102,44 @@ func (d Date) WithLang(lang Lang) Date {
}
}
// In converts the date to the specified IANA timezone.
// Returns error for invalid timezone names. Never panics.
//
// The method uses the IANA Timezone Database (e.g., "America/New_York", "Europe/Berlin", "UTC").
// Daylight Saving Time (DST) transitions are handled automatically by the timezone database.
//
// When combined with arithmetic operations, DST-safe behavior is preserved:
// Add(1, Days) means "same wall clock time on next calendar day", not "24 hours later".
//
// Example:
//
// utc := quando.From(time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC))
// berlin, err := utc.In("Europe/Berlin")
// // berlin is 2026-06-15 14:00:00 CEST (UTC+2 in summer)
//
// For a list of valid timezone names, see: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
func (d Date) In(location string) (Date, error) {
// Validate input
if location == "" {
return Date{}, fmt.Errorf("timezone location is empty: %w", ErrInvalidTimezone)
}
// Load timezone from IANA database
loc, err := time.LoadLocation(location)
if err != nil {
return Date{}, fmt.Errorf("loading timezone %q: %w", location, ErrInvalidTimezone)
}
// Convert time to new timezone
converted := d.t.In(loc)
// Return new Date with converted time, preserving language
return Date{
t: converted,
lang: d.lang,
}, nil
}
// String returns the ISO 8601 representation of the date (YYYY-MM-DD HH:MM:SS).
// This method is called automatically by fmt.Println and similar functions.
func (d Date) String() string {