hare

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

datetime.ha (9399B)


      1 // License: MPL-2.0
      2 // (c) 2021-2022 Byron Torres <b@torresjrjr.com>
      3 // (c) 2022 Drew DeVault <sir@cmpwn.com>
      4 use errors;
      5 use time;
      6 use time::chrono;
      7 
      8 // Invalid [[datetime]].
      9 export type invalid = !chrono::invalid;
     10 
     11 // A date/time object; a [[time::chrono::moment]] wrapper optimized for the
     12 // Gregorian chronology
     13 //
     14 // It is by extension a [[time::instant]] wrapper, and carries information about
     15 // its [[time::chrono::timescale]] and [[time::chrono::locality]].
     16 //
     17 // This object should be treated as private and immutable. Directly mutating its
     18 // fields causes undefined behaviour when used with module functions. Likewise,
     19 // interrogating the fields' type and value (e.g. using match statements) is
     20 // also improper.
     21 //
     22 // A datetime observes various chronological values, cached in its fields. To
     23 // evaluate and obtain these values, use the various "observe" functions
     24 // ([[year]], [[day]], etc.). These values are derived from the embedded moment
     25 // information, and thus are guaranteed to be valid.
     26 //
     27 // See [[virtual]] for an public, mutable, intermediary representation of a
     28 // datetime, which waives guarantees of validity.
     29 export type datetime = struct {
     30 	chrono::moment,
     31 
     32 	era:         (void | int),
     33 	year:        (void | int),
     34 	month:       (void | int),
     35 	day:         (void | int),
     36 	yearday:     (void | int),
     37 	isoweekyear: (void | int),
     38 	isoweek:     (void | int),
     39 	week:        (void | int),
     40 	sundayweek:  (void | int),
     41 	weekday:     (void | int),
     42 
     43 	hour:        (void | int),
     44 	minute:      (void | int),
     45 	second:      (void | int),
     46 	nanosecond:  (void | int),
     47 };
     48 
     49 fn init() datetime = datetime {
     50 	sec         = 0,
     51 	nsec        = 0,
     52 	loc         = chrono::UTC,
     53 	zone        = null,
     54 	date        = void,
     55 	time        = void,
     56 
     57 	era         = void,
     58 	year        = void,
     59 	month       = void,
     60 	day         = void,
     61 	yearday     = void,
     62 	isoweekyear = void,
     63 	isoweek     = void,
     64 	week        = void,
     65 	sundayweek  = void,
     66 	weekday     = void,
     67 
     68 	hour        = void,
     69 	minute      = void,
     70 	second      = void,
     71 	nanosecond  = void,
     72 };
     73 
     74 // Evaluates and populates all of a [[datetime]]'s fields.
     75 fn all(dt: *datetime) *datetime = {
     76 	_era(dt);
     77 	_year(dt);
     78 	_month(dt);
     79 	_day(dt);
     80 	_yearday(dt);
     81 	_isoweekyear(dt);
     82 	_isoweek(dt);
     83 	_week(dt);
     84 	_sundayweek(dt);
     85 	_weekday(dt);
     86 
     87 	_hour(dt);
     88 	_minute(dt);
     89 	_second(dt);
     90 	_nanosecond(dt);
     91 
     92 	return dt;
     93 };
     94 
     95 // Creates a new datetime. A maximum of 7 optional field arguments can be given:
     96 // year, month, day-of-month, hour, minute, second, nanosecond. 8 or more causes
     97 // an abort.
     98 //
     99 // 	// 0000-01-01 00:00:00.000000000 +0000 UTC UTC
    100 // 	datetime::new(time::chrono::UTC, 0);
    101 //
    102 // 	// 2019-12-27 20:07:08.000031415 +0000 UTC UTC
    103 // 	datetime::new(time::chrono::UTC, 0,  2019, 12, 27,  20, 07, 08, 31415);
    104 //
    105 // 	// 2019-12-27 21:00:00.000000000 +0100 CET Europe/Amsterdam
    106 // 	datetime::new(time::chrono::tz("Europe/Amsterdam")!, 1 * time::HOUR,
    107 // 		2019, 12, 27,  21);
    108 //
    109 // 'zo' is the zone offset from the normal timezone (in most cases, UTC). For
    110 // example, the "Asia/Tokyo" timezone has a single zoff of +9 hours, but the
    111 // "Australia/Sydney" timezone has zoffs +10 hours and +11 hours, as they
    112 // observe Daylight Saving Time.
    113 //
    114 // If specified (non-void), 'zo' must match one of the timezone's observed
    115 // zoffs, or will fail. See [[time::chrono::fixedzone]] for custom timezones.
    116 //
    117 // You may omit the zoff. If the givem timezone has a single zone, [[new]]
    118 // will use that zone's zoff. Otherwise [[new]] will try to infer the zoff
    119 // from the multiple zones. This will fail during certain timezone transitions,
    120 // where certain datetimes are ambiguous or nonexistent. For example:
    121 //
    122 // - In the Europe/Amsterdam timezone, at 1995 March 26th,
    123 //   the local time 02:30 was never observed,
    124 //   as the clock jumped forward 1 hour from 02:00 CET to 03:00 CEST.
    125 //
    126 // - In the Europe/Amsterdam timezone, at 1995 September 24th,
    127 //   the local time 02:30 was observed twice (00:30 UTC & 01:30 UTC),
    128 //   as the clock jumped back 1 hour from 03:00 CEST to 02:00 CET.
    129 export fn new(
    130 	loc: chrono::locality,
    131 	zo: (time::duration | void),
    132 	fields: int...
    133 ) (datetime | invalid) = {
    134 	// TODO:
    135 	// - revise examples
    136 	// - Implement as described.
    137 	// - fix calls with `years <= -4715`.
    138 	//   https://todo.sr.ht/~sircmpwn/hare/565
    139 	let _fields: [_]int = [
    140 		0, 1, 1,    // year month day
    141 		0, 0, 0, 0, // hour min sec nsec
    142 	];
    143 
    144 	assert(len(fields) <= len(_fields),
    145 			"datetime::new(): Too many field arguments");
    146 	_fields[..len(fields)] = fields;
    147 
    148 	const year  = _fields[0];
    149 	const month = _fields[1];
    150 	const day   = _fields[2];
    151 	const hour  = _fields[3];
    152 	const min   = _fields[4];
    153 	const sec   = _fields[5];
    154 	const nsec  = _fields[6];
    155 
    156 	const mdate = calc_date__ymd(year, month, day)?;
    157 	const mtime = calc_time__hmsn(hour, min, sec, nsec)?;
    158 
    159 	// create the moment
    160 	const m = match (zo) {
    161 	case let zo: time::duration =>
    162 		yield chrono::from_datetime(loc, zo, mdate, mtime);
    163 	case void =>
    164 		// TODO: Deduce the zone offset
    165 		//
    166 		// perform a zone lookup, then try that zone and the zones that
    167 		// are observed before and after. This requires knowlegde of the
    168 		// transition index.
    169 		abort("TODO: datetime::new(zo=void)");
    170 	};
    171 
    172 	const dt = from_moment(m);
    173 
    174 	const zo = match (zo) {
    175 	case void =>
    176 		yield chrono::mzone(&m).zoff;
    177 	case let d: time::duration =>
    178 		yield d;
    179 	};
    180 
    181 	// check if input values are actually observed
    182 	if (
    183 		zo       != chrono::mzone(&dt).zoff
    184 		|| year  != _year(&dt)
    185 		|| month != _month(&dt)
    186 		|| day   != _day(&dt)
    187 		|| hour  != _hour(&dt)
    188 		|| min   != _minute(&dt)
    189 		|| sec   != _second(&dt)
    190 		|| nsec  != _nanosecond(&dt)
    191 	) {
    192 		return invalid;
    193 	};
    194 
    195 	return dt;
    196 };
    197 
    198 // Returns a [[datetime]] of the current system time using
    199 // [[time::clock::REALTIME]], in the [[time::chrono::LOCAL]] locality.
    200 export fn now() datetime = {
    201 	return from_instant(chrono::LOCAL, time::now(time::clock::REALTIME));
    202 };
    203 
    204 // Returns a [[datetime]] of the current system time using
    205 // [[time::clock::REALTIME]], in the [[time::chrono::UTC]] locality.
    206 export fn nowutc() datetime = {
    207 	return from_instant(chrono::UTC, time::now(time::clock::REALTIME));
    208 };
    209 
    210 // Creates a [[datetime]] from a [[time::chrono::moment]].
    211 export fn from_moment(m: chrono::moment) datetime = {
    212 	const dt = init();
    213 	dt.loc = m.loc;
    214 	dt.sec = m.sec;
    215 	dt.nsec = m.nsec;
    216 	dt.date = m.date;
    217 	dt.time = m.time;
    218 	dt.zone = m.zone;
    219 	return dt;
    220 };
    221 
    222 // Creates a [[datetime]] from a [[time::instant]]
    223 // in a [[time::chrono::locality]].
    224 export fn from_instant(loc: chrono::locality, i: time::instant) datetime = {
    225 	return from_moment(chrono::new(loc, i));
    226 };
    227 
    228 // Creates a [[datetime]] from a string, parsed according to a layout format.
    229 // See [[parse]] and [[format]]. At least a complete calendar date has to be
    230 // provided. The if hour, minute, second, nanosecond, or zone offset are not
    231 // provided, they default to 0.
    232 //
    233 // 	let new = datetime::from_str(
    234 // 		datetime::STAMP_NOZL,
    235 // 		"2019-12-27 22:07:08.000000000 +0100 CET Europe/Amsterdam",
    236 // 		locs...
    237 // 	)!;
    238 //
    239 // The datetime's [[time::chrono::locality]] will be selected from the provided
    240 // locality arguments. The 'name' field of these localities will be matched
    241 // against the parsed result for the %L specifier. If %L is not specified, or if
    242 // no locality is provided, [[time::chrono::UTC]] is used.
    243 export fn from_str(
    244 	layout: str,
    245 	s: str,
    246 	locs: time::chrono::locality...
    247 ) (datetime | parsefail | insufficient | invalid) = {
    248 	const v = newvirtual();
    249 	v.zoff = 0;
    250 	v.hour = 0;
    251 	v.minute = 0;
    252 	v.second = 0;
    253 	v.nanosecond = 0;
    254 	parse(&v, layout, s)?;
    255 	return realize(v, locs...)?;
    256 };
    257 
    258 @test fn from_str() void = {
    259 	const amst = chrono::tz("Europe/Amsterdam")!;
    260 	defer chrono::timezone_free(amst);
    261 
    262 	let testcases: [_](str, str, []chrono::locality, (datetime | error)) = [
    263 		(STAMP_NOZL, "2001-02-03 15:16:17.123456789 +0000 UTC UTC", [],
    264 			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17, 123456789)!),
    265 		(STAMP, "2001-02-03 15:16:17", [],
    266 			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!),
    267 		(RFC3339, "2001-02-03T15:16:17+0000", [],
    268 			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!),
    269 		("%F", "2009-06-30", [],
    270 			new(chrono::UTC, 0, 2009, 6, 30)!),
    271 		("%F %L", "2009-06-30 GPS", [chrono::TAI, chrono::GPS],
    272 			new(chrono::GPS, 0, 2009, 6, 30)!),
    273 		("%F %T", "2009-06-30 01:02:03", [],
    274 			new(chrono::UTC, 0, 2009, 6, 30, 1, 2, 3)!),
    275 		("%FT%T%Z", "2009-06-30T18:30:00Z", [],
    276 			new(chrono::UTC, 0, 2009, 6, 30, 18, 30)!),
    277 		("%FT%T.%N%Z", "2009-06-30T18:30:00.987654321Z", [],
    278 			new(chrono::UTC, 0, 2009, 6, 30, 18, 30, 0, 987654321)!),
    279 		("%FT%T%z %L", "2009-06-30T18:30:00+0200 Europe/Amsterdam", [amst],
    280 			new(amst, 2 * time::HOUR, 2009, 6, 30, 18, 30)!),
    281 
    282 		("%Y", "a", [], 'a': parsefail),
    283 		("%X", "2008", [], '2': parsefail),
    284 	];
    285 
    286 	let buf: [64]u8 = [0...];
    287 	for (let i = 0z; i < len(testcases); i += 1) {
    288 		const t = testcases[i];
    289 		const expect = t.3;
    290 		const actual = from_str(t.0, t.1, t.2...);
    291 
    292 		match (expect) {
    293 		case let e: datetime =>
    294 			assert(actual is datetime, "wanted 'datetime', got 'error'");
    295 			assert(chrono::eq(&(actual as datetime), &e)!,
    296 				"incorrect 'datetime' value");
    297 		case let e: parsefail =>
    298 			assert(actual is parsefail,
    299 				"wanted 'parsefail', got other");
    300 		case insufficient =>
    301 			assert(actual is insufficient,
    302 				"wanted 'insufficient', got other");
    303 		case invalid =>
    304 			assert(actual is invalid,
    305 				"wanted 'invalid', got other");
    306 		};
    307 	};
    308 };