Files
Odin/core/time/time.odin
T
Jeroen van Rijn 72c15d7699 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.
2024-03-18 16:47:16 +01:00

412 lines
9.8 KiB
Odin

package time
import "base:intrinsics"
import dt "core:time/datetime"
Duration :: distinct i64
Nanosecond :: Duration(1)
Microsecond :: 1000 * Nanosecond
Millisecond :: 1000 * Microsecond
Second :: 1000 * Millisecond
Minute :: 60 * Second
Hour :: 60 * Minute
MIN_DURATION :: Duration(-1 << 63)
MAX_DURATION :: Duration(1<<63 - 1)
IS_SUPPORTED :: _IS_SUPPORTED
Time :: struct {
_nsec: i64, // Measured in UNIX nanonseconds
}
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,
}
Stopwatch :: struct {
running: bool,
_start_time: Tick,
_accumulation: Duration,
}
now :: proc "contextless" () -> Time {
return _now()
}
sleep :: proc "contextless" (d: Duration) {
_sleep(d)
}
stopwatch_start :: proc "contextless" (stopwatch: ^Stopwatch) {
if !stopwatch.running {
stopwatch._start_time = tick_now()
stopwatch.running = true
}
}
stopwatch_stop :: proc "contextless" (stopwatch: ^Stopwatch) {
if stopwatch.running {
stopwatch._accumulation += tick_diff(stopwatch._start_time, tick_now())
stopwatch.running = false
}
}
stopwatch_reset :: proc "contextless" (stopwatch: ^Stopwatch) {
stopwatch._accumulation = {}
stopwatch.running = false
}
stopwatch_duration :: proc "contextless" (stopwatch: Stopwatch) -> Duration {
if !stopwatch.running {
return stopwatch._accumulation
}
return stopwatch._accumulation + tick_diff(stopwatch._start_time, tick_now())
}
diff :: proc "contextless" (start, end: Time) -> Duration {
d := end._nsec - start._nsec
return Duration(d)
}
since :: proc "contextless" (start: Time) -> Duration {
return diff(start, now())
}
duration_nanoseconds :: proc "contextless" (d: Duration) -> i64 {
return i64(d)
}
duration_microseconds :: proc "contextless" (d: Duration) -> f64 {
return duration_seconds(d) * 1e6
}
duration_milliseconds :: proc "contextless" (d: Duration) -> f64 {
return duration_seconds(d) * 1e3
}
duration_seconds :: proc "contextless" (d: Duration) -> f64 {
sec := d / Second
nsec := d % Second
return f64(sec) + f64(nsec)/1e9
}
duration_minutes :: proc "contextless" (d: Duration) -> f64 {
min := d / Minute
nsec := d % Minute
return f64(min) + f64(nsec)/(60*1e9)
}
duration_hours :: proc "contextless" (d: Duration) -> f64 {
hour := d / Hour
nsec := d % Hour
return f64(hour) + f64(nsec)/(60*60*1e9)
}
duration_round :: proc "contextless" (d, m: Duration) -> Duration {
_less_than_half :: #force_inline proc "contextless" (x, y: Duration) -> bool {
return u64(x)+u64(x) < u64(y)
}
if m <= 0 {
return d
}
r := d % m
if d < 0 {
r = -r
if _less_than_half(r, m) {
return d + r
}
if d1 := d-m+r; d1 < d {
return d1
}
return MIN_DURATION
}
if _less_than_half(r, m) {
return d - r
}
if d1 := d+m-r; d1 > d {
return d1
}
return MAX_DURATION
}
duration_truncate :: proc "contextless" (d, m: Duration) -> Duration {
return d if m <= 0 else d - d%m
}
date :: proc "contextless" (t: Time) -> (year: int, month: Month, day: int) {
year, month, day, _ = _abs_date(_time_abs(t), true)
return
}
year :: proc "contextless" (t: Time) -> (year: int) {
year, _, _, _ = _date(t, true)
return
}
month :: proc "contextless" (t: Time) -> (month: Month) {
_, month, _, _ = _date(t, true)
return
}
day :: proc "contextless" (t: Time) -> (day: int) {
_, _, day, _ = _date(t, true)
return
}
weekday :: proc "contextless" (t: Time) -> (weekday: Weekday) {
abs := _time_abs(t)
sec := (abs + u64(Weekday.Monday) * SECONDS_PER_DAY) % SECONDS_PER_WEEK
return Weekday(int(sec) / SECONDS_PER_DAY)
}
clock :: proc { clock_from_time, clock_from_duration, clock_from_stopwatch }
clock_from_time :: proc "contextless" (t: Time) -> (hour, min, sec: int) {
return clock_from_seconds(_time_abs(t))
}
clock_from_duration :: proc "contextless" (d: Duration) -> (hour, min, sec: int) {
return clock_from_seconds(u64(d/1e9))
}
clock_from_stopwatch :: proc "contextless" (s: Stopwatch) -> (hour, min, sec: int) {
return clock_from_duration(stopwatch_duration(s))
}
clock_from_seconds :: proc "contextless" (nsec: u64) -> (hour, min, sec: int) {
sec = int(nsec % SECONDS_PER_DAY)
hour = sec / SECONDS_PER_HOUR
sec -= hour * SECONDS_PER_HOUR
min = sec / SECONDS_PER_MINUTE
sec -= min * SECONDS_PER_MINUTE
return
}
read_cycle_counter :: proc "contextless" () -> u64 {
return u64(intrinsics.read_cycle_counter())
}
unix :: proc "contextless" (sec: i64, nsec: i64) -> Time {
sec, nsec := sec, nsec
if nsec < 0 || nsec >= 1e9 {
n := nsec / 1e9
sec += n
nsec -= n * 1e9
if nsec < 0 {
nsec += 1e9
sec -= 1
}
}
return Time{(sec*1e9 + nsec)}
}
to_unix_seconds :: time_to_unix
time_to_unix :: proc "contextless" (t: Time) -> i64 {
return t._nsec/1e9
}
to_unix_nanoseconds :: time_to_unix_nano
time_to_unix_nano :: proc "contextless" (t: Time) -> i64 {
return t._nsec
}
time_add :: proc "contextless" (t: Time, d: Duration) -> Time {
return Time{t._nsec + i64(d)}
}
// Accurate sleep borrowed from: https://blat-blatnik.github.io/computerBear/making-accurate-sleep-function/
//
// Accuracy seems to be pretty good out of the box on Linux, to within around 4µs worst case.
// On Windows it depends but is comparable with regular sleep in the worst case.
// To get the same kind of accuracy as on Linux, have your program call `win32.time_begin_period(1)` to
// tell Windows to use a more accurate timer for your process.
accurate_sleep :: proc "contextless" (d: Duration) {
to_sleep, estimate, mean, m2, count: Duration
to_sleep = d
estimate = 5 * Millisecond
mean = 5 * Millisecond
count = 1
for to_sleep > estimate {
start := tick_now()
sleep(1 * Millisecond)
observed := tick_since(start)
to_sleep -= observed
count += 1
delta := observed - mean
mean += delta / count
m2 += delta * (observed - mean)
stddev := intrinsics.sqrt(f64(m2) / f64(count - 1))
estimate = mean + Duration(stddev)
}
start := tick_now()
for to_sleep > tick_since(start) {
// prevent the spinlock from taking the thread hostage, still accurate enough
_yield()
// NOTE: it might be possible that it yields for too long, in that case it should spinlock freely for a while
// TODO: needs actual testing done to check if that's the case
}
}
ABSOLUTE_ZERO_YEAR :: i64(-292277022399) // Day is chosen so that 2001-01-01 is Monday in the calculations
ABSOLUTE_TO_INTERNAL :: i64(-9223371966579724800) // i64((ABSOLUTE_ZERO_YEAR - 1) * 365.2425 * SECONDS_PER_DAY);
INTERNAL_TO_ABSOLUTE :: -ABSOLUTE_TO_INTERNAL
UNIX_TO_INTERNAL :: i64((1969*365 + 1969/4 - 1969/100 + 1969/400) * SECONDS_PER_DAY)
INTERNAL_TO_UNIX :: -UNIX_TO_INTERNAL
WALL_TO_INTERNAL :: i64((1884*365 + 1884/4 - 1884/100 + 1884/400) * SECONDS_PER_DAY)
INTERNAL_TO_WALL :: -WALL_TO_INTERNAL
UNIX_TO_ABSOLUTE :: UNIX_TO_INTERNAL + INTERNAL_TO_ABSOLUTE
ABSOLUTE_TO_UNIX :: -UNIX_TO_ABSOLUTE
@(private)
_date :: proc "contextless" (t: Time, full: bool) -> (year: int, month: Month, day: int, yday: int) {
year, month, day, yday = _abs_date(_time_abs(t), full)
return
}
@(private)
_time_abs :: proc "contextless" (t: Time) -> u64 {
return u64(t._nsec/1e9 + UNIX_TO_ABSOLUTE)
}
@(private)
_abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Month, day: int, yday: int) {
d := abs / SECONDS_PER_DAY
// 400 year cycles
n := d / DAYS_PER_400_YEARS
y := 400 * n
d -= DAYS_PER_400_YEARS * n
// Cut-off 100 year cycles
n = d / DAYS_PER_100_YEARS
n -= n >> 2
y += 100 * n
d -= DAYS_PER_100_YEARS * n
// Cut-off 4 year cycles
n = d / DAYS_PER_4_YEARS
y += 4 * n
d -= DAYS_PER_4_YEARS * n
n = d / 365
n -= n >> 2
y += n
d -= 365 * n
year = int(i64(y) + ABSOLUTE_ZERO_YEAR)
yday = int(d)
if !full {
return
}
day = yday
if is_leap_year(year) {
switch {
case day > 31+29-1:
day -= 1
case day == 31+29-1:
month = .February
day = 29
return
}
}
month = Month(day / 31)
end := int(days_before[int(month)+1])
begin: int
if day >= end {
(^int)(&month)^ += 1
begin = end
} else {
begin = int(days_before[month])
}
(^int)(&month)^ += 1 // January is 1
day = day - begin + 1
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}}
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}
is_leap_year :: proc "contextless" (year: int) -> (leap: bool) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
days_before := [?]i32{
0,
31,
31 + 28,
31 + 28 + 31,
31 + 28 + 31 + 30,
31 + 28 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
}
SECONDS_PER_MINUTE :: 60
SECONDS_PER_HOUR :: 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY :: 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK :: 7 * SECONDS_PER_DAY
DAYS_PER_400_YEARS :: 365*400 + 97
DAYS_PER_100_YEARS :: 365*100 + 24
DAYS_PER_4_YEARS :: 365*4 + 1