Add WiP datetime package and tests.

A new package `core:time/datetime` has been added which can represent moments much further in the past and future than `core:time`.
It is based on *the* reference work on the subject, Calendrical Calculations Ultimate Edition, Reingold & Dershowitz.

More procedures will be added to it in the future, to for example calculate the 3rd Thursday in March to figure out holidays.
The package has been tested for more than a year and can handle dates 25 quadrillion years into the past and future with 64-bit day ordinals, or 5 million with 32-bit ones.

This also fixes a longstanding bug where converting between YYYY-MM:DD hh:mm:ss and `time.Time` and back could result in a mismatch.

RFC 3339 timestamps can now also be parsed using the `core:time` package.
This commit is contained in:
Jeroen van Rijn
2024-03-13 19:04:39 +01:00
parent 009b6f44e3
commit 72c15d7699
9 changed files with 845 additions and 55 deletions
+86
View File
@@ -0,0 +1,86 @@
package datetime
// Ordinal 1 = Midnight Monday, January 1, 1 A.D. (Gregorian)
// | Midnight Monday, January 3, 1 A.D. (Julian)
Ordinal :: int
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)
}
Error :: enum {
None,
Invalid_Year,
Invalid_Month,
Invalid_Day,
Invalid_Hour,
Invalid_Minute,
Invalid_Second,
Invalid_Nano,
Invalid_Ordinal,
Invalid_Delta,
}
Date :: struct {
year: int,
month: int,
day: int,
}
Time :: struct {
hour: int,
minute: int,
second: int,
nano: int,
}
DateTime :: struct {
using date: Date,
using time: Time,
}
Delta :: struct {
days: int,
seconds: int,
nanos: int,
}
Month :: enum int {
January = 1,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December,
}
Weekday :: enum int {
Sunday = 0,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}
@(private)
MONTH_DAYS :: [?]int{-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+262
View File
@@ -0,0 +1,262 @@
/*
Calendrical conversions using a proleptic Gregorian calendar.
Implemented using formulas from: Calendrical Calculations Ultimate Edition, Reingold & Dershowitz
*/
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) {
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}
validate(date) or_return
return date, .None
}
ordinal_to_datetime :: proc "contextless" (ordinal: Ordinal) -> (datetime: DateTime, err: Error) {
d := ordinal_to_date(ordinal) or_return
return {Date(d), {}}, .None
}
day_of_week :: proc "contextless" (ordinal: Ordinal) -> (day: Weekday) {
return Weekday((ordinal - EPOCH) %% 7)
}
subtract_dates :: proc "contextless" (a, b: Date) -> (delta: Delta, err: Error) {
ord_a := date_to_ordinal(a) or_return
ord_b := date_to_ordinal(b) or_return
delta = Delta{days=ord_a - ord_b}
return
}
subtract_datetimes :: proc "contextless" (a, b: DateTime) -> (delta: Delta, err: Error) {
ord_a := date_to_ordinal(a) or_return
ord_b := date_to_ordinal(b) or_return
validate(a.time) or_return
validate(b.time) or_return
seconds_a := a.hour * 3600 + a.minute * 60 + a.second
seconds_b := b.hour * 3600 + b.minute * 60 + b.second
delta = Delta{ord_a - ord_b, seconds_a - seconds_b, a.nano - b.nano}
return
}
subtract_deltas :: proc "contextless" (a, b: Delta) -> (delta: Delta, err: Error) {
delta = Delta{a.days - b.days, a.seconds - b.seconds, a.nanos - b.nanos}
delta = normalize_delta(delta) or_return
return
}
sub :: proc{subtract_datetimes, subtract_dates, subtract_deltas}
add_days_to_date :: proc "contextless" (a: Date, days: int) -> (date: Date, err: Error) {
ord := date_to_ordinal(a) or_return
ord += days
return ordinal_to_date(ord)
}
add_delta_to_date :: proc "contextless" (a: Date, delta: Delta) -> (date: Date, err: Error) {
ord := date_to_ordinal(a) or_return
// Because the input is a Date, we add only the days from the Delta.
ord += delta.days
return ordinal_to_date(ord)
}
add_delta_to_datetime :: proc "contextless" (a: DateTime, delta: Delta) -> (datetime: DateTime, err: Error) {
days := date_to_ordinal(a) or_return
a_seconds := a.hour * 3600 + a.minute * 60 + a.second
a_delta := Delta{days=days, seconds=a_seconds, nanos=a.nano}
sum_delta := Delta{days=a_delta.days + delta.days, seconds=a_delta.seconds + delta.seconds, nanos=a_delta.nanos + delta.nanos}
sum_delta = normalize_delta(sum_delta) or_return
datetime.date = ordinal_to_date(sum_delta.days) or_return
r: int
datetime.hour, r = divmod(sum_delta.seconds, 3600)
datetime.minute, datetime.second = divmod(r, 60)
datetime.nano = sum_delta.nanos
return
}
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) {
validate(date) or_return
ord := unsafe_date_to_ordinal(date)
_, day_number = unsafe_ordinal_to_year(ord)
return
}
days_remaining :: proc "contextless" (date: Date) -> (days_remaining: int, 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) {
// 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]
if month == 2 && is_leap_year(year) {
day += 1
}
return
}
new_year :: proc "contextless" (year: int) -> (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 = {year, 12, 31}
validate(year_end) or_return
return
}
year_range :: proc (year: int, allocator := context.allocator) -> (range: []Date) {
is_leap := is_leap_year(year)
days := 366 if is_leap else 365
range = make([]Date, days, allocator)
month_days := MONTH_DAYS
if is_leap {
month_days[2] = 29
}
i := 0
for month in 1..=len(month_days) {
for day in 1..=month_days[month] {
range[i] = Date{year, month, day}
i += 1
}
}
return
}
normalize_delta :: proc "contextless" (delta: Delta) -> (normalized: Delta, err: Error) {
// Distribute nanos into seconds and remainder
seconds, nanos := divmod(delta.nanos, 1e9)
// Add original seconds to rolled over seconds.
seconds += delta.seconds
days: int
// Distribute seconds into number of days and remaining seconds.
days, seconds = divmod(seconds, 24 * 3600)
// Add original days
days += delta.days
if days <= MIN_ORD || days >= MAX_ORD {
return {}, .Invalid_Delta
}
return Delta{days, seconds, nanos}, .None
}
// The following procedures don't check whether their inputs are in a valid range.
// They're still exported for those who know their inputs have been validated.
unsafe_date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal) {
year_minus_one := date.year - 1
// Day before epoch
ordinal = EPOCH - 1
// Add non-leap days
ordinal += 365 * year_minus_one
// Add leap days
ordinal += floor_div(year_minus_one, 4) // Julian-rule leap days
ordinal -= floor_div(year_minus_one, 100) // Prior century years
ordinal += floor_div(year_minus_one, 400) // Prior 400-multiple years
ordinal += floor_div(367 * date.month - 362, 12) // Prior days this year
// Apply correction
if date.month <= 2 {
ordinal += 0
} else if is_leap_year(date.year) {
ordinal -= 1
} else {
ordinal -= 2
}
// Add days
ordinal += date.day
return
}
unsafe_ordinal_to_year :: proc "contextless" (ordinal: Ordinal) -> (year: int, day_ordinal: int) {
// Days after epoch
d0 := ordinal - EPOCH
// Number of 400-year cycles and remainder
n400, d1 := divmod(d0, 146097)
// Number of 100-year cycles and remainder
n100, d2 := divmod(d1, 36524)
// Number of 4-year cycles and remainder
n4, d3 := divmod(d2, 1461)
// Number of remaining days
n1, d4 := divmod(d3, 365)
year = 400 * n400 + 100 * n100 + 4 * n4 + n1
if n1 != 4 && n100 != 4 {
day_ordinal = d4 + 1
} else {
day_ordinal = 366
}
if n100 == 4 || n1 == 4 {
return year, day_ordinal
}
return year + 1, day_ordinal
}
unsafe_ordinal_to_date :: proc "contextless" (ordinal: Ordinal) -> (date: Date) {
year, _ := unsafe_ordinal_to_year(ordinal)
prior_days := ordinal - unsafe_date_to_ordinal(Date{year, 1, 1})
correction := Ordinal(2)
if ordinal < unsafe_date_to_ordinal(Date{year, 3, 1}) {
correction = 0
} else if is_leap_year(year) {
correction = 1
}
month := floor_div((12 * (prior_days + correction) + 373), 367)
day := ordinal - unsafe_date_to_ordinal(Date{year, month, 1}) + 1
return {year, month, day}
}
+95
View File
@@ -0,0 +1,95 @@
package datetime
// Internal helper functions for calendrical conversions
import "base:intrinsics"
sign :: proc "contextless" (v: int) -> (res: int) {
if v == 0 {
return 0
} else if v > 0 {
return 1
}
return -1
}
// Caller has to ensure y != 0
divmod :: proc "contextless" (x, y: $T, loc := #caller_location) -> (a: T, r: T)
where intrinsics.type_is_integer(T) {
a = x / y
r = x % y
if (r > 0 && y < 0) || (r < 0 && y > 0) {
a -= 1
r += y
}
return a, r
}
// Divides and floors
floor_div :: proc "contextless" (x, y: $T) -> (res: T)
where intrinsics.type_is_integer(T) {
res = x / y
r := x % y
if (r > 0 && y < 0) || (r < 0 && y > 0) {
res -= 1
}
return res
}
// Half open: x mod [1..b]
interval_mod :: proc "contextless" (x, a, b: int) -> (res: int) {
if a == b {
return x
}
return a + ((x - a) %% (b - a))
}
// x mod [1..b]
adjusted_remainder :: proc "contextless" (x, b: int) -> (res: int) {
m := x %% b
return b if m == 0 else m
}
gcd :: proc "contextless" (x, y: int) -> (res: int) {
if y == 0 {
return x
}
m := x %% y
return gcd(y, m)
}
lcm :: proc "contextless" (x, y: int) -> (res: int) {
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) {
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) {
res = 1
for idx := i; cond(idx); idx += 1 {
res *= f(idx)
}
return
}
smallest :: proc "contextless" (k: int, cond: proc "contextless" (n: int) -> bool) -> (d: int) {
k := k
for !cond(k) {
k += 1
}
return k
}
biggest :: proc "contextless" (k: int, cond: proc "contextless" (n: int) -> bool) -> (d: int) {
k := k
for !cond(k) {
k -= 1
}
return k
}
+67
View File
@@ -0,0 +1,67 @@
package datetime
// Validation helpers
is_leap_year :: proc "contextless" (year: int) -> (leap: bool) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
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) {
if year < MIN_DATE.year || year > MAX_DATE.year {
return .Invalid_Year
}
if month < 1 || month > 12 {
return .Invalid_Month
}
month_days := MONTH_DAYS
days_this_month := month_days[month]
if month == 2 && is_leap_year(year) {
days_this_month = 29
}
if day < 1 || day > days_this_month {
return .Invalid_Day
}
return .None
}
validate_ordinal :: proc "contextless" (ordinal: Ordinal) -> (err: Error) {
if ordinal < MIN_ORD || ordinal > MAX_ORD {
return .Invalid_Ordinal
}
return
}
validate_time :: proc "contextless" (time: Time) -> (err: Error) {
if time.hour < 0 || time.hour > 23 {
return .Invalid_Hour
}
if time.minute < 0 || time.minute > 59 {
return .Invalid_Minute
}
if time.second < 0 || time.second > 59 {
return .Invalid_Second
}
if time.nano < 0 || time.nano > 1e9 {
return .Invalid_Nano
}
return .None
}
validate_datetime :: proc "contextless" (using datetime: DateTime) -> (err: Error) {
validate(date) or_return
validate(time) or_return
return .None
}
validate :: proc{
validate_date,
validate_year_month_day,
validate_ordinal,
validate_time,
validate_datetime,
}
+122
View File
@@ -0,0 +1,122 @@
package time
// Parsing RFC 3339 date/time strings into time.Time.
// See https://www.rfc-editor.org/rfc/rfc3339 for the definition
import dt "core:time/datetime"
// Parses an RFC 3339 string and returns Time in UTC, with any UTC offset applied to it.
// Only 4-digit years are accepted.
// 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_utc :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, consumed: int) {
offset: int
res, offset, consumed = rfc3339_to_time_and_offset(rfc_datetime, is_leap)
res._nsec += (i64(-offset) * i64(Minute))
return res, consumed
}
// Parses an RFC 3339 string and returns Time and a UTC offset in minutes.
// e.g. 1985-04-12T23:20:50.52Z
// Note: Only 4-digit years are accepted.
// 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)
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 _res, ok := datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, moment.nano); !ok {
return {}, 0, 0
} else {
return _res, offset, count
}
}
// 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)
if !ok {
return
}
return moment, offset, 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) {
// 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
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
res.nano = 10_000_000 * hundredths
}
// Scan UTC offset
switch rfc_datetime[count] {
case 'Z':
utc_offset = 0
count += 1
case '+', '-':
(len(rfc_datetime[count:]) >= 6) or_return
offset_hour := scan_digits(rfc_datetime[count+1:], ":", 2) or_return
offset_minute := scan_digits(rfc_datetime[count+4:], "", 2) or_return
utc_offset = 60 * offset_hour + offset_minute
utc_offset *= -1 if rfc_datetime[count] == '-' else 1
count += 6
}
if consume_count != nil {
consume_count^ = count
}
return res, utc_offset, true
}
@(private)
scan_digits :: proc(s: string, sep: string, count: int) -> (res: int, ok: bool) {
needed := count + min(1, len(sep))
(len(s) >= needed) or_return
#no_bounds_check for i in 0..<count {
if v := s[i]; v >= '0' && v <= '9' {
res = res * 10 + int(v - '0')
} else {
return 0, false
}
}
found_sep := len(sep) == 0
#no_bounds_check for v in sep {
found_sep |= rune(s[count]) == v
}
return res, found_sep
}
+26 -54
View File
@@ -1,6 +1,7 @@
package time
import "base:intrinsics"
import "base:intrinsics"
import dt "core:time/datetime"
Duration :: distinct i64
@@ -299,10 +300,6 @@ _time_abs :: proc "contextless" (t: Time) -> u64 {
@(private)
_abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Month, day: int, yday: int) {
_is_leap_year :: proc "contextless" (year: int) -> bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
d := abs / SECONDS_PER_DAY
// 400 year cycles
@@ -335,7 +332,7 @@ _abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Mon
day = yday
if _is_leap_year(year) {
if is_leap_year(year) {
switch {
case day > 31+29-1:
day -= 1
@@ -360,57 +357,32 @@ _abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Mon
return
}
datetime_to_time :: proc "contextless" (year, month, day, hour, minute, second: int, nsec := int(0)) -> (t: Time, ok: bool) {
divmod :: proc "contextless" (year: int, divisor: int) -> (div: int, mod: int) {
if divisor <= 0 {
intrinsics.debug_trap()
}
div = int(year / divisor)
mod = year % divisor
return
}
_is_leap_year :: proc "contextless" (year: int) -> bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
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}}
return compound_to_time(this_date)
}
compound_to_time :: proc "contextless" (datetime: dt.DateTime) -> (t: Time, ok: bool) {
unix_epoch := dt.DateTime{{1970, 1, 1}, {0, 0, 0, 0}}
delta, err := dt.sub(datetime, unix_epoch)
ok = err == .None
seconds := delta.days * 86_400 + delta.seconds
nanoseconds := i128(seconds) * 1e9 + i128(delta.nanos)
// Can this moment be represented in i64 worth of nanoseconds?
// min(Time): 1677-09-21 00:12:44.145224192 +0000 UTC
// max(Time): 2262-04-11 23:47:16.854775807 +0000 UTC
if nanoseconds < i128(min(i64)) || nanoseconds > i128(max(i64)) {
return {}, false
}
return Time{_nsec=i64(nanoseconds)}, true
}
datetime_to_time :: proc{components_to_time, compound_to_time}
ok = true
_y := year - 1970
_m := month - 1
_d := day - 1
if month < 1 || month > 12 {
_m %= 12; ok = false
}
if day < 1 || day > 31 {
_d %= 31; ok = false
}
s := i64(0)
div, mod := divmod(_y, 400)
days := div * DAYS_PER_400_YEARS
div, mod = divmod(mod, 100)
days += div * DAYS_PER_100_YEARS
div, mod = divmod(mod, 4)
days += (div * DAYS_PER_4_YEARS) + (mod * 365)
days += int(days_before[_m]) + _d
if _is_leap_year(year) && _m >= 2 {
days += 1
}
s += i64(days) * SECONDS_PER_DAY
s += i64(hour) * SECONDS_PER_HOUR
s += i64(minute) * SECONDS_PER_MINUTE
s += i64(second)
t._nsec = (s * 1e9) + i64(nsec)
return
is_leap_year :: proc "contextless" (year: int) -> (leap: bool) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
days_before := [?]i32{
+5 -1
View File
@@ -24,7 +24,8 @@ all: c_libc_test \
slice_test \
strings_test \
thread_test \
runtime_test
runtime_test \
time_test
download_test_assets:
$(PYTHON) download_assets.py
@@ -94,3 +95,6 @@ thread_test:
runtime_test:
$(ODIN) run runtime $(COMMON) -out:test_core_runtime
time_test:
$(ODIN) run time $(COMMON) -out:test_core_time
+5
View File
@@ -100,3 +100,8 @@ echo ---
echo Running core:runtime tests
echo ---
%PATH_TO_ODIN% run runtime %COMMON% %COLLECTION% -out:test_core_runtime.exe || exit /b
echo ---
echo Running core:runtime tests
echo ---
%PATH_TO_ODIN% run time %COMMON% %COLLECTION% -out:test_core_time.exe || exit /b
+177
View File
@@ -0,0 +1,177 @@
package test_core_time
import "core:fmt"
import "core:mem"
import "core:os"
import "core:testing"
import "core:time"
import dt "core:time/datetime"
is_leap_year :: time.is_leap_year
TEST_count := 0
TEST_fail := 0
when ODIN_TEST {
expect :: testing.expect
expect_value :: testing.expect_value
log :: testing.log
} else {
expect :: proc(t: ^testing.T, condition: bool, message: string, loc := #caller_location) {
TEST_count += 1
if !condition {
TEST_fail += 1
fmt.printf("[%v] %v\n", loc, message)
return
}
}
log :: proc(t: ^testing.T, v: any, loc := #caller_location) {
fmt.printf("[%v] ", loc)
fmt.printf("log: %v\n", v)
}
}
main :: proc() {
t := testing.T{}
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
context.allocator = mem.tracking_allocator(&track)
test_ordinal_date_roundtrip(&t)
test_component_to_time_roundtrip(&t)
test_parse_rfc3339_string(&t)
for _, leak in track.allocation_map {
expect(&t, false, fmt.tprintf("%v leaked %m\n", leak.location, leak.size))
}
for bad_free in track.bad_free_array {
expect(&t, false, fmt.tprintf("%v allocation %p was freed badly\n", bad_free.location, bad_free.memory))
}
fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count)
if TEST_fail > 0 {
os.exit(1)
}
}
@test
test_ordinal_date_roundtrip :: proc(t: ^testing.T) {
expect(t, dt.unsafe_ordinal_to_date(dt.unsafe_date_to_ordinal(dt.MIN_DATE)) == dt.MIN_DATE, "Roundtripping MIN_DATE failed.")
expect(t, dt.unsafe_date_to_ordinal(dt.unsafe_ordinal_to_date(dt.MIN_ORD)) == dt.MIN_ORD, "Roundtripping MIN_ORD failed.")
expect(t, dt.unsafe_ordinal_to_date(dt.unsafe_date_to_ordinal(dt.MAX_DATE)) == dt.MAX_DATE, "Roundtripping MAX_DATE failed.")
expect(t, dt.unsafe_date_to_ordinal(dt.unsafe_ordinal_to_date(dt.MAX_ORD)) == dt.MAX_ORD, "Roundtripping MAX_ORD failed.")
}
/*
1990-12-31T23:59:60Z
This represents the leap second inserted at the end of 1990.
1990-12-31T15:59:60-08:00
This represents the same leap second in Pacific Standard Time, 8 hours behind UTC.
1937-01-01T12:00:27.87+00:20
This represents the same instant of time as noon, January 1, 1937, Netherlands time.
Standard time in the Netherlands was exactly 19 minutes and 32.13 seconds ahead of UTC by law from 1909-05-01 through 1937-06-30.
This time zone cannot be represented exactly using the HH:MM format, and this timestamp uses the closest representable UTC offset.
*/
RFC3339_Test :: struct{
rfc_3339: string,
datetime: time.Time,
apply_offset: bool,
utc_offset: int,
consumed: int,
is_leap: bool,
}
// These are based on RFC 3339's examples, see https://www.rfc-editor.org/rfc/rfc3339#page-10
rfc3339_tests :: []RFC3339_Test{
// This represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC.
{"1985-04-12T23:20:50.52Z", {482196050520000000}, true, 0, 23, false},
// This represents 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).
// Note that this is equivalent to 1996-12-20T00:39:57Z in UTC.
{"1996-12-19T16:39:57-08:00", {851013597000000000}, false, -480, 25, false},
{"1996-12-19T16:39:57-08:00", {851042397000000000}, true, 0, 25, false},
{"1996-12-20T00:39:57Z", {851042397000000000}, false, 0, 20, false},
// This represents the leap second inserted at the end of 1990.
// It'll be represented as 1990-12-31 23:59:59 UTC after parsing, and `is_leap` will be set to `true`.
{"1990-12-31T23:59:60Z", {662687999000000000}, true, 0, 20, true},
// This represents the same leap second in Pacific Standard Time, 8 hours behind UTC.
{"1990-12-31T15:59:60-08:00", {662687999000000000}, true, 0, 25, true},
// This represents the same instant of time as noon, January 1, 1937, Netherlands time.
// Standard time in the Netherlands was exactly 19 minutes and 32.13 seconds ahead of UTC by law
// from 1909-05-01 through 1937-06-30. This time zone cannot be represented exactly using the
// HH:MM format, and this timestamp uses the closest representable UTC offset.
{"1937-01-01T12:00:27.87+00:20", {-1041335972130000000}, false, 20, 28, false},
{"1937-01-01T12:00:27.87+00:20", {-1041337172130000000}, true, 0, 28, false},
}
@test
test_parse_rfc3339_string :: proc(t: ^testing.T) {
for test in rfc3339_tests {
is_leap := false
if test.apply_offset {
res, consumed := time.rfc3339_to_time_utc(test.rfc_3339, &is_leap)
msg := fmt.tprintf("[apply offet] Parsing failed: %v -> %v (nsec: %v). Expected %v consumed, got %v", test.rfc_3339, res, res._nsec, test.consumed, consumed)
expect(t, test.consumed == consumed, msg)
if test.consumed == consumed {
expect(t, test.datetime == res, fmt.tprintf("Time didn't match. Expected %v (%v), got %v (%v)", test.datetime, test.datetime._nsec, res, res._nsec))
expect(t, test.is_leap == is_leap, "Expected a leap second, got none.")
}
} else {
res, offset, consumed := time.rfc3339_to_time_and_offset(test.rfc_3339)
msg := fmt.tprintf("Parsing failed: %v -> %v (nsec: %v), offset: %v. Expected %v consumed, got %v", test.rfc_3339, res, res._nsec, offset, test.consumed, consumed)
expect(t, test.consumed == consumed, msg)
if test.consumed == consumed {
expect(t, test.datetime == res, fmt.tprintf("Time didn't match. Expected %v (%v), got %v (%v)", test.datetime, test.datetime._nsec, res, res._nsec))
expect(t, test.utc_offset == offset, fmt.tprintf("UTC offset didn't match. Expected %v, got %v", test.utc_offset, offset))
expect(t, test.is_leap == is_leap, "Expected a leap second, got none.")
}
}
}
}
MONTH_DAYS := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
YEAR_START :: 1900
YEAR_END :: 2024
@test
test_component_to_time_roundtrip :: proc(t: ^testing.T) {
// Roundtrip a datetime through `datetime_to_time` to `Time` and back to its components.
for year in YEAR_START..=YEAR_END {
for month in 1..=12 {
days := MONTH_DAYS[month - 1]
if month == 2 && is_leap_year(year) {
days += 1
}
for day in 1..=days {
date_component_roundtrip_test(t, {{year, month, day}, {0, 0, 0, 0}})
}
}
}
}
date_component_roundtrip_test :: proc(t: ^testing.T, moment: dt.DateTime) {
res, ok := time.datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second)
expect(t, ok, "Couldn't convert date components into date")
YYYY, MM, DD := time.date(res)
hh, mm, ss := time.clock(res)
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
expect(t, ok, expected)
}