hare

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

arithmetic.ha (23562B)


      1 // License: MPL-2.0
      2 // (c) 2021-2022 Byron Torres <b@torresjrjr.com>
      3 // (c) 2022 Drew DeVault <sir@cmpwn.com>
      4 // (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net>
      5 use fmt;
      6 use math;
      7 use time;
      8 use time::chrono;
      9 
     10 // Represents a span of time in the Gregorian chronology, using nominal units of
     11 // time. Used for datetime arithmetic.
     12 export type period = struct {
     13 	eras: int,
     14 	years: int,
     15 
     16 	// Can be 28, 29, 30, or 31 days long
     17 	months: int,
     18 
     19 	// Weeks start on Monday
     20 	weeks: int,
     21 
     22 	days: int,
     23 	hours: int,
     24 	minutes: int,
     25 	seconds: int,
     26 	nanoseconds: i64,
     27 };
     28 
     29 // Specifies the behaviour of calendar arithmetic.
     30 export type calculus = enum int {
     31 	// Units are added in the order of largest (years) to smallest
     32 	// (nanoseconds). If the resulting date does not exist, the first extant
     33 	// date previous to the initial result is returned.
     34 	DEFAULT,
     35 };
     36 // TODO: ^ Expand this
     37 
     38 // The nominal units of the Gregorian chronology. Used for datetime arithmetic.
     39 export type unit = enum int {
     40 	ERA,
     41 	YEAR,
     42 	MONTH,
     43 	WEEK,
     44 	DAY,
     45 	HOUR,
     46 	MINUTE,
     47 	SECOND,
     48 	NANOSECOND,
     49 };
     50 
     51 // Returns true if two [[datetime]]s are equivalent.
     52 //
     53 // Equivalence means they represent the same moment in time, regardless of their
     54 // locality or observed chronological values.
     55 export fn eq(a: datetime, b: datetime) bool = {
     56 	return a.date == b.date && a.time == b.time;
     57 };
     58 
     59 // Returns true if [[datetime]] "a" succeeds [[datetime]] "b".
     60 //
     61 // Temporal order is evaluated in a universal frame of reference, regardless of
     62 // their locality or observed chronological values.
     63 export fn after(a: datetime, b: datetime) bool = {
     64 	return !eq(a, b) &&
     65 		(a.date > b.date || a.date == b.date && a.time > b.time);
     66 };
     67 
     68 // Returns true if [[datetime]] "a" precedes [[datetime]] "b".
     69 //
     70 // Temporal order is evaluated in a universal frame of reference, regardless of
     71 // their locality or observed chronological values.
     72 export fn before(a: datetime, b: datetime) bool = {
     73 	return !eq(a, b) && !after(a, b);
     74 };
     75 
     76 // Calculates the [[period]] between two [[datetime]]s.
     77 export fn diff(a: datetime, b: datetime) period = {
     78 	let res = period { ... };
     79 	if (eq(a, b)) {
     80 		return res;
     81 	};
     82 	if (after(b, a)) {
     83 		const tmp = a;
     84 		a = b;
     85 		b = tmp;
     86 	};
     87 
     88 	res.years = year(&a) - year(&b);
     89 
     90 	res.months = month(&a) - month(&b);
     91 	if (res.months < 0) {
     92 		res.years -= 1;
     93 		res.months = 12 + res.months;
     94 	};
     95 
     96 	res.days = day(&a) - day(&b);
     97 	if (res.days < 0) {
     98 		let prev_month_year = year(&a);
     99 		let prev_month = month(&a) - 1;
    100 		if (prev_month == 0) {
    101 			prev_month_year -= 1;
    102 			prev_month = 12;
    103 		};
    104 		const n_days_in_prev_month = calc_n_days_in_month(
    105 			prev_month_year, prev_month);
    106 		res.months -= 1;
    107 		res.days = n_days_in_prev_month + res.days;
    108 	};
    109 
    110 	res.hours = hour(&a) - hour(&b);
    111 	if (res.hours < 0) {
    112 		res.days -= 1;
    113 		res.hours = 24 + res.hours;
    114 	};
    115 
    116 	res.minutes = min(&a) - min(&b);
    117 	if (res.minutes < 0) {
    118 		res.hours -= 1;
    119 		res.minutes = 60 + res.minutes;
    120 	};
    121 
    122 	res.seconds = sec(&a) - sec(&b);
    123 	if (res.seconds < 0) {
    124 		res.minutes -= 1;
    125 		res.seconds = 60 + res.seconds;
    126 	};
    127 
    128 	res.nanoseconds = nsec(&a) - nsec(&b);
    129 	if (res.nanoseconds < 0) {
    130 		res.seconds -= 1;
    131 		res.nanoseconds = time::SECOND + res.nanoseconds;
    132 	};
    133 
    134 	return res;
    135 };
    136 
    137 // Calculates the difference between two [[datetime]]s using the given nominal
    138 // [[unit]], truncating towards zero.
    139 export fn unitdiff(a: datetime, b: datetime, u: unit) i64 = {
    140 	return switch (u) {
    141 	case unit::ERA =>
    142 		yield math::absi(era(&a) - era(&b)): i64;
    143 	case unit::YEAR =>
    144 		yield diff(a, b).years;
    145 	case unit::MONTH =>
    146 		const full_diff = diff(a, b);
    147 		yield full_diff.years * 12 + full_diff.months;
    148 	case unit::WEEK =>
    149 		yield unitdiff(a, b, unit::DAY) / 7;
    150 	case unit::DAY =>
    151 		yield math::absi(a.date - b.date): int;
    152 	case unit::HOUR =>
    153 		const full_diff = diff(a, b);
    154 		yield (unitdiff(a, b, unit::DAY) * 24) + full_diff.hours;
    155 	case unit::MINUTE =>
    156 		const full_diff = diff(a, b);
    157 		yield unitdiff(a, b, unit::HOUR) * 60 + full_diff.minutes;
    158 	case unit::SECOND =>
    159 		const full_diff = diff(a, b);
    160 		yield unitdiff(a, b, unit::MINUTE) * 60 + full_diff.seconds;
    161 	case unit::NANOSECOND =>
    162 		const full_diff = diff(a, b);
    163 		yield unitdiff(a, b, unit::SECOND) * time::SECOND +
    164 			full_diff.nanoseconds;
    165 	};
    166 };
    167 
    168 // Returns true if two [[period]]s are numerically equal.
    169 export fn period_eq(a: period, b: period) bool = {
    170 	return a.eras == b.eras &&
    171 		a.years == b.years &&
    172 		a.months == b.months &&
    173 		a.weeks == b.weeks &&
    174 		a.days == b.days &&
    175 		a.hours == b.hours &&
    176 		a.minutes == b.minutes &&
    177 		a.seconds == b.seconds &&
    178 		a.nanoseconds == b.nanoseconds;
    179 };
    180 
    181 // Truncates the given [[datetime]] at the provided nominal [[unit]].
    182 //
    183 // For example, truncating to the nearest [[unit::MONTH]] will set the day,
    184 // hour, minute, seconds, and nanoseconds fields to their minimum values.
    185 export fn truncate(dt: datetime, u: unit) datetime = {
    186 	// TODO: Replace all of the 0s for the zoffset with the actual
    187 	// zoffset once the API is solidified a bit
    188 	return switch (u) {
    189 	case unit::ERA =>
    190 		yield new(dt.loc, 0,
    191 			01, 01, 01,
    192 			00, 00, 00, 0,
    193 		)!;
    194 	case unit::YEAR =>
    195 		yield new(dt.loc, 0,
    196 			year(&dt), 01, 01,
    197 			00, 00, 00, 0,
    198 		)!;
    199 	case unit::MONTH =>
    200 		yield new(dt.loc, 0,
    201 			year(&dt), month(&dt), 01,
    202 			00, 00, 00, 0,
    203 		)!;
    204 	case unit::WEEK =>
    205 		const date = dt.date - (weekday(&dt) - 1);
    206 		const ymd = calc_ymd(date);
    207 		yield new(dt.loc, 0,
    208 			ymd.0, ymd.1, ymd.2,
    209 			00, 00, 00, 0,
    210 		)!;
    211 	case unit::DAY =>
    212 		yield new(dt.loc, 0,
    213 			year(&dt), month(&dt), day(&dt),
    214 			00, 00, 00, 0,
    215 		)!;
    216 	case unit::HOUR =>
    217 		yield new(dt.loc, 0,
    218 			year(&dt), month(&dt), day(&dt),
    219 			hour(&dt), 00, 00, 0,
    220 		)!;
    221 	case unit::MINUTE =>
    222 		yield new(dt.loc, 0,
    223 			year(&dt), month(&dt), day(&dt),
    224 			hour(&dt), min(&dt), 00, 0,
    225 		)!;
    226 	case unit::SECOND =>
    227 		yield new(dt.loc, 0,
    228 			year(&dt), month(&dt), day(&dt),
    229 			hour(&dt), min(&dt), sec(&dt), 0,
    230 		)!;
    231 	case unit::NANOSECOND =>
    232 		yield dt;
    233 	};
    234 };
    235 
    236 // Given a [[datetime]] and a [[period]], "hops" to the minimum value of each
    237 // field (years, months, days, etc) plus or minus an offset, and returns a new
    238 // datetime. This can be used, for example, to find the start of last year.
    239 //
    240 // Consults each period's fields from most to least significant (from years to
    241 // nanoseconds).
    242 //
    243 // If a period's field's value N is zero, it's a no-op. Otherwise, hop will
    244 // reckon to the Nth inter-period point from where last reckoned. This repeats
    245 // until all the given period's fields are exhausted.
    246 //
    247 // 	let dt = ... // 1999-05-13 12:30:45
    248 // 	datetime::hop(dt, datetime::period {
    249 // 		years  = 22, // produces 2021-01-01 00:00:00
    250 // 		months = -1, // produces 2020-11-01 00:00:00
    251 // 		days   = -4, // produces 2020-10-27 00:00:00
    252 // 	});
    253 //
    254 export fn hop(dt: datetime, pp: period...) datetime = {
    255 	let new_dt = dt;
    256 	for (let i = 0z; i < len(pp); i += 1) {
    257 		const p = pp[i];
    258 
    259 		if (p.years != 0) {
    260 			const dt_inc = add(new_dt, calculus::DEFAULT,
    261 				period { years = p.years, ... });
    262 			new_dt = truncate(dt_inc, unit::YEAR);
    263 		};
    264 		if (p.months != 0) {
    265 			const dt_inc = add(new_dt, calculus::DEFAULT,
    266 				period { months = p.months, ... });
    267 			new_dt = truncate(dt_inc, unit::MONTH);
    268 		};
    269 		if (p.weeks != 0) {
    270 			const dt_inc = add(new_dt, calculus::DEFAULT,
    271 				period { weeks = p.weeks, ... });
    272 			new_dt = truncate(dt_inc, unit::WEEK);
    273 		};
    274 		if (p.days != 0) {
    275 			const dt_inc = add(new_dt, calculus::DEFAULT,
    276 				period { days = p.days, ... });
    277 			new_dt = truncate(dt_inc, unit::DAY);
    278 		};
    279 		if (p.hours != 0) {
    280 			const dt_inc = add(new_dt, calculus::DEFAULT,
    281 				period { hours = p.hours, ... });
    282 			new_dt = truncate(dt_inc, unit::HOUR);
    283 		};
    284 		if (p.minutes != 0) {
    285 			const dt_inc = add(new_dt, calculus::DEFAULT,
    286 				period { minutes = p.minutes, ... });
    287 			new_dt = truncate(dt_inc, unit::MINUTE);
    288 		};
    289 		if (p.seconds != 0) {
    290 			const dt_inc = add(new_dt, calculus::DEFAULT,
    291 				period { seconds = p.seconds, ... });
    292 			new_dt = truncate(dt_inc, unit::SECOND);
    293 		};
    294 		if (p.nanoseconds != 0) {
    295 			new_dt = add(new_dt, calculus::DEFAULT,
    296 				period { nanoseconds = p.nanoseconds, ... });
    297 		};
    298 	};
    299 	return new_dt;
    300 };
    301 
    302 // Adds a period of time to a datetime, most significant units first. Conserves
    303 // relative distance from cyclical points on the calendar when possible. This
    304 // can be used, for example, to find the date one year from now.
    305 //
    306 // 	let dt = ... // 1999-05-13 12:30:45
    307 // 	datetime::add(dt, datetime::calculus::DEFAULT, datetime::period {
    308 // 		years  = 22, // 2021-05-13 12:30:45
    309 // 		months = -1, // 2021-04-13 12:30:45
    310 // 		days   = -4, // 2020-04-09 12:30:45
    311 // 	});
    312 //
    313 export fn add(dt: datetime, flag: calculus, pp: period...) datetime = {
    314 	// TODO: Use [[builder]] to simplify some code.
    315 	let d_year = year(&dt);
    316 	let d_month = month(&dt);
    317 	let d_day = day(&dt);
    318 	let d_hour = hour(&dt);
    319 	let d_min = min(&dt);
    320 	let d_sec = sec(&dt);
    321 	let d_nsec = ((nsec(&dt)): i64);
    322 	for (let i = 0z; i < len(pp); i += 1) {
    323 		const p = pp[i];
    324 
    325 		let latest_date = dt.date;
    326 
    327 		if (p.years != 0) {
    328 			d_year += p.years;
    329 		};
    330 		if (p.months != 0) {
    331 			d_month += p.months;
    332 		};
    333 		if (d_month > 12) {
    334 			d_year += (d_month - 1) / 12;
    335 			d_month = d_month % 12;
    336 		};
    337 		if (d_month < 1) {
    338 			d_year -= (12 + -(d_month - 1)) / 12;
    339 			d_month = 12 - (-d_month % 12);
    340 		};
    341 		const n_days_in_month = calc_n_days_in_month(d_year, d_month);
    342 		if (d_day > n_days_in_month) {
    343 			d_day = n_days_in_month;
    344 		};
    345 
    346 		if (p.weeks != 0) {
    347 			p.days += p.weeks * 7;
    348 		};
    349 		latest_date = calc_date_from_ymd(
    350 			d_year, d_month, d_day)!;
    351 		if (p.days != 0) {
    352 			const new_ymd = calc_ymd(latest_date + p.days);
    353 			d_year = new_ymd.0;
    354 			d_month = new_ymd.1;
    355 			d_day = new_ymd.2;
    356 			latest_date = calc_date_from_ymd(
    357 				d_year, d_month, d_day)!;
    358 		};
    359 
    360 		if (p.hours != 0) {
    361 			p.nanoseconds += p.hours * time::HOUR;
    362 		};
    363 		if (p.minutes != 0) {
    364 			p.nanoseconds += p.minutes * time::MINUTE;
    365 		};
    366 		if (p.seconds != 0) {
    367 			p.nanoseconds += p.seconds * time::SECOND;
    368 		};
    369 		if (p.nanoseconds != 0) {
    370 			const ns_in_day = 24 * time::HOUR;
    371 			let overflowed_days = 0;
    372 
    373 			if (math::absi(p.nanoseconds): i64 > ns_in_day) {
    374 				overflowed_days +=
    375 					((p.nanoseconds / ns_in_day): int);
    376 				p.nanoseconds %= ns_in_day;
    377 			};
    378 
    379 			let new_time = dt.time + p.nanoseconds;
    380 
    381 			if (new_time >= ns_in_day) {
    382 				overflowed_days += 1;
    383 				new_time -= ns_in_day;
    384 			} else if (new_time < 0) {
    385 				overflowed_days -= 1;
    386 				new_time += ns_in_day;
    387 			};
    388 
    389 			if (overflowed_days != 0) {
    390 				const new_date = latest_date +
    391 					overflowed_days;
    392 				const new_ymd = calc_ymd(new_date);
    393 				d_year = new_ymd.0;
    394 				d_month = new_ymd.1;
    395 				d_day = new_ymd.2;
    396 			};
    397 			const new_hmsn = calc_hmsn(new_time);
    398 			d_hour = new_hmsn.0;
    399 			d_min = new_hmsn.1;
    400 			d_sec = new_hmsn.2;
    401 			d_nsec = new_hmsn.3;
    402 		};
    403 	};
    404 	// TODO: Add zoffset back in here once API is settled
    405 	return new(dt.loc, 0,
    406 		d_year, d_month, d_day, d_hour, d_min, d_sec, d_nsec: int,
    407 	)!;
    408 };
    409 
    410 // Subtracts a calendrical period of time to a datetime, most significant units
    411 // first. Conserves relative distance from cyclical points on the calendar when
    412 // possible.
    413 //
    414 // 	let dt = ... // 1999-05-13 12:30:45
    415 // 	datetime::subtract(dt, datetime::calculus::DEFAULT, datetime::period {
    416 // 		years  = 22, // 1977-05-13 12:30:45
    417 // 		months = -1, // 1977-06-13 12:30:45
    418 // 		days   = -4, // 1977-06-17 12:30:45
    419 // 	});
    420 //
    421 export fn sub(dt: datetime, flag: calculus, pp: period...) datetime = {
    422 	for (let i = 0z; i < len(pp); i += 1) {
    423 		pp[i].eras *= -1;
    424 		pp[i].years *= -1;
    425 		pp[i].months *= -1;
    426 		pp[i].weeks *= -1;
    427 		pp[i].days *= -1;
    428 		pp[i].minutes *= -1;
    429 		pp[i].seconds *= -1;
    430 		pp[i].nanoseconds *= -1;
    431 	};
    432 	return add(dt, flag, pp...);
    433 };
    434 
    435 @test fn eq() void = {
    436 	const dt = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
    437 	const cases = [
    438 		((-768, 01, 01, 03, 14, 07, 0), false),
    439 		((1, 1, 01, 14, 00, 00, 1234), false),
    440 		((2022, 02, 04, 03, 14, 07, 0), true),
    441 		((2022, 02, 04, 03, 14, 07, 1), false),
    442 		((2038, 01, 19, 03, 14, 07, 0), false),
    443 		((5555, 05, 05, 05, 55, 55, 5555), false),
    444 	];
    445 	for (let i = 0z; i < len(cases); i += 1) {
    446 		const c = cases[i].0;
    447 		const expected = cases[i].1;
    448 		const case_dt = new(chrono::UTC, 0,
    449 			c.0, c.1, c.2, c.3, c.4, c.5, c.6)!;
    450 		assert(eq(dt, case_dt) == expected,
    451 			"equality comparison failed");
    452 	};
    453 };
    454 
    455 @test fn after() void = {
    456 	const dt = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
    457 	const cases = [
    458 		((-768, 01, 01, 03, 14, 07, 0), false),
    459 		((1, 1, 01, 14, 00, 00, 1234), false),
    460 		((2020, 02, 04, 03, 14, 07, 1), false),
    461 		((2022, 02, 04, 03, 14, 07, 0), false),
    462 		((2022, 02, 04, 04, 01, 01, 0), true),
    463 		((2038, 01, 19, 03, 14, 07, 0), true),
    464 		((5555, 05, 05, 05, 55, 55, 5555), true),
    465 	];
    466 	for (let i = 0z; i < len(cases); i += 1) {
    467 		const c = cases[i].0;
    468 		const expected = cases[i].1;
    469 		const case_dt = new(chrono::UTC, 0,
    470 			c.0, c.1, c.2, c.3, c.4, c.5, c.6)!;
    471 		assert(after(case_dt, dt) == expected,
    472 			"incorrect date ordering in after()");
    473 	};
    474 };
    475 
    476 @test fn before() void = {
    477 	const dt = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
    478 	const cases = [
    479 		((-768, 01, 01, 03, 14, 07, 0), true),
    480 		((1, 1, 01, 14, 00, 00, 1234), true),
    481 		((2020, 02, 04, 03, 14, 07, 1), true),
    482 		((2022, 02, 04, 03, 14, 07, 0), false),
    483 		((2022, 02, 04, 04, 01, 01, 0), false),
    484 		((2038, 01, 19, 03, 14, 07, 0), false),
    485 		((5555, 05, 05, 05, 55, 55, 5555), false),
    486 	];
    487 	for (let i = 0z; i < len(cases); i += 1) {
    488 		const c = cases[i].0;
    489 		const expected = cases[i].1;
    490 		const case_dt = new(chrono::UTC, 0,
    491 			c.0, c.1, c.2, c.3, c.4, c.5, c.6)!;
    492 		assert(before(case_dt, dt) == expected,
    493 			"incorrect date ordering in before()");
    494 	};
    495 };
    496 
    497 @test fn diff() void = {
    498 	const cases = [
    499 		(
    500 			new(chrono::UTC, 0, 2021, 01, 15, 00, 00, 00, 0)!,
    501 			new(chrono::UTC, 0, 2022, 02, 16, 00, 00, 00, 0)!,
    502 			period {
    503 				years = 1,
    504 				months = 1,
    505 				days = 1,
    506 				...
    507 			},
    508 		),
    509 		(
    510 			new(chrono::UTC, 0, 2021, 01, 15, 00, 00, 00, 0)!,
    511 			new(chrono::UTC, 0, 2022, 03, 27, 00, 00, 00, 0)!,
    512 			period {
    513 				years = 1,
    514 				months = 2,
    515 				days = 12,
    516 				...
    517 			},
    518 		),
    519 		(
    520 			new(chrono::UTC, 0, 2021, 01, 15, 00, 00, 00, 0)!,
    521 			new(chrono::UTC, 0, 2022, 03, 14, 00, 00, 00, 0)!,
    522 			period {
    523 				years = 1,
    524 				months = 1,
    525 				days = 27,
    526 				...
    527 			},
    528 		),
    529 		(
    530 			new(chrono::UTC, 0, 2021, 01, 15, 00, 00, 00, 0)!,
    531 			new(chrono::UTC, 0, 2021, 01, 16, 00, 00, 00, 0)!,
    532 			period {
    533 				days = 1,
    534 				...
    535 			},
    536 		),
    537 		(
    538 			new(chrono::UTC, 0, 2021, 01, 15, 00, 00, 00, 0)!,
    539 			new(chrono::UTC, 0, 2021, 01, 16, 01, 03, 02, 4)!,
    540 			period {
    541 				days = 1,
    542 				hours = 1,
    543 				minutes = 3,
    544 				seconds = 2,
    545 				nanoseconds = 4,
    546 				...
    547 			},
    548 		),
    549 		(
    550 			new(chrono::UTC, 0, 2021, 01, 15, 02, 03, 02, 2)!,
    551 			new(chrono::UTC, 0, 2021, 01, 16, 01, 01, 02, 4)!,
    552 			period {
    553 				hours = 22,
    554 				minutes = 58,
    555 				nanoseconds = 2,
    556 				...
    557 			},
    558 		),
    559 		(
    560 			new(chrono::UTC, 0, 0500, 01, 01, 00, 00, 00, 0)!,
    561 			new(chrono::UTC, 0, 3500, 01, 01, 00, 06, 00, 0)!,
    562 			period {
    563 				years = 3000,
    564 				minutes = 6,
    565 				...
    566 			},
    567 		),
    568 		(
    569 			new(chrono::UTC, 0, -500, 01, 01, 00, 00, 00, 0)!,
    570 			new(chrono::UTC, 0, 2500, 01, 01, 00, 06, 00, 0)!,
    571 			period {
    572 				years = 3000,
    573 				minutes = 6,
    574 				...
    575 			},
    576 		),
    577 		(
    578 			new(chrono::UTC, 0, 2000, 01, 01, 00, 00, 00, 0)!,
    579 			new(chrono::UTC, 0, 2000, 01, 01, 00, 06, 00, 999999999)!,
    580 			period {
    581 				minutes = 6,
    582 				nanoseconds = 999999999,
    583 				...
    584 			},
    585 		),
    586 		(
    587 			new(chrono::UTC, 0, 2000, 01, 01, 00, 06, 00, 999999999)!,
    588 			new(chrono::UTC, 0, 2000, 01, 01, 00, 06, 01, 0)!,
    589 			period {
    590 				nanoseconds = 1,
    591 				...
    592 			},
    593 		),
    594 		(
    595 			new(chrono::UTC, 0, -4000, 01, 01, 00, 06, 00, 999999999)!,
    596 			new(chrono::UTC, 0, 4000, 01, 01, 00, 06, 01, 0)!,
    597 			period {
    598 				years = 8000,
    599 				nanoseconds = 1,
    600 				...
    601 			},
    602 		),
    603 	];
    604 	for (let i = 0z; i < len(cases); i += 1) {
    605 		const dta = cases[i].0;
    606 		const dtb = cases[i].1;
    607 		const expected = cases[i].2;
    608 		const actual = diff(dta, dtb);
    609 		assert(period_eq(actual, expected), "diff miscalculation");
    610 	};
    611 };
    612 
    613 @test fn unitdiff() void = {
    614 	const cases = [
    615 		(
    616 			new(chrono::UTC, 0, 1994, 08, 27, 11, 20, 01, 2)!,
    617 			new(chrono::UTC, 0, 2022, 01, 05, 13, 53, 30, 20)!,
    618 			(27, 328, 1427, 9993, 239834, 14390073, 863404409i64,
    619 				(863404409i64 * time::SECOND) + 18),
    620 		),
    621 		(
    622 			new(chrono::UTC, 0, 1994, 08, 28, 11, 20, 01, 2)!,
    623 			new(chrono::UTC, 0, 1994, 08, 27, 11, 20, 01, 0)!,
    624 			(0, 0, 0, 1, 24, 1440, 86400i64,
    625 				(86400i64 * time::SECOND) + 2),
    626 		),
    627 		(
    628 			new(chrono::UTC, 0, 1994, 08, 27, 11, 20, 01, 0)!,
    629 			new(chrono::UTC, 0, 1994, 08, 27, 11, 20, 01, 0)!,
    630 			(0, 0, 0, 0, 0, 0, 0i64, 0i64),
    631 		),
    632 		(
    633 			new(chrono::UTC, 0, -500, 01, 01, 00, 59, 01, 0)!,
    634 			new(chrono::UTC, 0, 2000, 01, 01, 23, 01, 01, 0)!,
    635 			(2500, 30000, 130443, 913106, 913106 * 24 + 22,
    636 				(913106 * 24 + 22) * 60 + 2,
    637 				((913106 * 24 + 22) * 60 + 2) * 60i64,
    638 				(((913106 * 24 + 22) * 60 + 2) * 60i64 *
    639 					time::SECOND)),
    640 		),
    641 	];
    642 	for (let i = 0z; i < len(cases); i += 1) {
    643 		const dta = cases[i].0;
    644 		const dtb = cases[i].1;
    645 		const expected = cases[i].2;
    646 		assert(unitdiff(dtb, dta, unit::YEAR) == expected.0,
    647 			"invalid diff_in_years() result");
    648 		assert(unitdiff(dtb, dta, unit::MONTH) == expected.1,
    649 			"invalid diff_in_months() result");
    650 		assert(unitdiff(dtb, dta, unit::WEEK) == expected.2,
    651 			"invalid diff_in_weeks() result");
    652 		assert(unitdiff(dtb, dta, unit::DAY) == expected.3,
    653 			"invalid diff_in_days() result");
    654 		assert(unitdiff(dtb, dta, unit::HOUR) == expected.4,
    655 			"invalid diff_in_hours() result");
    656 		assert(unitdiff(dtb, dta, unit::MINUTE) == expected.5,
    657 			"invalid diff_in_minutes() result");
    658 		assert(unitdiff(dtb, dta, unit::SECOND) == expected.6,
    659 			"invalid diff_in_seconds() result");
    660 		assert(unitdiff(dtb, dta, unit::NANOSECOND) == expected.7,
    661 			"invalid diff_in_nanoseconds() result");
    662 	};
    663 };
    664 
    665 @test fn truncate() void = {
    666 	const dt = new(chrono::UTC, 0, 1994, 08, 27, 11, 20, 01, 2)!;
    667 	assert(eq(truncate(dt, unit::ERA),
    668 			new(chrono::UTC, 0, 01, 01, 01, 00, 00, 00, 0)!),
    669 		"invalid truncate() result");
    670 	assert(eq(truncate(dt, unit::YEAR),
    671 			new(chrono::UTC, 0, 1994, 01, 01, 00, 00, 00, 0)!),
    672 		"invalid truncate() result");
    673 	assert(eq(truncate(dt, unit::MONTH),
    674 			new(chrono::UTC, 0, 1994, 08, 01, 00, 00, 00, 0)!),
    675 		"invalid truncate() result");
    676 	assert(eq(truncate(dt, unit::WEEK),
    677 			new(chrono::UTC, 0, 1994, 08, 22, 00, 00, 00, 0)!),
    678 		"invalid truncate() result");
    679 	assert(eq(truncate(dt, unit::DAY),
    680 			new(chrono::UTC, 0, 1994, 08, 27, 00, 00, 00, 0)!),
    681 		"invalid truncate() result");
    682 	assert(eq(truncate(dt, unit::HOUR),
    683 			new(chrono::UTC, 0, 1994, 08, 27, 11, 00, 00, 0)!),
    684 		"invalid truncate() result");
    685 	assert(eq(truncate(dt, unit::MINUTE),
    686 			new(chrono::UTC, 0, 1994, 08, 27, 11, 20, 00, 0)!),
    687 		"invalid truncate() result");
    688 	assert(eq(truncate(dt, unit::SECOND),
    689 			new(chrono::UTC, 0, 1994, 08, 27, 11, 20, 01, 0)!),
    690 		"invalid truncate() result");
    691 	assert(eq(truncate(dt, unit::NANOSECOND), dt),
    692 		"invalid truncate() result");
    693 };
    694 
    695 @test fn add() void = {
    696 	const d = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
    697 	const cases = [
    698 		(
    699 			period { years = 1, ... },
    700 			new(chrono::UTC, 0, 2023, 02, 04, 03, 14, 07, 0)!,
    701 		),
    702 		(
    703 			period { years = -23, ... },
    704 			new(chrono::UTC, 0, 1999, 02, 04, 03, 14, 07, 0)!,
    705 		),
    706 		(
    707 			period { months = 2, ... },
    708 			new(chrono::UTC, 0, 2022, 04, 04, 03, 14, 07, 0)!,
    709 		),
    710 		(
    711 			period { months = 11, ... },
    712 			new(chrono::UTC, 0, 2023, 01, 04, 03, 14, 07, 0)!,
    713 		),
    714 		(
    715 			period { months = -1, ... },
    716 			new(chrono::UTC, 0, 2022, 01, 04, 03, 14, 07, 0)!,
    717 		),
    718 		(
    719 			period { months = -2, ... },
    720 			new(chrono::UTC, 0, 2021, 12, 04, 03, 14, 07, 0)!,
    721 		),
    722 		(
    723 			period { days = 3, ... },
    724 			new(chrono::UTC, 0, 2022, 02, 07, 03, 14, 07, 0)!,
    725 		),
    726 		(
    727 			period { days = 33, ... },
    728 			new(chrono::UTC, 0, 2022, 03, 09, 03, 14, 07, 0)!,
    729 		),
    730 		(
    731 			period { days = 333, ... },
    732 			new(chrono::UTC, 0, 2023, 01, 03, 03, 14, 07, 0)!,
    733 		),
    734 		(
    735 			period { days = -2, ... },
    736 			new(chrono::UTC, 0, 2022, 02, 02, 03, 14, 07, 0)!,
    737 		),
    738 		(
    739 			period { days = -4, ... },
    740 			new(chrono::UTC, 0, 2022, 01, 31, 03, 14, 07, 0)!,
    741 		),
    742 		(
    743 			period { days = -1337, ... },
    744 			new(chrono::UTC, 0, 2018, 06, 08, 03, 14, 07, 0)!,
    745 		),
    746 		(
    747 			period { hours = 1, ... },
    748 			new(chrono::UTC, 0, 2022, 02, 04, 04, 14, 07, 0)!,
    749 		),
    750 		(
    751 			period { hours = 24, ... },
    752 			new(chrono::UTC, 0, 2022, 02, 05, 03, 14, 07, 0)!,
    753 		),
    754 		(
    755 			period { hours = 25, ... },
    756 			new(chrono::UTC, 0, 2022, 02, 05, 04, 14, 07, 0)!,
    757 		),
    758 		(
    759 			period { hours = 123456, ... },
    760 			new(chrono::UTC, 0, 2036, 03, 06, 03, 14, 07, 0)!,
    761 		),
    762 		(
    763 			period { hours = -2, ... },
    764 			new(chrono::UTC, 0, 2022, 02, 04, 01, 14, 07, 0)!,
    765 		),
    766 		(
    767 			period { hours = -24, ... },
    768 			new(chrono::UTC, 0, 2022, 02, 03, 03, 14, 07, 0)!,
    769 		),
    770 		(
    771 			period { hours = -123456, ... },
    772 			new(chrono::UTC, 0, 2008, 01, 05, 03, 14, 07, 0)!,
    773 		),
    774 		(
    775 			period { seconds = 2, ... },
    776 			new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 09, 0)!,
    777 		),
    778 		(
    779 			period { seconds = 666666666, ... },
    780 			new(chrono::UTC, 0, 2043, 03, 22, 04, 25, 13, 0)!,
    781 		),
    782 		(
    783 			period { seconds = -2, ... },
    784 			new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 05, 0)!,
    785 		),
    786 		(
    787 			period { seconds = -666666666, ... },
    788 			new(chrono::UTC, 0, 2000, 12, 20, 02, 03, 01, 0)!,
    789 		),
    790 		(
    791 			period { nanoseconds = 123, ... },
    792 			new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 123)!,
    793 		),
    794 		(
    795 			period { nanoseconds = 1361661361461, ... },
    796 			new(chrono::UTC, 0, 2022, 02, 04, 03, 36, 48, 661361461)!,
    797 		),
    798 		(
    799 			period { nanoseconds = -1361661361461, ... },
    800 			new(chrono::UTC, 0, 2022, 02, 04, 02, 51, 25, 338638539)!,
    801 		),
    802 		(
    803 			period { months = 1, seconds = -666666666, ... },
    804 			new(chrono::UTC, 0, 2001, 01, 17, 02, 03, 01, 0)!,
    805 		),
    806 		(
    807 			period { months = 1, seconds = -666666666, ... },
    808 			new(chrono::UTC, 0, 2001, 01, 17, 02, 03, 01, 0)!,
    809 		),
    810 		(
    811 			period {
    812 				years = -1,
    813 				months = -2,
    814 				weeks = -3,
    815 				days = -4,
    816 				hours = -5,
    817 				minutes = -6,
    818 				seconds = -7,
    819 				nanoseconds = -8,
    820 				...
    821 			},
    822 			new(chrono::UTC, 0, 2020, 11, 08, 22, 07, 59, 999999992)!,
    823 		),
    824 		(
    825 			period {
    826 				years = 1,
    827 				months = 2,
    828 				weeks = 3,
    829 				days = 4,
    830 				hours = 5,
    831 				minutes = 6,
    832 				seconds = 7,
    833 				nanoseconds = 8,
    834 				...
    835 			},
    836 			new(chrono::UTC, 0, 2023, 04, 29, 08, 20, 14, 8)!,
    837 		),
    838 		(
    839 			period {
    840 				years = 1,
    841 				months = -2,
    842 				weeks = 3,
    843 				days = -5,
    844 				hours = 8,
    845 				minutes = -13,
    846 				seconds = 21,
    847 				nanoseconds = -34,
    848 				...
    849 			},
    850 			new(chrono::UTC, 0, 2022, 12, 20, 11, 01, 27, 999999966)!,
    851 		),
    852 		(
    853 			period {
    854 				years = -1,
    855 				months = 12,
    856 				weeks = -52,
    857 				days = -31,
    858 				hours = 24,
    859 				minutes = -3600,
    860 				seconds = 3600,
    861 				nanoseconds = -86400000000000,
    862 				...
    863 			},
    864 			new(chrono::UTC, 0, 2021, 01, 02, 16, 14, 07, 0)!,
    865 		),
    866 	];
    867 	for (let i = 0z; i < len(cases); i += 1) {
    868 		const p = cases[i].0;
    869 		const expected = cases[i].1;
    870 		const actual = add(d, calculus::DEFAULT, p);
    871 		assert(eq(actual, expected), "addition miscalculation");
    872 	};
    873 };
    874 
    875 @test fn sub() void = {
    876 	const d = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
    877 	const cases = [
    878 		(
    879 			period { years = 1, ... },
    880 			new(chrono::UTC, 0, 2021, 02, 04, 03, 14, 07, 0)!,
    881 		),
    882 		(
    883 			period { months = 2, ... },
    884 			new(chrono::UTC, 0, 2021, 12, 04, 03, 14, 07, 0)!,
    885 		),
    886 		(
    887 			period { months = 14, ... },
    888 			new(chrono::UTC, 0, 2020, 12, 04, 03, 14, 07, 0)!,
    889 		),
    890 	];
    891 	for (let i = 0z; i < len(cases); i += 1) {
    892 		const p = cases[i].0;
    893 		const expected = cases[i].1;
    894 		const actual = sub(d, calculus::DEFAULT, p);
    895 		assert(eq(actual, expected), "subtraction miscalculation");
    896 	};
    897 };