mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat(parse_iso_Date): Added timezone parsing.
This commit is contained in:
+66
-7
@@ -7,12 +7,15 @@ import "core:time"
|
|||||||
import "core:time/datetime"
|
import "core:time/datetime"
|
||||||
|
|
||||||
Date_Components :: struct {
|
Date_Components :: struct {
|
||||||
year: int,
|
year: int,
|
||||||
month: int,
|
month: int,
|
||||||
day: int,
|
day: int,
|
||||||
hour: int,
|
hour: int,
|
||||||
minute: int,
|
minute: int,
|
||||||
second: int,
|
second: int,
|
||||||
|
offset_seconds: int,
|
||||||
|
has_offset: bool,
|
||||||
|
tz_abbr: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use some kind of scanner interface
|
// TODO: Use some kind of scanner interface
|
||||||
@@ -38,6 +41,9 @@ parse_iso_date :: proc(iso: string) -> (c: Date_Components, ok: bool) {
|
|||||||
c.second = parse_2_digits(iso, 17)
|
c.second = parse_2_digits(iso, 17)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parse_offset(iso, &c)
|
||||||
|
|
||||||
|
c.tz_abbr = "UTC"
|
||||||
return c, true
|
return c, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +54,54 @@ parse_2_digits :: proc(s: string, offset: int) -> int {
|
|||||||
return (int(s[offset]) - 0x30) * 10 + (int(s[offset + 1]) - 0x30)
|
return (int(s[offset]) - 0x30) * 10 + (int(s[offset + 1]) - 0x30)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse_offset parses the timezone suffix of an ISO 8601 string (Z,
|
||||||
|
// +HH:MM, +HHMM, -HH:MM, -HHMM). Skips fractional seconds if present.
|
||||||
|
// Does nothing if no recognizable offset is found.
|
||||||
|
parse_offset :: proc(iso: string, c: ^Date_Components) {
|
||||||
|
pos := 19
|
||||||
|
if pos >= len(iso) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip fractional seconds (e.g., .123)
|
||||||
|
if iso[pos] == '.' {
|
||||||
|
pos += 1
|
||||||
|
for pos < len(iso) && iso[pos] >= '0' && iso[pos] <= '9' {
|
||||||
|
pos += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos >= len(iso) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch iso[pos] {
|
||||||
|
case 'Z', 'z':
|
||||||
|
c.has_offset = true
|
||||||
|
case '+', '-':
|
||||||
|
sign := 1 if iso[pos] == '+' else -1
|
||||||
|
pos += 1
|
||||||
|
if pos + 1 >= len(iso) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hours := parse_2_digits(iso, pos)
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
minutes := 0
|
||||||
|
if pos < len(iso) && iso[pos] == ':' {
|
||||||
|
pos += 1
|
||||||
|
}
|
||||||
|
if pos + 1 < len(iso) {
|
||||||
|
minutes = parse_2_digits(iso, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.offset_seconds = sign * (hours * 3600 + minutes * 60)
|
||||||
|
c.has_offset = true
|
||||||
|
case:
|
||||||
|
// no recognizable offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
format_date :: proc(
|
format_date :: proc(
|
||||||
dt: Date_Components,
|
dt: Date_Components,
|
||||||
fmt: string,
|
fmt: string,
|
||||||
@@ -80,7 +134,12 @@ match_token :: proc(b: ^strings.Builder, dt: Date_Components, s: string) -> int
|
|||||||
s,
|
s,
|
||||||
"2006",
|
"2006",
|
||||||
) {strings.write_string(b, fmt.tprintf("%04d", dt.year)); return 4}
|
) {strings.write_string(b, fmt.tprintf("%04d", dt.year)); return 4}
|
||||||
if strings.has_prefix(s, "MST") {strings.write_string(b, "UTC"); return 3}
|
if strings.has_prefix(s, "MST") {
|
||||||
|
abbr := dt.tz_abbr
|
||||||
|
if len(abbr) == 0 do abbr = "UTC"
|
||||||
|
strings.write_string(b, abbr)
|
||||||
|
return 3
|
||||||
|
}
|
||||||
if strings.has_prefix(s, "Jan") {emit_month_abbr(b, dt); return 3}
|
if strings.has_prefix(s, "Jan") {emit_month_abbr(b, dt); return 3}
|
||||||
if strings.has_prefix(s, "Mon") {emit_weekday(b, dt, full = false); return 3}
|
if strings.has_prefix(s, "Mon") {emit_weekday(b, dt, full = false); return 3}
|
||||||
if strings.has_prefix(
|
if strings.has_prefix(
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ test_parse_iso_date_extracts_time :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, c.hour, 8)
|
testing.expect_value(t, c.hour, 8)
|
||||||
testing.expect_value(t, c.minute, 49)
|
testing.expect_value(t, c.minute, 49)
|
||||||
testing.expect_value(t, c.second, 54)
|
testing.expect_value(t, c.second, 54)
|
||||||
|
testing.expect_value(t, c.has_offset, true)
|
||||||
|
testing.expect_value(t, c.offset_seconds, -14400)
|
||||||
|
testing.expect_value(t, c.tz_abbr, "UTC")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -26,6 +29,8 @@ test_parse_iso_date_lowercase_t :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, c.hour, 8)
|
testing.expect_value(t, c.hour, 8)
|
||||||
testing.expect_value(t, c.minute, 49)
|
testing.expect_value(t, c.minute, 49)
|
||||||
testing.expect_value(t, c.second, 54)
|
testing.expect_value(t, c.second, 54)
|
||||||
|
testing.expect_value(t, c.has_offset, true)
|
||||||
|
testing.expect_value(t, c.offset_seconds, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -35,6 +40,7 @@ test_parse_iso_date_date_only_zero_time :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, c.hour, 0)
|
testing.expect_value(t, c.hour, 0)
|
||||||
testing.expect_value(t, c.minute, 0)
|
testing.expect_value(t, c.minute, 0)
|
||||||
testing.expect_value(t, c.second, 0)
|
testing.expect_value(t, c.second, 0)
|
||||||
|
testing.expect_value(t, c.has_offset, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -49,6 +55,47 @@ test_parse_iso_date_too_short_errors :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, !ok, "input shorter than 10 chars should fail")
|
testing.expect(t, !ok, "input shorter than 10 chars should fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_offset_positive_colon :: proc(t: ^testing.T) {
|
||||||
|
c, ok := parse_iso_date("2026-03-15T08:49:54+07:00")
|
||||||
|
testing.expect(t, ok, "should parse")
|
||||||
|
testing.expect_value(t, c.has_offset, true)
|
||||||
|
testing.expect_value(t, c.offset_seconds, 25200)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_offset_positive_no_colon :: proc(t: ^testing.T) {
|
||||||
|
c, ok := parse_iso_date("2026-03-15T08:49:54+0530")
|
||||||
|
testing.expect(t, ok, "should parse")
|
||||||
|
testing.expect_value(t, c.has_offset, true)
|
||||||
|
testing.expect_value(t, c.offset_seconds, 19800)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_offset_hours_only :: proc(t: ^testing.T) {
|
||||||
|
c, ok := parse_iso_date("2026-03-15T08:49:54+05")
|
||||||
|
testing.expect(t, ok, "should parse")
|
||||||
|
testing.expect_value(t, c.has_offset, true)
|
||||||
|
testing.expect_value(t, c.offset_seconds, 18000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_offset_none_with_time :: proc(t: ^testing.T) {
|
||||||
|
c, ok := parse_iso_date("2026-03-15T08:49:54")
|
||||||
|
testing.expect(t, ok, "should parse")
|
||||||
|
testing.expect_value(t, c.has_offset, false)
|
||||||
|
testing.expect_value(t, c.offset_seconds, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_offset_skips_fractional_seconds :: proc(t: ^testing.T) {
|
||||||
|
c, ok := parse_iso_date("2026-03-15T08:49:54.123Z")
|
||||||
|
testing.expect(t, ok, "should parse")
|
||||||
|
testing.expect_value(t, c.has_offset, true)
|
||||||
|
testing.expect_value(t, c.offset_seconds, 0)
|
||||||
|
testing.expect_value(t, c.second, 54)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// format_date / match_token
|
// format_date / match_token
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -140,9 +187,9 @@ test_format_date_month_day_numeric_padding :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_format_date_mst_always_utc :: proc(t: ^testing.T) {
|
test_format_date_mst_defaults_utc :: proc(t: ^testing.T) {
|
||||||
// Date_Components carries no offset yet, so MST is a hardcoded
|
// Date_Components constructed directly (not via parse_iso_date)
|
||||||
// placeholder until real timezone support lands.
|
// defaults to UTC for the MST token.
|
||||||
dt := Date_Components{year = 2026, month = 1, day = 1, hour = 12}
|
dt := Date_Components{year = 2026, month = 1, day = 1, hour = 12}
|
||||||
result := format_date(dt, "MST")
|
result := format_date(dt, "MST")
|
||||||
testing.expect_value(t, result, "UTC")
|
testing.expect_value(t, result, "UTC")
|
||||||
|
|||||||
Reference in New Issue
Block a user