mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat: Added timezone infrastructure.
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
- [ ] display an error when no part of the date appears in the output.
|
- [ ] display an error when no part of the date appears in the output.
|
||||||
- [x] use `date.format` as the default format.
|
- [x] use `date.format` as the default format.
|
||||||
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
||||||
|
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
|
||||||
|
|
||||||
## General
|
## General
|
||||||
- [ ] Integrity hash
|
- [ ] Integrity hash
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import "core:log"
|
|||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
import "core:time/datetime"
|
import "core:time/datetime"
|
||||||
|
import "core:time/timezone"
|
||||||
|
|
||||||
Date_Components :: struct {
|
Date_Components :: struct {
|
||||||
year: int,
|
year: int,
|
||||||
@@ -226,3 +227,101 @@ emit_am_pm_lower :: proc(b: ^strings.Builder, dt: Date_Components) {
|
|||||||
strings.write_string(b, "pm" if dt.hour >= 12 else "am")
|
strings.write_string(b, "pm" if dt.hour >= 12 else "am")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Timezone conversion infrastructure
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
format_offset :: proc(offset_seconds: int) -> string {
|
||||||
|
if offset_seconds == 0 do return "UTC"
|
||||||
|
sign := "+" if offset_seconds > 0 else "-"
|
||||||
|
abs_val := abs(offset_seconds)
|
||||||
|
hours := abs_val / 3600
|
||||||
|
minutes := (abs_val % 3600) / 60
|
||||||
|
return fmt.tprintf("UTC%s%02d:%02d", sign, hours, minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tz_cache caches loaded TZ_Region pointers by timezone name.
|
||||||
|
// Entries persist until destroy_tz_cache is called.
|
||||||
|
tz_cache: map[string]^datetime.TZ_Region
|
||||||
|
|
||||||
|
get_cached_tz :: proc(name: string) -> (^datetime.TZ_Region, bool) {
|
||||||
|
if name == "" || name == "UTC" {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
if cached, ok := tz_cache[name]; ok {
|
||||||
|
return cached, true
|
||||||
|
}
|
||||||
|
tz, ok := timezone.region_load(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
tz_cache[name] = tz
|
||||||
|
return tz, true
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_tz_cache :: proc() {
|
||||||
|
for _, tz in tz_cache {
|
||||||
|
if tz != nil {
|
||||||
|
timezone.region_destroy(tz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(tz_cache)
|
||||||
|
tz_cache = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert_to_tz converts date components from their source timezone to a
|
||||||
|
// target timezone.
|
||||||
|
//
|
||||||
|
// If the source has no offset (has_offset=false), the components are
|
||||||
|
// assumed to be in the target timezone — only the abbreviation is resolved.
|
||||||
|
//
|
||||||
|
// If the source has an offset, the components are first adjusted to true
|
||||||
|
// UTC, then converted to the target timezone.
|
||||||
|
//
|
||||||
|
// Precondition: target_tz != nil.
|
||||||
|
convert_to_tz :: proc(
|
||||||
|
c: Date_Components,
|
||||||
|
target_tz: ^datetime.TZ_Region,
|
||||||
|
) -> (
|
||||||
|
result: Date_Components,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
result = c
|
||||||
|
dt := datetime.DateTime {
|
||||||
|
year = i64(c.year),
|
||||||
|
month = i8(c.month),
|
||||||
|
day = i8(c.day),
|
||||||
|
hour = i8(c.hour),
|
||||||
|
minute = i8(c.minute),
|
||||||
|
second = i8(c.second),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.has_offset {
|
||||||
|
dt.tz = target_tz
|
||||||
|
abbr, _ := timezone.shortname(dt)
|
||||||
|
result.tz_abbr = abbr
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := time.datetime_to_time(dt) or_return
|
||||||
|
|
||||||
|
secs := time.time_to_unix(tm) - i64(c.offset_seconds)
|
||||||
|
tm = time.unix(secs, 0)
|
||||||
|
|
||||||
|
dt_utc := time.time_to_datetime(tm) or_return
|
||||||
|
dt_out := timezone.datetime_to_tz(dt_utc, target_tz) or_return
|
||||||
|
|
||||||
|
abbr, _ := timezone.shortname(dt_out)
|
||||||
|
|
||||||
|
return {
|
||||||
|
year = int(dt_out.year),
|
||||||
|
month = int(dt_out.month),
|
||||||
|
day = int(dt_out.day),
|
||||||
|
hour = int(dt_out.hour),
|
||||||
|
minute = int(dt_out.minute),
|
||||||
|
second = int(dt_out.second),
|
||||||
|
tz_abbr = abbr,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,3 +209,104 @@ test_format_date_combined_go_reference_layout :: proc(t: ^testing.T) {
|
|||||||
result := format_date(dt, "Mon Jan 2 15:04:05 MST 2006")
|
result := format_date(dt, "Mon Jan 2 15:04:05 MST 2006")
|
||||||
testing.expect_value(t, result, "Sun Oct 15 13:18:50 UTC 2023")
|
testing.expect_value(t, result, "Sun Oct 15 13:18:50 UTC 2023")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// format_offset
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_format_offset_zero :: proc(t: ^testing.T) {
|
||||||
|
testing.expect_value(t, format_offset(0), "UTC")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_format_offset_negative :: proc(t: ^testing.T) {
|
||||||
|
testing.expect_value(t, format_offset(-14400), "UTC-04:00")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_format_offset_positive :: proc(t: ^testing.T) {
|
||||||
|
testing.expect_value(t, format_offset(19800), "UTC+05:30")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// get_cached_tz / convert_to_tz (require system zoneinfo)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_get_cached_tz_utc_returns_nil :: proc(t: ^testing.T) {
|
||||||
|
tz, ok := get_cached_tz("UTC")
|
||||||
|
testing.expect_value(t, ok, true)
|
||||||
|
testing.expect_value(t, tz == nil, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_get_cached_tz_empty_returns_nil :: proc(t: ^testing.T) {
|
||||||
|
tz, ok := get_cached_tz("")
|
||||||
|
testing.expect_value(t, ok, true)
|
||||||
|
testing.expect_value(t, tz == nil, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_get_cached_tz_loads_named_region :: proc(t: ^testing.T) {
|
||||||
|
tz, ok := get_cached_tz("America/New_York")
|
||||||
|
defer destroy_tz_cache()
|
||||||
|
if !ok do return
|
||||||
|
testing.expect(t, tz != nil, "should load TZ_Region for America/New_York")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_convert_to_tz_no_offset_assumes_target :: proc(t: ^testing.T) {
|
||||||
|
tz, tz_ok := get_cached_tz("America/New_York")
|
||||||
|
defer destroy_tz_cache()
|
||||||
|
if !tz_ok do return
|
||||||
|
|
||||||
|
c := Date_Components{
|
||||||
|
year = 2026, month = 3, day = 15,
|
||||||
|
hour = 8, minute = 49, second = 54,
|
||||||
|
}
|
||||||
|
result, ok := convert_to_tz(c, tz)
|
||||||
|
testing.expect_value(t, ok, true)
|
||||||
|
testing.expect_value(t, result.hour, 8)
|
||||||
|
testing.expect_value(t, result.minute, 49)
|
||||||
|
testing.expect(t, len(result.tz_abbr) > 0, "should resolve abbreviation")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_convert_to_tz_with_offset_converts :: proc(t: ^testing.T) {
|
||||||
|
tz, tz_ok := get_cached_tz("America/New_York")
|
||||||
|
defer destroy_tz_cache()
|
||||||
|
if !tz_ok do return
|
||||||
|
|
||||||
|
// 2026-03-15T12:49:54Z (UTC) → 08:49:54 EDT (UTC-4)
|
||||||
|
c := Date_Components{
|
||||||
|
year = 2026, month = 3, day = 15,
|
||||||
|
hour = 12, minute = 49, second = 54,
|
||||||
|
offset_seconds = 0,
|
||||||
|
has_offset = true,
|
||||||
|
}
|
||||||
|
result, ok := convert_to_tz(c, tz)
|
||||||
|
testing.expect_value(t, ok, true)
|
||||||
|
testing.expect_value(t, result.hour, 8)
|
||||||
|
testing.expect_value(t, result.minute, 49)
|
||||||
|
testing.expect_value(t, result.tz_abbr, "EDT")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_convert_to_tz_with_negative_offset_converts :: proc(t: ^testing.T) {
|
||||||
|
tz, tz_ok := get_cached_tz("America/New_York")
|
||||||
|
defer destroy_tz_cache()
|
||||||
|
if !tz_ok do return
|
||||||
|
|
||||||
|
// 2026-03-15T08:49:54-04:00 → UTC 12:49:54 → EDT 08:49:54
|
||||||
|
c := Date_Components{
|
||||||
|
year = 2026, month = 3, day = 15,
|
||||||
|
hour = 8, minute = 49, second = 54,
|
||||||
|
offset_seconds = -14400,
|
||||||
|
has_offset = true,
|
||||||
|
}
|
||||||
|
result, ok := convert_to_tz(c, tz)
|
||||||
|
testing.expect_value(t, ok, true)
|
||||||
|
testing.expect_value(t, result.hour, 8)
|
||||||
|
testing.expect_value(t, result.tz_abbr, "EDT")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user