diff --git a/core/time/datetime/constants.odin b/core/time/datetime/constants.odin index 5b6c2d77c..8ae0565e0 100644 --- a/core/time/datetime/constants.odin +++ b/core/time/datetime/constants.odin @@ -2,23 +2,14 @@ package datetime // Ordinal 1 = Midnight Monday, January 1, 1 A.D. (Gregorian) // | Midnight Monday, January 3, 1 A.D. (Julian) -Ordinal :: int +Ordinal :: i64 EPOCH :: Ordinal(1) // Minimum and maximum dates and ordinals. Chosen for safe roundtripping. -when size_of(int) == 4 { - MIN_DATE :: Date{year = -5_879_608, month = 1, day = 1} - MAX_DATE :: Date{year = 5_879_608, month = 12, day = 31} - - MIN_ORD :: Ordinal(-2_147_483_090) - MAX_ORD :: Ordinal( 2_147_482_725) -} else { - MIN_DATE :: Date{year = -25_252_734_927_766_552, month = 1, day = 1} - MAX_DATE :: Date{year = 25_252_734_927_766_552, month = 12, day = 31} - - MIN_ORD :: Ordinal(-9_223_372_036_854_775_234) - MAX_ORD :: Ordinal( 9_223_372_036_854_774_869) -} +MIN_DATE :: Date{year = -25_252_734_927_766_552, month = 1, day = 1} +MAX_DATE :: Date{year = 25_252_734_927_766_552, month = 12, day = 31} +MIN_ORD :: Ordinal(-9_223_372_036_854_775_234) +MAX_ORD :: Ordinal( 9_223_372_036_854_774_869) Error :: enum { None, @@ -34,16 +25,16 @@ Error :: enum { } Date :: struct { - year: int, - month: int, - day: int, + year: i64, + month: i64, + day: i64, } Time :: struct { - hour: int, - minute: int, - second: int, - nano: int, + hour: i64, + minute: i64, + second: i64, + nano: i64, } DateTime :: struct { @@ -52,12 +43,12 @@ DateTime :: struct { } Delta :: struct { - days: int, - seconds: int, - nanos: int, + days: i64, // These are all i64 because we can also use it to add a number of seconds or nanos to a moment, + seconds: i64, // that are then normalized within their respective ranges. + nanos: i64, } -Month :: enum int { +Month :: enum i8 { January = 1, February, March, @@ -72,7 +63,7 @@ Month :: enum int { December, } -Weekday :: enum int { +Weekday :: enum i8 { Sunday = 0, Monday, Tuesday, @@ -83,4 +74,4 @@ Weekday :: enum int { } @(private) -MONTH_DAYS :: [?]int{-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} \ No newline at end of file +MONTH_DAYS :: [?]i8{-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} \ No newline at end of file diff --git a/core/time/datetime/datetime.odin b/core/time/datetime/datetime.odin index 9998e0a76..823aa50a6 100644 --- a/core/time/datetime/datetime.odin +++ b/core/time/datetime/datetime.odin @@ -8,27 +8,41 @@ package datetime import "base:intrinsics" // Procedures that return an Ordinal + date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal, err: Error) { validate(date) or_return return unsafe_date_to_ordinal(date), .None } -components_to_ordinal :: proc "contextless" (year, month, day: int) -> (ordinal: Ordinal, err: Error) { +components_to_ordinal :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (ordinal: Ordinal, err: Error) { return date_to_ordinal(Date{year, month, day}) } // Procedures that return a Date + ordinal_to_date :: proc "contextless" (ordinal: Ordinal) -> (date: Date, err: Error) { validate(ordinal) or_return return unsafe_ordinal_to_date(ordinal), .None } -components_to_date :: proc "contextless" (year, month, day: int) -> (date: Date, err: Error) { - date = Date{year, month, day} +components_to_date :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (date: Date, err: Error) { + date = Date{i64(year), i64(month), i64(day)} validate(date) or_return return date, .None } +components_to_time :: proc "contextless" (#any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (time: Time, err: Error) { + time = Time{i64(hour), i64(minute), i64(second), i64(nanos)} + validate(time) or_return + return time, .None +} + +components_to_datetime :: proc "contextless" (#any_int year, #any_int month, #any_int day, #any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (datetime: DateTime, err: Error) { + date := components_to_date(year, month, day) or_return + time := components_to_time(hour, minute, second, nanos) or_return + return {date, time}, .None +} + ordinal_to_datetime :: proc "contextless" (ordinal: Ordinal) -> (datetime: DateTime, err: Error) { d := ordinal_to_date(ordinal) or_return return {Date(d), {}}, .None @@ -67,7 +81,7 @@ subtract_deltas :: proc "contextless" (a, b: Delta) -> (delta: Delta, err: Error } sub :: proc{subtract_datetimes, subtract_dates, subtract_deltas} -add_days_to_date :: proc "contextless" (a: Date, days: int) -> (date: Date, err: Error) { +add_days_to_date :: proc "contextless" (a: Date, days: i64) -> (date: Date, err: Error) { ord := date_to_ordinal(a) or_return ord += days return ordinal_to_date(ord) @@ -91,7 +105,7 @@ add_delta_to_datetime :: proc "contextless" (a: DateTime, delta: Delta) -> (date datetime.date = ordinal_to_date(sum_delta.days) or_return - r: int + r: i64 datetime.hour, r = divmod(sum_delta.seconds, 3600) datetime.minute, datetime.second = divmod(r, 60) datetime.nano = sum_delta.nanos @@ -100,7 +114,7 @@ add_delta_to_datetime :: proc "contextless" (a: DateTime, delta: Delta) -> (date } add :: proc{add_days_to_date, add_delta_to_date, add_delta_to_datetime} -day_number :: proc "contextless" (date: Date) -> (day_number: int, err: Error) { +day_number :: proc "contextless" (date: Date) -> (day_number: i64, err: Error) { validate(date) or_return ord := unsafe_date_to_ordinal(date) @@ -108,39 +122,39 @@ day_number :: proc "contextless" (date: Date) -> (day_number: int, err: Error) { return } -days_remaining :: proc "contextless" (date: Date) -> (days_remaining: int, err: Error) { +days_remaining :: proc "contextless" (date: Date) -> (days_remaining: i64, err: Error) { // Alternative formulation `day_number` subtracted from 365 or 366 depending on leap year validate(date) or_return delta := sub(date, Date{date.year, 12, 31}) or_return return delta.days, .None } -last_day_of_month :: proc "contextless" (year, month: int) -> (day: int, err: Error) { +last_day_of_month :: proc "contextless" (year, month: i64) -> (day: i64, err: Error) { // Not using formula 2.27 from the book. This is far simpler and gives the same answer. validate(Date{year, month, 1}) or_return month_days := MONTH_DAYS - day = month_days[month] + day = i64(month_days[month]) if month == 2 && is_leap_year(year) { day += 1 } return } -new_year :: proc "contextless" (year: int) -> (new_year: Date, err: Error) { +new_year :: proc "contextless" (#any_int year: i64) -> (new_year: Date, err: Error) { new_year = {year, 1, 1} validate(new_year) or_return return } -year_end :: proc "contextless" (year: int) -> (year_end: Date, err: Error) { +year_end :: proc "contextless" (#any_int year: i64) -> (year_end: Date, err: Error) { year_end = {year, 12, 31} validate(year_end) or_return return } -year_range :: proc (year: int, allocator := context.allocator) -> (range: []Date) { +year_range :: proc (#any_int year: i64, allocator := context.allocator) -> (range: []Date) { is_leap := is_leap_year(year) days := 366 if is_leap else 365 @@ -154,7 +168,7 @@ year_range :: proc (year: int, allocator := context.allocator) -> (range: []Date i := 0 for month in 1..=len(month_days) { for day in 1..=month_days[month] { - range[i] = Date{year, month, day} + range[i], _ = components_to_date(year, month, day) i += 1 } } @@ -167,7 +181,7 @@ normalize_delta :: proc "contextless" (delta: Delta) -> (normalized: Delta, err: // Add original seconds to rolled over seconds. seconds += delta.seconds - days: int + days: i64 // Distribute seconds into number of days and remaining seconds. days, seconds = divmod(seconds, 24 * 3600) @@ -213,7 +227,7 @@ unsafe_date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal) return } -unsafe_ordinal_to_year :: proc "contextless" (ordinal: Ordinal) -> (year: int, day_ordinal: int) { +unsafe_ordinal_to_year :: proc "contextless" (ordinal: Ordinal) -> (year: i64, day_ordinal: i64) { // Days after epoch d0 := ordinal - EPOCH diff --git a/core/time/datetime/internal.odin b/core/time/datetime/internal.odin index 8a5efdb37..45c2b99ab 100644 --- a/core/time/datetime/internal.odin +++ b/core/time/datetime/internal.odin @@ -4,7 +4,7 @@ package datetime import "base:intrinsics" -sign :: proc "contextless" (v: int) -> (res: int) { +sign :: proc "contextless" (v: i64) -> (res: i64) { if v == 0 { return 0 } else if v > 0 { @@ -37,7 +37,7 @@ floor_div :: proc "contextless" (x, y: $T) -> (res: T) } // Half open: x mod [1..b] -interval_mod :: proc "contextless" (x, a, b: int) -> (res: int) { +interval_mod :: proc "contextless" (x, a, b: i64) -> (res: i64) { if a == b { return x } @@ -45,12 +45,12 @@ interval_mod :: proc "contextless" (x, a, b: int) -> (res: int) { } // x mod [1..b] -adjusted_remainder :: proc "contextless" (x, b: int) -> (res: int) { +adjusted_remainder :: proc "contextless" (x, b: i64) -> (res: i64) { m := x %% b return b if m == 0 else m } -gcd :: proc "contextless" (x, y: int) -> (res: int) { +gcd :: proc "contextless" (x, y: i64) -> (res: i64) { if y == 0 { return x } @@ -59,18 +59,18 @@ gcd :: proc "contextless" (x, y: int) -> (res: int) { return gcd(y, m) } -lcm :: proc "contextless" (x, y: int) -> (res: int) { +lcm :: proc "contextless" (x, y: i64) -> (res: i64) { return x * y / gcd(x, y) } -sum :: proc "contextless" (i: int, f: proc "contextless" (n: int) -> int, cond: proc "contextless" (n: int) -> bool) -> (res: int) { +sum :: proc "contextless" (i: i64, f: proc "contextless" (n: i64) -> i64, cond: proc "contextless" (n: i64) -> bool) -> (res: i64) { for idx := i; cond(idx); idx += 1 { res += f(idx) } return } -product :: proc "contextless" (i: int, f: proc "contextless" (n: int) -> int, cond: proc "contextless" (n: int) -> bool) -> (res: int) { +product :: proc "contextless" (i: i64, f: proc "contextless" (n: i64) -> i64, cond: proc "contextless" (n: i64) -> bool) -> (res: i64) { res = 1 for idx := i; cond(idx); idx += 1 { res *= f(idx) @@ -78,7 +78,7 @@ product :: proc "contextless" (i: int, f: proc "contextless" (n: int) -> int, co return } -smallest :: proc "contextless" (k: int, cond: proc "contextless" (n: int) -> bool) -> (d: int) { +smallest :: proc "contextless" (k: i64, cond: proc "contextless" (n: i64) -> bool) -> (d: i64) { k := k for !cond(k) { k += 1 @@ -86,7 +86,7 @@ smallest :: proc "contextless" (k: int, cond: proc "contextless" (n: int) -> boo return k } -biggest :: proc "contextless" (k: int, cond: proc "contextless" (n: int) -> bool) -> (d: int) { +biggest :: proc "contextless" (k: i64, cond: proc "contextless" (n: i64) -> bool) -> (d: i64) { k := k for !cond(k) { k -= 1 diff --git a/core/time/datetime/validation.odin b/core/time/datetime/validation.odin index 0bf2a2a25..38176269b 100644 --- a/core/time/datetime/validation.odin +++ b/core/time/datetime/validation.odin @@ -1,7 +1,7 @@ package datetime // Validation helpers -is_leap_year :: proc "contextless" (year: int) -> (leap: bool) { +is_leap_year :: proc "contextless" (#any_int year: i64) -> (leap: bool) { return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } @@ -9,7 +9,7 @@ validate_date :: proc "contextless" (date: Date) -> (err: Error) { return validate(date.year, date.month, date.day) } -validate_year_month_day :: proc "contextless" (year, month, day: int) -> (err: Error) { +validate_year_month_day :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (err: Error) { if year < MIN_DATE.year || year > MAX_DATE.year { return .Invalid_Year } @@ -23,7 +23,7 @@ validate_year_month_day :: proc "contextless" (year, month, day: int) -> (err: E days_this_month = 29 } - if day < 1 || day > days_this_month { + if day < 1 || day > i64(days_this_month) { return .Invalid_Day } return .None diff --git a/core/time/rfc3339.odin b/core/time/rfc3339.odin index 5a3ac77c3..30c255c79 100644 --- a/core/time/rfc3339.odin +++ b/core/time/rfc3339.odin @@ -22,17 +22,13 @@ rfc3339_to_time_utc :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: // Optional pointer to boolean `is_leap` will return `true` if the moment was a leap second. // Leap seconds are smeared into 23:59:59. rfc3339_to_time_and_offset :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, utc_offset: int, consumed: int) { - moment, offset, count := rfc3339_to_components(rfc_datetime) + moment, offset, leap_second, count := rfc3339_to_components(rfc_datetime) if count == 0 { return } - // Leap second handling - if moment.minute == 59 && moment.second == 60 { - moment.second = 59 - if is_leap != nil { - is_leap^ = true - } + if is_leap != nil { + is_leap^ = leap_second } if _res, ok := datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, moment.nano); !ok { @@ -45,40 +41,48 @@ rfc3339_to_time_and_offset :: proc(rfc_datetime: string, is_leap: ^bool = nil) - // Parses an RFC 3339 string and returns Time and a UTC offset in minutes. // e.g. 1985-04-12T23:20:50.52Z // Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given -rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, consumed: int) { - count: int - moment, offset, ok := _rfc3339_to_components(rfc_datetime, &count) +rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, is_leap: bool, consumed: int) { + moment, offset, count, leap_second, ok := _rfc3339_to_components(rfc_datetime) if !ok { return } - return moment, offset, count + return moment, offset, leap_second, count } // Parses an RFC 3339 string and returns datetime.DateTime. // Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given @(private) -_rfc3339_to_components :: proc(rfc_datetime: string, consume_count: ^int = nil) -> (res: dt.DateTime, utc_offset: int, ok: bool) { +_rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, consumed: int, is_leap: bool, ok: bool) { // A compliant date is at minimum 20 characters long, e.g. YYYY-MM-DDThh:mm:ssZ (len(rfc_datetime) >= 20) or_return - // Scan and eat YYYY-MM-DD[Tt] - res.year = scan_digits(rfc_datetime[0:], "-", 4) or_return - res.month = scan_digits(rfc_datetime[5:], "-", 2) or_return - res.day = scan_digits(rfc_datetime[8:], "Tt", 2) or_return - - // Scan and eat HH:MM:SS, leave separator - res.hour = scan_digits(rfc_datetime[11:], ":", 2) or_return - res.minute = scan_digits(rfc_datetime[14:], ":", 2) or_return - res.second = scan_digits(rfc_datetime[17:], "", 2) or_return - count := 19 + // Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator + year := scan_digits(rfc_datetime[0:], "-", 4) or_return + month := scan_digits(rfc_datetime[5:], "-", 2) or_return + day := scan_digits(rfc_datetime[8:], "Tt", 2) or_return + hour := scan_digits(rfc_datetime[11:], ":", 2) or_return + minute := scan_digits(rfc_datetime[14:], ":", 2) or_return + second := scan_digits(rfc_datetime[17:], "", 2) or_return + nanos := 0 + count := 19 if rfc_datetime[count] == '.' { // Scan hundredths. The string must be at least 4 bytes long (.hhZ) (len(rfc_datetime[count:]) >= 4) or_return hundredths := scan_digits(rfc_datetime[count+1:], "", 2) or_return count += 3 + nanos = 10_000_000 * hundredths + } - res.nano = 10_000_000 * hundredths + // Leap second handling + if minute == 59 && second == 60 { + second = 59 + is_leap = true + } + + err: dt.Error + if res, err = dt.components_to_datetime(year, month, day, hour, minute, second, nanos); err != .None { + return {}, 0, 0, false, false } // Scan UTC offset @@ -95,11 +99,7 @@ _rfc3339_to_components :: proc(rfc_datetime: string, consume_count: ^int = nil) utc_offset *= -1 if rfc_datetime[count] == '-' else 1 count += 6 } - - if consume_count != nil { - consume_count^ = count - } - return res, utc_offset, true + return res, utc_offset, count, is_leap, true } @(private) diff --git a/core/time/time.odin b/core/time/time.odin index 6716be35c..10b71ee0d 100644 --- a/core/time/time.odin +++ b/core/time/time.odin @@ -357,8 +357,11 @@ _abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Mon return } -components_to_time :: proc "contextless" (year, month, day, hour, minute, second: int, nsec := int(0)) -> (t: Time, ok: bool) { - this_date := dt.DateTime{date={year, month, day}, time={hour, minute, second, nsec}} +components_to_time :: proc "contextless" (#any_int year, #any_int month, #any_int day, #any_int hour, #any_int minute, #any_int second: i64, #any_int nsec := i64(0)) -> (t: Time, ok: bool) { + this_date, err := dt.components_to_datetime(year, month, day, hour, minute, second, nsec) + if err != .None { + return + } return compound_to_time(this_date) } diff --git a/tests/core/time/test_core_time.odin b/tests/core/time/test_core_time.odin index 2d13ee326..0e324ffaf 100644 --- a/tests/core/time/test_core_time.odin +++ b/tests/core/time/test_core_time.odin @@ -155,7 +155,8 @@ test_component_to_time_roundtrip :: proc(t: ^testing.T) { days += 1 } for day in 1..=days { - date_component_roundtrip_test(t, {{year, month, day}, {0, 0, 0, 0}}) + d, _ := dt.components_to_datetime(year, month, day, 0, 0, 0, 0) + date_component_roundtrip_test(t, d) } } } @@ -171,7 +172,7 @@ date_component_roundtrip_test :: proc(t: ^testing.T, moment: dt.DateTime) { expected := fmt.tprintf("Expected %4d-%2d-%2d %2d:%2d:%2d, got %4d-%2d-%2d %2d:%2d:%2d", moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, YYYY, MM, DD, hh, mm, ss) - ok = moment.year == YYYY && moment.month == int(MM) && moment.day == DD - ok &= moment.hour == hh && moment.minute == mm && moment.second == ss + ok = moment.year == i64(YYYY) && moment.month == i64(MM) && moment.day == i64(DD) + ok &= moment.hour == i64(hh) && moment.minute == i64(mm) && moment.second == i64(ss) expect(t, ok, expected) } \ No newline at end of file