hare

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

arithmetic.ha (9976B)


      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 // The nominal units of the Gregorian chronology. Used for chronological
     11 // arithmetic.
     12 export type unit = enum int {
     13 	ERA,
     14 	YEAR,
     15 	MONTH,
     16 	WEEK,
     17 	DAY,
     18 	HOUR,
     19 	MINUTE,
     20 	SECOND,
     21 	NANOSECOND,
     22 };
     23 
     24 // Calculates the [[period]] between two [[datetime]]s, from A to B.
     25 // The returned period, provided to [[reckon]] along with A, will produce B,
     26 // regardless of the [[calculus]] used. All the period's non-zero fields will
     27 // have the same sign.
     28 export fn pdiff(a: datetime, b: datetime) period = {
     29 	let p = period { ... };
     30 
     31 	if (chrono::compare(&a, &b) == 0) {
     32 		return p;
     33 	};
     34 
     35 	let reverse = if (chrono::compare(&a, &b) > 0) true else false;
     36 	if (reverse) {
     37 		let tmp = a;
     38 		a = b;
     39 		b = tmp;
     40 	};
     41 
     42 	p.years = _year(&b) - _year(&a);
     43 
     44 	p.months = _month(&b) - _month(&a);
     45 	if (p.months < 0) {
     46 		p.years -= 1;
     47 		p.months += 12;
     48 	};
     49 
     50 	p.days = _day(&b) - _day(&a);
     51 	let year = _year(&b);
     52 	let month = _month(&b);
     53 	let daycnt = calc_month_daycnt(year, month);
     54 	for (_day(&a) > daycnt || p.days < 0) {
     55 		month -= 1;
     56 		if (month == 0) {
     57 			year -= 1;
     58 			month = 12;
     59 		};
     60 		daycnt = calc_month_daycnt(year, month);
     61 
     62 		p.months -= 1;
     63 		if (p.months < 0) {
     64 			p.years -= 1;
     65 			p.months += 12;
     66 		};
     67 		p.days += daycnt;
     68 	};
     69 
     70 	p.hours = _hour(&b) - _hour(&a);
     71 	if (p.hours < 0) {
     72 		p.days -= 1;
     73 		p.hours += 24;
     74 	};
     75 
     76 	p.minutes = _minute(&b) - _minute(&a);
     77 	if (p.minutes < 0) {
     78 		p.hours -= 1;
     79 		p.minutes += 60;
     80 	};
     81 
     82 	p.seconds = _second(&b) - _second(&a);
     83 	if (p.seconds < 0) {
     84 		p.minutes -= 1;
     85 		p.seconds += 60;
     86 	};
     87 
     88 	p.nanoseconds = _nanosecond(&b) - _nanosecond(&a);
     89 	if (p.nanoseconds < 0) {
     90 		p.seconds -= 1;
     91 		p.nanoseconds += 1000000000; // 10E9
     92 	};
     93 
     94 	return if (reverse) neg(p) else p;
     95 };
     96 
     97 // Calculates the nominal [[unit]] difference between two [[datetime]]s.
     98 export fn unitdiff(a: datetime, b: datetime, u: unit) i64 = {
     99 	switch (u) {
    100 	case unit::ERA =>
    101 		return era(&b) - era(&a);
    102 	case unit::YEAR =>
    103 		return pdiff(a, b).years;
    104 	case unit::MONTH =>
    105 		const d = pdiff(a, b);
    106 		return d.years * 12 + d.months;
    107 	case unit::WEEK =>
    108 		return unitdiff(a, b, unit::DAY) / 7;
    109 	case unit::DAY =>
    110 		return chrono::date(&b) - chrono::date(&a);
    111 	case unit::HOUR =>
    112 		return unitdiff(a, b, unit::DAY) * 24 + pdiff(a, b).hours;
    113 	case unit::MINUTE =>
    114 		return unitdiff(a, b, unit::HOUR) * 60 + pdiff(a, b).minutes;
    115 	case unit::SECOND =>
    116 		return unitdiff(a, b, unit::MINUTE) * 60 + pdiff(a, b).seconds;
    117 	case unit::NANOSECOND =>
    118 		return unitdiff(a, b, unit::SECOND) * 1000000000 + pdiff(a, b).nanoseconds;
    119 	};
    120 };
    121 
    122 // Truncates the given [[datetime]] at the provided nominal [[unit]].
    123 //
    124 // For example, truncating to the nearest unit::MONTH will set the 'day',
    125 // 'hour', 'minute', 'second', and 'nanosecond' fields to their minimum values.
    126 export fn truncate(dt: datetime, u: unit) datetime = {
    127 	// TODO: There exist timezones where midnight is invalid on certain
    128 	// days. The new()! calls will fail, but we probably don't want to '?'
    129 	// propagate [[invalid]] to keep this function's use simple. The minimum
    130 	// values (the zeroes and ones here) can't be hardcoded. They need
    131 	// calculation. We should either handle this here; or probably in
    132 	// realize(), and then use realize() here.
    133 	return switch (u) {
    134 	case unit::ERA =>
    135 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    136 			1, 1, 1,
    137 			0, 0, 0, 0,
    138 		)!;
    139 	case unit::YEAR =>
    140 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    141 			_year(&dt), 1, 1,
    142 			0, 0, 0, 0,
    143 		)!;
    144 	case unit::MONTH =>
    145 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    146 			_year(&dt), _month(&dt), 1,
    147 			0, 0, 0, 0,
    148 		)!;
    149 	case unit::WEEK =>
    150 		const date = chrono::date(&dt) - _weekday(&dt);
    151 		const ymd = calc_ymd(date);
    152 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    153 			ymd.0, ymd.1, ymd.2,
    154 			0, 0, 0, 0,
    155 		)!;
    156 	case unit::DAY =>
    157 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    158 			_year(&dt), _month(&dt), _day(&dt),
    159 			0, 0, 0, 0,
    160 		)!;
    161 	case unit::HOUR =>
    162 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    163 			_year(&dt), _month(&dt), _day(&dt),
    164 			_hour(&dt), 0, 0, 0,
    165 		)!;
    166 	case unit::MINUTE =>
    167 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    168 			_year(&dt), _month(&dt), _day(&dt),
    169 			_hour(&dt), _minute(&dt), 0, 0,
    170 		)!;
    171 	case unit::SECOND =>
    172 		yield new(dt.loc, chrono::mzone(&dt).zoff,
    173 			_year(&dt), _month(&dt), _day(&dt),
    174 			_hour(&dt), _minute(&dt), _second(&dt), 0,
    175 		)!;
    176 	case unit::NANOSECOND =>
    177 		yield dt;
    178 	};
    179 };
    180 
    181 @test fn pdiff() void = {
    182 	const cases = [
    183 		(
    184 			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
    185 			new(chrono::UTC, 0, 2022, 2, 16, 0, 0, 0, 0)!,
    186 			period {
    187 				years = 1,
    188 				months = 1,
    189 				days = 1,
    190 				...
    191 			},
    192 		),
    193 		(
    194 			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
    195 			new(chrono::UTC, 0, 2022, 3, 27, 0, 0, 0, 0)!,
    196 			period {
    197 				years = 1,
    198 				months = 2,
    199 				days = 12,
    200 				...
    201 			},
    202 		),
    203 		(
    204 			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
    205 			new(chrono::UTC, 0, 2022, 3, 14, 0, 0, 0, 0)!,
    206 			period {
    207 				years = 1,
    208 				months = 1,
    209 				days = 27,
    210 				...
    211 			},
    212 		),
    213 		(
    214 			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
    215 			new(chrono::UTC, 0, 2021, 1, 16, 0, 0, 0, 0)!,
    216 			period {
    217 				days = 1,
    218 				...
    219 			},
    220 		),
    221 		(
    222 			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
    223 			new(chrono::UTC, 0, 2021, 1, 16, 1, 3, 2, 4)!,
    224 			period {
    225 				days = 1,
    226 				hours = 1,
    227 				minutes = 3,
    228 				seconds = 2,
    229 				nanoseconds = 4,
    230 				...
    231 			},
    232 		),
    233 		(
    234 			new(chrono::UTC, 0, 2021, 1, 15, 2, 3, 2, 2)!,
    235 			new(chrono::UTC, 0, 2021, 1, 16, 1, 1, 2, 4)!,
    236 			period {
    237 				hours = 22,
    238 				minutes = 58,
    239 				nanoseconds = 2,
    240 				...
    241 			},
    242 		),
    243 		(
    244 			new(chrono::UTC, 0,  500, 1, 1, 0, 0, 0, 0)!,
    245 			new(chrono::UTC, 0, 3500, 1, 1, 0, 6, 0, 0)!,
    246 			period {
    247 				years = 3000,
    248 				minutes = 6,
    249 				...
    250 			},
    251 		),
    252 		(
    253 			new(chrono::UTC, 0, -500, 1, 1, 0, 0, 0, 0)!,
    254 			new(chrono::UTC, 0, 2500, 1, 1, 0, 6, 0, 0)!,
    255 			period {
    256 				years = 3000,
    257 				minutes = 6,
    258 				...
    259 			},
    260 		),
    261 		(
    262 			new(chrono::UTC, 0, 2000, 1, 1, 0, 0, 0, 0)!,
    263 			new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!,
    264 			period {
    265 				minutes = 6,
    266 				nanoseconds = 999999999,
    267 				...
    268 			},
    269 		),
    270 		(
    271 			new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!,
    272 			new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 1, 0)!,
    273 			period {
    274 				nanoseconds = 1,
    275 				...
    276 			},
    277 		),
    278 		(
    279 			new(chrono::UTC, 0, -4000, 1, 1, 0, 6, 0, 999999999)!,
    280 			new(chrono::UTC, 0, 4000,  1, 1, 0, 6, 1, 0)!,
    281 			period {
    282 				years = 8000,
    283 				nanoseconds = 1,
    284 				...
    285 			},
    286 		),
    287 	];
    288 	for (let i = 0z; i < len(cases); i += 1) {
    289 		const dta = cases[i].0;
    290 		const dtb = cases[i].1;
    291 		const expected = cases[i].2;
    292 		const actual = pdiff(dta, dtb);
    293 		assert(peq(actual, expected), "pdiff miscalculation");
    294 	};
    295 };
    296 
    297 @test fn unitdiff() void = {
    298 	const cases = [
    299 		(
    300 			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         2)!,
    301 			new(chrono::UTC, 0,  2022,  1,  5,  13, 53, 30,        20)!,
    302 			(27, 328, 1427, 9993, 239834, 14390073, 863404409i64,
    303 				(863404409i64 * time::SECOND) + 18),
    304 		),
    305 		(
    306 			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         0)!,
    307 			new(chrono::UTC, 0,  1994,  8, 28,  11, 20,  1,         2)!,
    308 			(0, 0, 0, 1, 24, 1440, 86400i64,
    309 				(86400i64 * time::SECOND) + 2),
    310 		),
    311 		(
    312 			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         0)!,
    313 			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         0)!,
    314 			(0, 0, 0, 0, 0, 0, 0i64, 0i64),
    315 		),
    316 		(
    317 			new(chrono::UTC, 0,  -500,  1,  1,   0, 59,  1,         0)!,
    318 			new(chrono::UTC, 0,  2000,  1,  1,  23,  1,  1,         0)!,
    319 			(2500, 30000, 130443, 913106, 913106 * 24 + 22,
    320 				(913106 * 24 + 22) * 60 + 2,
    321 				((913106 * 24 + 22) * 60 + 2) * 60i64,
    322 				(((913106 * 24 + 22) * 60 + 2) * 60i64 *
    323 					time::SECOND)),
    324 		),
    325 	];
    326 	for (let i = 0z; i < len(cases); i += 1) {
    327 		const dta = cases[i].0;
    328 		const dtb = cases[i].1;
    329 		const expected = cases[i].2;
    330 		assert(unitdiff(dta, dtb, unit::YEAR) == expected.0,
    331 			"invalid diff_in_years() result");
    332 		assert(unitdiff(dta, dtb, unit::MONTH) == expected.1,
    333 			"invalid diff_in_months() result");
    334 		assert(unitdiff(dta, dtb, unit::WEEK) == expected.2,
    335 			"invalid diff_in_weeks() result");
    336 		assert(unitdiff(dta, dtb, unit::DAY) == expected.3,
    337 			"invalid diff_in_days() result");
    338 		assert(unitdiff(dta, dtb, unit::HOUR) == expected.4,
    339 			"invalid diff_in_hours() result");
    340 		assert(unitdiff(dta, dtb, unit::MINUTE) == expected.5,
    341 			"invalid diff_in_minutes() result");
    342 		assert(unitdiff(dta, dtb, unit::SECOND) == expected.6,
    343 			"invalid diff_in_seconds() result");
    344 		assert(unitdiff(dta, dtb, unit::NANOSECOND) == expected.7,
    345 			"invalid diff_in_nanoseconds() result");
    346 	};
    347 };
    348 
    349 @test fn truncate() void = {
    350 	const dt = new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!;
    351 
    352 	assert(chrono::eq(
    353 			&truncate(dt, unit::ERA),
    354 			&new(chrono::UTC, 0, 1, 1, 1, 0, 0, 0, 0)!)!,
    355 		"invalid truncate() result 01");
    356 
    357 	assert(chrono::eq(
    358 			&truncate(dt, unit::YEAR),
    359 			&new(chrono::UTC, 0, 1994, 1, 1, 0, 0, 0, 0)!)!,
    360 		"invalid truncate() result 02");
    361 
    362 	assert(chrono::eq(
    363 			&truncate(dt, unit::MONTH),
    364 			&new(chrono::UTC, 0, 1994, 8, 1, 0, 0, 0, 0)!)!,
    365 		"invalid truncate() result 03");
    366 
    367 	assert(chrono::eq(
    368 			&truncate(dt, unit::WEEK),
    369 			&new(chrono::UTC, 0, 1994, 8, 22, 0, 0, 0, 0)!)!,
    370 		"invalid truncate() result 04");
    371 
    372 	assert(chrono::eq(
    373 			&truncate(dt, unit::DAY),
    374 			&new(chrono::UTC, 0, 1994, 8, 27, 0, 0, 0, 0)!)!,
    375 		"invalid truncate() result 05");
    376 
    377 	assert(chrono::eq(
    378 			&truncate(dt, unit::HOUR),
    379 			&new(chrono::UTC, 0, 1994, 8, 27, 11, 0, 0, 0)!)!,
    380 		"invalid truncate() result 06");
    381 
    382 	assert(chrono::eq(
    383 			&truncate(dt, unit::MINUTE),
    384 			&new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 0, 0)!)!,
    385 		"invalid truncate() result 07");
    386 
    387 	assert(chrono::eq(
    388 			&truncate(dt, unit::SECOND),
    389 			&new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!)!,
    390 		"invalid truncate() result 08");
    391 
    392 	assert(chrono::eq(
    393 			&truncate(dt, unit::NANOSECOND),
    394 			&dt)!,
    395 		"invalid truncate() result 09");
    396 };