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:
+100
-1
@@ -5,6 +5,7 @@ import "core:log"
|
||||
import "core:strings"
|
||||
import "core:time"
|
||||
import "core:time/datetime"
|
||||
import "core:time/timezone"
|
||||
|
||||
Date_Components :: struct {
|
||||
year: int,
|
||||
@@ -98,7 +99,7 @@ parse_offset :: proc(iso: string, c: ^Date_Components) {
|
||||
c.offset_seconds = sign * (hours * 3600 + minutes * 60)
|
||||
c.has_offset = true
|
||||
case:
|
||||
// no recognizable offset
|
||||
// no recognizable offset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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")
|
||||
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