hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 3bd4544d20954217ecc17516417d8fea758ad0b2
parent de5d92ff32c045d7f6ad0bc391630318b6915646
Author: Vlad-Stefan Harbuz <vlad@vladh.net>
Date:   Sat, 22 Jan 2022 17:31:34 +0100

add arithmetic functions

Add the following functions:
* diff()
* diff_in_unit()
* start_of()
* hop()
* add()
* subtract()

Signed-off-by: Vlad-Stefan Harbuz <vlad@vladh.net>

Diffstat:
Mdatetime/arithmetic.ha | 837++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 790 insertions(+), 47 deletions(-)

diff --git a/datetime/arithmetic.ha b/datetime/arithmetic.ha @@ -1,5 +1,14 @@ use fmt; use time::chrono; +use time; + +fn absi(n: i64) i64 = { + if (n < 0) { + return -n; + } else { + return n; + }; +}; // Represents a span of time in the proleptic Gregorian calendar, // using relative units of time. Used for calendar arithmetic. @@ -17,53 +26,154 @@ export type period = struct { hours: int, minutes: int, seconds: int, - nanoseconds: int, + nanoseconds: i64, }; // Prints to stdout the representation of a period. // // TODO: This is a debug utility. Remove this in favour of changing format() to -// accept arguments of type (*datetime | period), using the "intervals" standard +// accept arguments of type (datetime | period), using the "intervals" standard // representation provided by ISO 8601. // // See https://en.wikipedia.org/wiki/ISO_8601#Time_intervals export fn print_period(p: period) void = { fmt::printfln( "eras: {}\nyears: {}\nmonths: {}\nweeks: {}\ndays: {}\n" - "hours: {}\nminutes: {}\nseconds: {}\nnanoseconds: {}\n", + "hours: {}\nminutes: {}\nseconds: {}\nnanoseconds: {}", p.eras, p.years, p.months, p.weeks, p.days, p.hours, p.minutes, p.seconds, p.nanoseconds )!; }; // Specifies behaviour during calendar arithmetic +// +// * DEFAULT +// Units are added in the order of largest (years) to smallest (nanoseconds). If +// the resulting date does not exist, the first extant date previous to the +// initial result is returned. export type calculus = enum int { - LOGICAL, - PHYSICAL, + DEFAULT, +}; + +// The various datetime units that can be used for arithmetic +export type unit = enum int { + ERA, + YEAR, + MONTH, + WEEK, + DAY, + HOUR, + MINUTE, + SECOND, + NANOSECOND, }; // Returns whether or not two dates are numerically equal -export fn eq(a: *datetime, b: *datetime) bool = { - // TODO: Factor timezones into this +export fn eq(a: datetime, b: datetime) bool = { return a.date == b.date && a.time == b.time; }; // Returns whether or not the first date is after the second date -export fn is_after(a: *datetime, b: *datetime) bool = { - // TODO: Factor timezones into this +export fn is_after(a: datetime, b: datetime) bool = { return !eq(a, b) && (a.date > b.date || a.date == b.date && a.time > b.time); }; // Returns whether or not the first date is before the second date -export fn is_before(a: *datetime, b: *datetime) bool = { +export fn is_before(a: datetime, b: datetime) bool = { return !eq(a, b) && !is_after(a, b); }; // Calculates the difference between two datetimes export fn diff(a: datetime, b: datetime) period = { - // TODO - return period { ... }; + let res = period { ... }; + if (eq(a, b)) { + return res; + }; + if (is_after(b, a)) { + const tmp = a; + a = b; + b = tmp; + }; + + res.years = year(&a) - year(&b); + + res.months = month(&a) - month(&b); + if (res.months < 0) { + res.years -= 1; + res.months = 12 + res.months; + }; + + res.days = day(&a) - day(&b); + if (res.days < 0) { + let prev_month_year = year(&a); + let prev_month = month(&a) - 1; + if (prev_month == 0) { + prev_month_year -= 1; + prev_month = 12; + }; + const n_days_in_prev_month = calc_n_days_in_month( + prev_month_year, prev_month); + res.months -= 1; + res.days = n_days_in_prev_month + res.days; + }; + + res.hours = hour(&a) - hour(&b); + if (res.hours < 0) { + res.days -= 1; + res.hours = 24 + res.hours; + }; + + res.minutes = min(&a) - min(&b); + if (res.minutes < 0) { + res.hours -= 1; + res.minutes = 60 + res.minutes; + }; + + res.seconds = sec(&a) - sec(&b); + if (res.seconds < 0) { + res.minutes -= 1; + res.seconds = 60 + res.seconds; + }; + + res.nanoseconds = nsec(&a) - nsec(&b); + if (res.nanoseconds < 0) { + res.seconds -= 1; + res.nanoseconds = time::SECOND + res.nanoseconds; + }; + + return res; +}; + +// Calculates the difference between two datetimes in the given unit. An integer +// is returned, with the fractional part of the difference being discarded. +export fn diff_in_unit(a: datetime, b: datetime, u: unit) i64 = { + return switch (u) { + case unit::ERA => + yield absi(era(&a) - era(&b)); + case unit::YEAR => + yield diff(a, b).years; + case unit::MONTH => + const full_diff = diff(a, b); + yield full_diff.years * 12 + full_diff.months; + case unit::WEEK => + yield diff_in_unit(a, b, unit::DAY) / 7; + case unit::DAY => + yield absi(a.date - b.date): int; + case unit::HOUR => + const full_diff = diff(a, b); + yield (diff_in_unit(a, b, unit::DAY) * 24) + full_diff.hours; + case unit::MINUTE => + const full_diff = diff(a, b); + yield diff_in_unit(a, b, unit::HOUR) * 60 + full_diff.minutes; + case unit::SECOND => + const full_diff = diff(a, b); + yield diff_in_unit(a, b, unit::MINUTE) * 60 + full_diff.seconds; + case unit::NANOSECOND => + const full_diff = diff(a, b); + yield diff_in_unit(a, b, unit::SECOND) * time::SECOND + + full_diff.nanoseconds; + }; }; // Returns whether or not two periods are numerically equal @@ -79,6 +189,43 @@ export fn period_eq(a: period, b: period) bool = { a.nanoseconds == b.nanoseconds; }; +// Returns the given datetime at the start of a particular given unit, e.g. +// the start of the day or the start of the minute. +export fn start_of(u: unit, dt: datetime) datetime = { + // TODO: Replace all of the 0s for the zoffset with the actual + // zoffset once the API is solidified a bit + return switch (u) { + case unit::ERA => + yield new(01, 01, 01, 00, 00, 00, 0, + 0, dt.loc)!; + case unit::YEAR => + yield new(year(&dt), 01, 01, 00, 00, 00, 0, + 0, dt.loc)!; + case unit::MONTH => + yield new(year(&dt), month(&dt), 01, 00, 00, 00, 0, + 0, dt.loc)!; + case unit::WEEK => + const new_epochal = dt.date - (weekday(&dt) - 1); + const new_ymd = calc_ymd(new_epochal); + yield new(new_ymd.0, new_ymd.1, new_ymd.2, 00, 00, 00, 0, + 0, dt.loc)!; + case unit::DAY => + yield new(year(&dt), month(&dt), day(&dt), 00, 00, 00, 0, + 0, dt.loc)!; + case unit::HOUR => + yield new(year(&dt), month(&dt), day(&dt), hour(&dt), 00, 00, 0, + 0, dt.loc)!; + case unit::MINUTE => + yield new(year(&dt), month(&dt), day(&dt), hour(&dt), min(&dt), + 00, 0, 0, dt.loc)!; + case unit::SECOND => + yield new(year(&dt), month(&dt), day(&dt), hour(&dt), min(&dt), + sec(&dt), 0, 0, dt.loc)!; + case unit::NANOSECOND => + yield dt; + }; +}; + // Hops, starting from a datetime, to static inter-period points along the // calendar, according to the given periods, and returns a new datetime. // Inter-period points are the starts of years, months, days, etc. @@ -98,60 +245,656 @@ export fn period_eq(a: period, b: period) bool = { // }); // export fn hop(dt: datetime, pp: period...) datetime = { - // TODO + let new_dt = clone(dt); for (let i = 0z; i < len(pp); i += 1) { const p = pp[i]; + + if (p.years != 0) { + const dt_inc = add(new_dt, calculus::DEFAULT, + period { years = p.years, ... }); + new_dt = start_of(unit::YEAR, dt_inc); + }; + if (p.months != 0) { + const dt_inc = add(new_dt, calculus::DEFAULT, + period { months = p.months, ... }); + new_dt = start_of(unit::MONTH, dt_inc); + }; + if (p.weeks != 0) { + const dt_inc = add(new_dt, calculus::DEFAULT, + period { weeks = p.weeks, ... }); + new_dt = start_of(unit::WEEK, dt_inc); + }; + if (p.days != 0) { + const dt_inc = add(new_dt, calculus::DEFAULT, + period { days = p.days, ... }); + new_dt = start_of(unit::DAY, dt_inc); + }; + if (p.hours != 0) { + const dt_inc = add(new_dt, calculus::DEFAULT, + period { hours = p.hours, ... }); + new_dt = start_of(unit::HOUR, dt_inc); + }; + if (p.minutes != 0) { + const dt_inc = add(new_dt, calculus::DEFAULT, + period { minutes = p.minutes, ... }); + new_dt = start_of(unit::MINUTE, dt_inc); + }; + if (p.seconds != 0) { + const dt_inc = add(new_dt, calculus::DEFAULT, + period { seconds = p.seconds, ... }); + new_dt = start_of(unit::SECOND, dt_inc); + }; + if (p.nanoseconds != 0) { + new_dt = add(new_dt, calculus::DEFAULT, + period { nanoseconds = p.nanoseconds, ... }); + }; }; - return dt; + return new_dt; }; -// Adds a calindrical period of time to a datetime, largest units first. +// Adds a calendrical period of time to a datetime, largest units first. // Tries to conserve relative distance from cyclical points on the calendar. // // let dt = ... // 1999-05-13 12:30:45 -// datetime::hop(dt, datetime::calculus::LOGICAL, datetime::period { -// years = 22, // 2021-05-13 00:00:00 -// months = -1, // 2021-04-13 00:00:00 -// days = -4, // 2020-04-09 00:00:00 +// datetime::add(dt, datetime::calculus::DEFAULT, datetime::period { +// years = 22, // 2021-05-13 12:30:45 +// months = -1, // 2021-04-13 12:30:45 +// days = -4, // 2020-04-09 12:30:45 // }); -// -// When units overflow, such as when adding a month to Jan 31st would -// erroneously result in Feb 31th, the flag is consulted on how to handle this. -// -// TODO: -// How to handle overflows and predictability with cal-arithm in general? -export fn add(dt: datetime, flag: int, pp: period...) datetime = { - // TODO +export fn add(dt: datetime, flag: calculus, pp: period...) datetime = { + let d_year = year(&dt); + let d_month = month(&dt); + let d_day = day(&dt); + let d_hour = hour(&dt); + let d_min = min(&dt); + let d_sec = sec(&dt); + let d_nsec = ((nsec(&dt)): i64); for (let i = 0z; i < len(pp); i += 1) { const p = pp[i]; + + let latest_epochal = dt.date; + + if (p.years != 0) { + d_year += p.years; + }; + if (p.months != 0) { + d_month += p.months; + }; + if (d_month > 12) { + d_year += (d_month - 1) / 12; + d_month = d_month % 12; + }; + if (d_month < 1) { + d_year -= (12 + -(d_month - 1)) / 12; + d_month = 12 - (-d_month % 12); + }; + const n_days_in_month = calc_n_days_in_month(d_year, d_month); + if (d_day > n_days_in_month) { + d_day = n_days_in_month; + }; + + if (p.weeks != 0) { + p.days += p.weeks * 7; + }; + latest_epochal = calc_epochal_from_ymd( + d_year, d_month, d_day)!; + if (p.days != 0) { + const new_ymd = calc_ymd(latest_epochal + p.days); + d_year = new_ymd.0; + d_month = new_ymd.1; + d_day = new_ymd.2; + latest_epochal = calc_epochal_from_ymd( + d_year, d_month, d_day)!; + }; + + if (p.hours != 0) { + p.nanoseconds += p.hours * time::HOUR; + }; + if (p.minutes != 0) { + p.nanoseconds += p.minutes * time::MINUTE; + }; + if (p.seconds != 0) { + p.nanoseconds += p.seconds * time::SECOND; + }; + if (p.nanoseconds != 0) { + const ns_in_day = 24 * time::HOUR; + let overflowed_days = 0; + + if (absi(p.nanoseconds) > ns_in_day) { + overflowed_days += + ((p.nanoseconds / ns_in_day): int); + p.nanoseconds %= ns_in_day; + }; + + let new_time = dt.time + p.nanoseconds; + + if (new_time >= ns_in_day) { + overflowed_days += 1; + new_time -= ns_in_day; + } else if (new_time < 0) { + overflowed_days -= 1; + new_time += ns_in_day; + }; + + if (overflowed_days != 0) { + const new_epochal = latest_epochal + + overflowed_days; + const new_ymd = calc_ymd(new_epochal); + d_year = new_ymd.0; + d_month = new_ymd.1; + d_day = new_ymd.2; + }; + const new_hmsn = calc_hmsn(new_time); + d_hour = new_hmsn.0; + d_min = new_hmsn.1; + d_sec = new_hmsn.2; + d_nsec = new_hmsn.3; + }; + }; + // TODO: Add zoffset back in here once API is settled + return new(d_year, d_month, d_day, d_hour, d_min, d_sec, d_nsec: int, + 0, dt.loc)!; +}; + +// Subtracts a calendrical period of time to a datetime, largest units first. +// Tries to conserve relative distance from cyclical points on the calendar. +// +// let dt = ... // 1999-05-13 12:30:45 +// datetime::subtract(dt, datetime::calculus::DEFAULT, datetime::period { +// years = 22, // 1977-05-13 12:30:45 +// months = -1, // 1977-06-13 12:30:45 +// days = -4, // 1977-06-17 12:30:45 +// }); +export fn subtract(dt: datetime, flag: calculus, pp: period...) datetime = { + for (let i = 0z; i < len(pp); i += 1) { + pp[i].eras *= -1; + pp[i].years *= -1; + pp[i].months *= -1; + pp[i].weeks *= -1; + pp[i].days *= -1; + pp[i].minutes *= -1; + pp[i].seconds *= -1; + pp[i].nanoseconds *= -1; }; - return dt; + return add(dt, flag, pp...); }; @test fn eq() void = { - const d0 = new(2022, 02, 04, 03, 14, 07, 00, 0, chrono::local)!; - const d_eq = new(2022, 02, 04, 03, 14, 07, 00, 0, chrono::local)!; - const d_neq = new(2022, 02, 04, 03, 14, 07, 01, 0, chrono::local)!; - assert(eq(&d0, &d_eq), "equal dates erroneously treated as unequal"); - assert(!eq(&d0, &d_neq), "unequal dates erroneously treated as equal"); + const dt = new(2022, 02, 04, 03, 14, 07, 00, 0, chrono::UTC_Z)!; + const cases = [ + ((-768, 01, 01, 03, 14, 07, 0), false), + ((1, 1, 01, 14, 00, 00, 1234), false), + ((2022, 02, 04, 03, 14, 07, 0), true), + ((2022, 02, 04, 03, 14, 07, 1), false), + ((2038, 01, 19, 03, 14, 07, 0), false), + ((5555, 05, 05, 05, 55, 55, 5555), false), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const parts = cases[i].0; + const expected = cases[i].1; + const case_dt = new(parts.0, parts.1, parts.2, + parts.3, parts.4, parts.5, parts.6, + 0, chrono::UTC_Z)!; + assert(eq(dt, case_dt) == expected, + "equality comparison failed"); + }; }; @test fn is_after() void = { - const d0 = new(2022, 02, 04, 03, 14, 07, 00, 0, chrono::local)!; - const d_eq = new(2022, 02, 04, 03, 14, 07, 00, 0, chrono::local)!; - const d_gt = new(2022, 02, 04, 04, 01, 01, 01, 0, chrono::local)!; - const d_lt = new(2020, 02, 04, 33, 14, 07, 01, 0, chrono::local)!; - assert(is_after(&d0, &d_lt), "incorrect date ordering in is_after()"); - assert(!is_after(&d0, &d_eq), "incorrect date ordering in is_after()"); - assert(!is_after(&d0, &d_gt), "incorrect date ordering in is_after()"); + const dt = new(2022, 02, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!; + const cases = [ + ((-768, 01, 01, 03, 14, 07, 0), false), + ((1, 1, 01, 14, 00, 00, 1234), false), + ((2020, 02, 04, 03, 14, 07, 1), false), + ((2022, 02, 04, 03, 14, 07, 0), false), + ((2022, 02, 04, 04, 01, 01, 0), true), + ((2038, 01, 19, 03, 14, 07, 0), true), + ((5555, 05, 05, 05, 55, 55, 5555), true), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const parts = cases[i].0; + const expected = cases[i].1; + const case_dt = new(parts.0, parts.1, parts.2, + parts.3, parts.4, parts.5, parts.6, + 0, chrono::UTC_Z)!; + assert(is_after(case_dt, dt) == expected, + "incorrect date ordering in is_after()"); + }; }; @test fn is_before() void = { - const d0 = new(2022, 02, 04, 03, 14, 07, 00, 0, chrono::local)!; - const d_eq = new(2022, 02, 04, 03, 14, 07, 00, 0, chrono::local)!; - const d_gt = new(2022, 02, 04, 04, 01, 01, 01, 0, chrono::local)!; - const d_lt = new(2020, 02, 04, 33, 14, 07, 01, 0, chrono::local)!; - assert(!is_before(&d0, &d_lt), "incorrect date ordering in is_before()"); - assert(!is_before(&d0, &d_eq), "incorrect date ordering in is_before()"); - assert(is_before(&d0, &d_gt), "incorrect date ordering in is_before()"); + const dt = new(2022, 02, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!; + const cases = [ + ((-768, 01, 01, 03, 14, 07, 0), true), + ((1, 1, 01, 14, 00, 00, 1234), true), + ((2020, 02, 04, 03, 14, 07, 1), true), + ((2022, 02, 04, 03, 14, 07, 0), false), + ((2022, 02, 04, 04, 01, 01, 0), false), + ((2038, 01, 19, 03, 14, 07, 0), false), + ((5555, 05, 05, 05, 55, 55, 5555), false), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const parts = cases[i].0; + const expected = cases[i].1; + const case_dt = new(parts.0, parts.1, parts.2, + parts.3, parts.4, parts.5, parts.6, + 0, chrono::UTC_Z)!; + assert(is_before(case_dt, dt) == expected, + "incorrect date ordering in is_before()"); + }; +}; + +@test fn diff() void = { + const cases = [ + ( + new(2021, 01, 15, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(2022, 02, 16, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + period { + years = 1, + months = 1, + days = 1, + ... + }, + ), + ( + new(2021, 01, 15, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(2022, 03, 27, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + period { + years = 1, + months = 2, + days = 12, + ... + }, + ), + ( + new(2021, 01, 15, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(2022, 03, 14, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + period { + years = 1, + months = 1, + days = 27, + ... + }, + ), + ( + new(2021, 01, 15, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(2021, 01, 16, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + period { + days = 1, + ... + }, + ), + ( + new(2021, 01, 15, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(2021, 01, 16, 01, 03, 02, 4, 0, chrono::UTC_Z)!, + period { + days = 1, + hours = 1, + minutes = 3, + seconds = 2, + nanoseconds = 4, + ... + }, + ), + ( + new(2021, 01, 15, 02, 03, 02, 2, 0, chrono::UTC_Z)!, + new(2021, 01, 16, 01, 01, 02, 4, 0, chrono::UTC_Z)!, + period { + hours = 22, + minutes = 58, + nanoseconds = 2, + ... + }, + ), + ( + new(0500, 01, 01, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(3500, 01, 01, 00, 06, 00, 0, 0, chrono::UTC_Z)!, + period { + years = 3000, + minutes = 6, + ... + }, + ), + ( + new(-500, 01, 01, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(2500, 01, 01, 00, 06, 00, 0, 0, chrono::UTC_Z)!, + period { + years = 3000, + minutes = 6, + ... + }, + ), + ( + new(2000, 01, 01, 00, 00, 00, 0, 0, chrono::UTC_Z)!, + new(2000, 01, 01, 00, 06, 00, 999999999, 0, chrono::UTC_Z)!, + period { + minutes = 6, + nanoseconds = 999999999, + ... + }, + ), + ( + new(2000, 01, 01, 00, 06, 00, 999999999, 0, chrono::UTC_Z)!, + new(2000, 01, 01, 00, 06, 01, 0, 0, chrono::UTC_Z)!, + period { + nanoseconds = 1, + ... + }, + ), + ( + new(-9000, 01, 01, 00, 06, 00, 999999999, 0, chrono::UTC_Z)!, + new(9000, 01, 01, 00, 06, 01, 0, 0, chrono::UTC_Z)!, + period { + years = 18000, + nanoseconds = 1, + ... + }, + ), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const dta = cases[i].0; + const dtb = cases[i].1; + const expected = cases[i].2; + const actual = diff(dta, dtb); + assert(period_eq(actual, expected), "diff miscalculation"); + }; +}; + +@test fn diff_in_unit() void = { + const cases = [ + ( + new(1994, 08, 27, 11, 20, 01, 2, 0, chrono::UTC_Z)!, + new(2022, 01, 05, 13, 53, 30, 20, 0, chrono::UTC_Z)!, + (27, 328, 1427, 9993, 239834, 14390073, 863404409i64, + (863404409i64 * time::SECOND) + 18), + ), + ( + new(1994, 08, 28, 11, 20, 01, 2, 0, chrono::UTC_Z)!, + new(1994, 08, 27, 11, 20, 01, 0, 0, chrono::UTC_Z)!, + (0, 0, 0, 1, 24, 1440, 86400i64, + (86400i64 * time::SECOND) + 2), + ), + ( + new(1994, 08, 27, 11, 20, 01, 0, 0, chrono::UTC_Z)!, + new(1994, 08, 27, 11, 20, 01, 0, 0, chrono::UTC_Z)!, + (0, 0, 0, 0, 0, 0, 0i64, 0i64), + ), + ( + new(-500, 01, 01, 00, 59, 01, 0, 0, chrono::UTC_Z)!, + new(2000, 01, 01, 23, 01, 01, 0, 0, chrono::UTC_Z)!, + (2500, 30000, 130443, 913106, 913106 * 24 + 22, + (913106 * 24 + 22) * 60 + 2, + ((913106 * 24 + 22) * 60 + 2) * 60i64, + (((913106 * 24 + 22) * 60 + 2) * 60i64 * + time::SECOND)), + ), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const dta = cases[i].0; + const dtb = cases[i].1; + const expected = cases[i].2; + assert(diff_in_unit(dtb, dta, unit::YEAR) == expected.0, + "invalid diff_in_years() result"); + assert(diff_in_unit(dtb, dta, unit::MONTH) == expected.1, + "invalid diff_in_months() result"); + assert(diff_in_unit(dtb, dta, unit::WEEK) == expected.2, + "invalid diff_in_weeks() result"); + assert(diff_in_unit(dtb, dta, unit::DAY) == expected.3, + "invalid diff_in_days() result"); + assert(diff_in_unit(dtb, dta, unit::HOUR) == expected.4, + "invalid diff_in_hours() result"); + assert(diff_in_unit(dtb, dta, unit::MINUTE) == expected.5, + "invalid diff_in_minutes() result"); + assert(diff_in_unit(dtb, dta, unit::SECOND) == expected.6, + "invalid diff_in_seconds() result"); + assert(diff_in_unit(dtb, dta, unit::NANOSECOND) == expected.7, + "invalid diff_in_nanoseconds() result"); + }; +}; + +@test fn start_of() void = { + const dt = new(1994, 08, 27, 11, 20, 01, 2, 0, chrono::UTC_Z)!; + assert(eq(start_of(unit::ERA, dt), + new(01, 01, 01, 00, 00, 00, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::YEAR, dt), + new(1994, 01, 01, 00, 00, 00, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::MONTH, dt), + new(1994, 08, 01, 00, 00, 00, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::WEEK, dt), + new(1994, 08, 22, 00, 00, 00, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::DAY, dt), + new(1994, 08, 27, 00, 00, 00, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::HOUR, dt), + new(1994, 08, 27, 11, 00, 00, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::MINUTE, dt), + new(1994, 08, 27, 11, 20, 00, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::SECOND, dt), + new(1994, 08, 27, 11, 20, 01, 0, 0, chrono::UTC_Z)!), + "invalid start_of() result"); + assert(eq(start_of(unit::NANOSECOND, dt), dt), + "invalid start_of() result"); +}; + +@test fn add() void = { + const d = new(2022, 02, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!; + const cases = [ + ( + period { years = 1, ... }, + new(2023, 02, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { years = -23, ... }, + new(1999, 02, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { months = 2, ... }, + new(2022, 04, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { months = 11, ... }, + new(2023, 01, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { months = -1, ... }, + new(2022, 01, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { months = -2, ... }, + new(2021, 12, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { days = 3, ... }, + new(2022, 02, 07, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { days = 33, ... }, + new(2022, 03, 09, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { days = 333, ... }, + new(2023, 01, 03, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { days = -2, ... }, + new(2022, 02, 02, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { days = -4, ... }, + new(2022, 01, 31, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { days = -1337, ... }, + new(2018, 06, 08, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { hours = 1, ... }, + new(2022, 02, 04, 04, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { hours = 24, ... }, + new(2022, 02, 05, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { hours = 25, ... }, + new(2022, 02, 05, 04, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { hours = 123456, ... }, + new(2036, 03, 06, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { hours = -2, ... }, + new(2022, 02, 04, 01, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { hours = -24, ... }, + new(2022, 02, 03, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { hours = -123456, ... }, + new(2008, 01, 05, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { seconds = 2, ... }, + new(2022, 02, 04, 03, 14, 09, 0, 0, chrono::UTC_Z)!, + ), + ( + period { seconds = 666666666, ... }, + new(2043, 03, 22, 04, 25, 13, 0, 0, chrono::UTC_Z)!, + ), + ( + period { seconds = -2, ... }, + new(2022, 02, 04, 03, 14, 05, 0, 0, chrono::UTC_Z)!, + ), + ( + period { seconds = -666666666, ... }, + new(2000, 12, 20, 02, 03, 01, 0, 0, chrono::UTC_Z)!, + ), + ( + period { nanoseconds = 123, ... }, + new(2022, 02, 04, 03, 14, 07, 123, 0, chrono::UTC_Z)!, + ), + ( + period { nanoseconds = 1361661361461, ... }, + new(2022, 02, 04, 03, 36, 48, 661361461, + 0, chrono::UTC_Z)!, + ), + ( + period { nanoseconds = -1361661361461, ... }, + new(2022, 02, 04, 02, 51, 25, 338638539, + 0, chrono::UTC_Z)!, + ), + ( + period { months = 1, seconds = -666666666, ... }, + new(2001, 01, 17, 02, 03, 01, 0, 0, chrono::UTC_Z)!, + ), + ( + period { months = 1, seconds = -666666666, ... }, + new(2001, 01, 17, 02, 03, 01, 0, 0, chrono::UTC_Z)!, + ), + ( + period { + years = -1, + months = -2, + weeks = -3, + days = -4, + hours = -5, + minutes = -6, + seconds = -7, + nanoseconds = -8, + ... + }, + new(2020, 11, 08, 22, 07, 59, 999999992, + 0, chrono::UTC_Z)!, + ), + ( + period { + years = 1, + months = 2, + weeks = 3, + days = 4, + hours = 5, + minutes = 6, + seconds = 7, + nanoseconds = 8, + ... + }, + new(2023, 04, 29, 08, 20, 14, 8, 0, chrono::UTC_Z)!, + ), + ( + period { + years = 1, + months = -2, + weeks = 3, + days = -5, + hours = 8, + minutes = -13, + seconds = 21, + nanoseconds = -34, + ... + }, + new(2022, 12, 20, 11, 01, 27, 999999966, + 0, chrono::UTC_Z)!, + ), + ( + period { + years = -1, + months = 12, + weeks = -52, + days = -31, + hours = 24, + minutes = -3600, + seconds = 3600, + nanoseconds = -86400000000000, + ... + }, + new(2021, 01, 02, 16, 14, 07, 0, + 0, chrono::UTC_Z)!, + ), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const p = cases[i].0; + const expected = cases[i].1; + const actual = add(d, calculus::DEFAULT, p); + if (!eq(actual, expected)) { + fmt::printfln("attempting to add:")!; + print_period(p); + fmt::printfln("expected {}", + format("%F %T.%N", &expected)!)!; + fmt::printfln("was {}\n", + format("%F %T.%N", &actual)!)!; + }; + assert(eq(actual, expected), "addition miscalculation"); + }; +}; + +@test fn subtract() void = { + const d = new(2022, 02, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!; + const cases = [ + ( + period { years = 1, ... }, + new(2021, 02, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { months = 2, ... }, + new(2021, 12, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ( + period { months = 14, ... }, + new(2020, 12, 04, 03, 14, 07, 0, 0, chrono::UTC_Z)!, + ), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const p = cases[i].0; + const expected = cases[i].1; + const actual = subtract(d, calculus::DEFAULT, p); + assert(eq(actual, expected), "subtraction miscalculation"); + }; };