hare

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

date.ha (9680B)


      1 // SPDX-License-Identifier: MPL-2.0
      2 // (c) Hare authors <https://harelang.org>
      3 
      4 use time;
      5 use time::chrono;
      6 
      7 // Invalid [[date]].
      8 export type invalid = !chrono::invalid;
      9 
     10 // A date/time object; a [[time::chrono::moment]] wrapper optimized for the
     11 // Gregorian chronology, and by extension a [[time::instant]] wrapper.
     12 //
     13 // This object should be treated as private and immutable. Directly mutating its
     14 // fields causes undefined behaviour when used with module functions. Likewise,
     15 // interrogating the fields' type and value (e.g. using match statements) is
     16 // also improper.
     17 //
     18 // A date observes various chronological values, cached in its fields. To
     19 // evaluate and obtain these values, use the various observer functions
     20 // ([[year]], [[hour]], etc.). These values are derived from the embedded moment
     21 // information, and thus are guaranteed to be valid.
     22 //
     23 // See [[virtual]] for an public, mutable, intermediary representation of a
     24 // date, which waives guarantees of validity.
     25 export type date = struct {
     26 	chrono::moment,
     27 
     28 	era:         (void | int),
     29 	year:        (void | int),
     30 	month:       (void | int),
     31 	day:         (void | int),
     32 	yearday:     (void | int),
     33 	isoweekyear: (void | int),
     34 	isoweek:     (void | int),
     35 	week:        (void | int),
     36 	sundayweek:  (void | int),
     37 	weekday:     (void | int),
     38 
     39 	hour:        (void | int),
     40 	minute:      (void | int),
     41 	second:      (void | int),
     42 	nanosecond:  (void | int),
     43 };
     44 
     45 fn init() date = date {
     46 	sec         = 0,
     47 	nsec        = 0,
     48 	loc         = chrono::UTC,
     49 	zone        = null,
     50 	daydate     = void,
     51 	daytime     = void,
     52 
     53 	era         = void,
     54 	year        = void,
     55 	month       = void,
     56 	day         = void,
     57 	yearday     = void,
     58 	isoweekyear = void,
     59 	isoweek     = void,
     60 	week        = void,
     61 	sundayweek  = void,
     62 	weekday     = void,
     63 
     64 	hour        = void,
     65 	minute      = void,
     66 	second      = void,
     67 	nanosecond  = void,
     68 };
     69 
     70 // Evaluates and populates all of a [[date]]'s fields.
     71 fn all(d: *date) *date = {
     72 	_era(d);
     73 	_year(d);
     74 	_month(d);
     75 	_day(d);
     76 	_yearday(d);
     77 	_isoweekyear(d);
     78 	_isoweek(d);
     79 	_week(d);
     80 	_sundayweek(d);
     81 	_weekday(d);
     82 
     83 	_hour(d);
     84 	_minute(d);
     85 	_second(d);
     86 	_nanosecond(d);
     87 
     88 	return d;
     89 };
     90 
     91 // Creates a new [[date]]. Accepts a [[time::chrono::locality]], a zone-offset,
     92 // and up to seven nominal fields applied in the following order:
     93 //
     94 // - year
     95 // - month
     96 // - day
     97 // - hour
     98 // - minute
     99 // - second
    100 // - nanosecond
    101 //
    102 // 8 or more fields causes an abort. If omitted, the month and day default to 1,
    103 // and the rest default to 0.
    104 //
    105 // If the desired zone-offset is known, it can be given as a [[time::duration]].
    106 // Otherwise, use a zflag. See [[zflag]] on its effects to the result.
    107 //
    108 // An invalid combination of provided date/time/zoff values returns [[invalid]].
    109 //
    110 // Examples:
    111 //
    112 // 	// 0000-01-01 00:00:00.000000000 +0000 UTC UTC
    113 // 	date::new(time::chrono::UTC, date::zflag::CONTIG);
    114 //
    115 // 	// 2000-01-02 15:04:05.600000000 +0000 UTC UTC
    116 // 	date::new(time::chrono::UTC, 0,
    117 // 		2000,  1,  2,  15,  4,  5, 600000000);
    118 //
    119 // 	// 2000-01-02 15:00:00.000000000 +0100 CET Europe/Amsterdam
    120 // 	date::new(time::chrono::tz("Europe/Amsterdam")!,
    121 // 		1 * time::HOUR, // standard time in January
    122 // 		2000,  1,  2,  15);
    123 //
    124 // 	// Could return [[zfunresolved]] by encountering a timezone transition.
    125 // 	date::new(time::chrono::tz("Europe/Amsterdam")!,
    126 // 		date::zflag::CONTIG,
    127 // 		fields...);
    128 //
    129 // 	// Will never return [[zfunresolved]].
    130 // 	date::new(time::chrono::tz("Europe/Amsterdam")!,
    131 // 		date::zflag::LAP_EARLY | date::zflag::GAP_END,
    132 // 		fields...);
    133 //
    134 // 	// On this day in Amsterdam, the clock jumped +1 hour at 02:00.
    135 // 	// 02:30 is never observed. Note the difference in zone-offset.
    136 // 	//
    137 // 	// 2000-03-26 01:59:59.999999999 +0100 CET Europe/Amsterdam
    138 // 	date::new(time::chrono::tz("Europe/Amsterdam")!,
    139 // 		date::zflag::GAP_START,
    140 // 		2000,  3, 26,   2, 30);
    141 // 	//
    142 // 	// 2000-03-26 03:00:00.000000000 +0200 CET Europe/Amsterdam
    143 // 	date::new(time::chrono::tz("Europe/Amsterdam")!,
    144 // 		date::zflag::GAP_END,
    145 // 		2000,  3, 26,   2, 30);
    146 //
    147 // 	// On this day in Amsterdam, the clock jumped -1 hour at 03:00.
    148 // 	// 02:30 is observed twice. Note the difference in zone-offset.
    149 // 	//
    150 // 	// 2000-10-29 02:30:00.000000000 +0200 CET Europe/Amsterdam
    151 // 	date::new(time::chrono::tz("Europe/Amsterdam")!,
    152 // 		date::zflag::LAP_EARLY,
    153 // 		2000, 10, 29,   2, 30);
    154 // 	//
    155 // 	// 2000-10-29 02:30:00.000000000 +0100 CET Europe/Amsterdam
    156 // 	date::new(time::chrono::tz("Europe/Amsterdam")!,
    157 // 		date::zflag::LAP_LATE,
    158 // 		2000, 10, 29,   2, 30);
    159 //
    160 export fn new(
    161 	loc: chrono::locality,
    162 	zoff: (time::duration | zflag),
    163 	fields: int...
    164 ) (date | invalid | zfunresolved) = {
    165 	let _fields: [_]int = [
    166 		0, 1, 1,    // year month day
    167 		0, 0, 0, 0, // hour min sec nsec
    168 	];
    169 
    170 	assert(len(fields) <= len(_fields),
    171 		"time::date::new(): Too many field arguments");
    172 	_fields[..len(fields)] = fields;
    173 
    174 	let v = newvirtual();
    175 
    176 	v.vloc       = loc;
    177 	v.zoff       = zoff;
    178 	v.year       = _fields[0];
    179 	v.month      = _fields[1];
    180 	v.day        = _fields[2];
    181 	v.hour       = _fields[3];
    182 	v.minute     = _fields[4];
    183 	v.second     = _fields[5];
    184 	v.nanosecond = _fields[6];
    185 
    186 	let d = (realize(v, loc) as (date | invalid | zfunresolved))?;
    187 
    188 	// if zflag::GAP_START or zflag::GAP_END was not specified,
    189 	// check if input values are actually observed
    190 	if (
    191 		// TODO: check observe values outside of gap?
    192 		zoff is zflag
    193 		&& zoff as zflag & (zflag::GAP_START | zflag::GAP_END) == 0
    194 	) {
    195 		if (
    196 			_fields[0] != _year(&d)
    197 			|| _fields[1] != _month(&d)
    198 			|| _fields[2] != _day(&d)
    199 			|| _fields[3] != _hour(&d)
    200 			|| _fields[4] != _minute(&d)
    201 			|| _fields[5] != _second(&d)
    202 			|| _fields[6] != _nanosecond(&d)
    203 		) {
    204 			return invalid;
    205 		};
    206 	};
    207 
    208 	return d;
    209 };
    210 
    211 // Returns a [[date]] of the current system time using
    212 // [[time::clock::REALTIME]], in the [[time::chrono::UTC]] locality.
    213 export fn now() date = {
    214 	return from_instant(chrono::UTC, time::now(time::clock::REALTIME));
    215 };
    216 
    217 // Returns a [[date]] of the current system time using
    218 // [[time::clock::REALTIME]], in the [[time::chrono::LOCAL]] locality.
    219 export fn localnow() date = {
    220 	return from_instant(chrono::LOCAL, time::now(time::clock::REALTIME));
    221 };
    222 
    223 // Creates a [[date]] from a [[time::chrono::moment]].
    224 export fn from_moment(m: chrono::moment) date = {
    225 	const d = init();
    226 	d.loc = m.loc;
    227 	d.sec = m.sec;
    228 	d.nsec = m.nsec;
    229 	d.daydate = m.daydate;
    230 	d.daytime = m.daytime;
    231 	d.zone = m.zone;
    232 	return d;
    233 };
    234 
    235 // Creates a [[date]] from a [[time::instant]] in a [[time::chrono::locality]].
    236 export fn from_instant(loc: chrono::locality, i: time::instant) date = {
    237 	return from_moment(chrono::new(loc, i));
    238 };
    239 
    240 // Creates a [[date]] from a string, parsed according to a layout format.
    241 // See [[parse]] and [[format]]. Example:
    242 //
    243 // 	let new = date::from_str(
    244 // 		date::STAMPLOC,
    245 // 		"2000-01-02 15:04:05.600000000 +0100 CET Europe/Amsterdam",
    246 // 		chrono::tz("Europe/Amsterdam")!
    247 // 	)!;
    248 //
    249 // At least a complete calendar date has to be provided. If the hour, minute,
    250 // second, or nanosecond values are not provided, they default to 0.
    251 // If the zone-offset or zone-abbreviation are not provided, the [[zflags]]s
    252 // LAP_EARLY and GAP_END are used.
    253 //
    254 // The date's [[time::chrono::locality]] will be selected from the provided
    255 // locality arguments. The 'name' field of these localities will be matched
    256 // against the parsed result of the %L specifier. If %L is not specified,
    257 // or if no locality is provided, [[time::chrono::UTC]] is used.
    258 export fn from_str(
    259 	layout: str,
    260 	s: str,
    261 	locs: chrono::locality...
    262 ) (date | parsefail | insufficient | invalid) = {
    263 	const v = newvirtual();
    264 	v.zoff = zflag::LAP_EARLY | zflag::GAP_END;
    265 	v.hour = 0;
    266 	v.minute = 0;
    267 	v.second = 0;
    268 	v.nanosecond = 0;
    269 
    270 	parse(&v, layout, s)?;
    271 
    272 	if (v.locname is void || len(locs) == 0) {
    273 		v.vloc = chrono::UTC;
    274 	};
    275 
    276 	return realize(v, locs...) as (date | insufficient | invalid);
    277 };
    278 
    279 @test fn from_str() void = {
    280 	let testcases: [_](str, str, []chrono::locality, (date | error)) = [
    281 		(STAMPLOC, "2001-02-03 15:16:17.123456789 +0000 UTC UTC", [],
    282 			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17, 123456789)!),
    283 		(STAMP, "2001-02-03 15:16:17", [],
    284 			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!),
    285 		(RFC3339, "2001-02-03T15:16:17+0000", [],
    286 			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!),
    287 		("%F", "2009-06-30", [],
    288 			new(chrono::UTC, 0, 2009, 6, 30)!),
    289 		("%F %L", "2009-06-30 GPS", [chrono::TAI, chrono::GPS],
    290 			new(chrono::GPS, 0, 2009, 6, 30)!),
    291 		("%F %T", "2009-06-30 01:02:03", [],
    292 			new(chrono::UTC, 0, 2009, 6, 30, 1, 2, 3)!),
    293 		("%FT%T%z", "2009-06-30T18:30:00Z", [],
    294 			new(chrono::UTC, 0, 2009, 6, 30, 18, 30)!),
    295 		("%FT%T.%N%z", "2009-06-30T18:30:00.987654321Z", [],
    296 			new(chrono::UTC, 0, 2009, 6, 30, 18, 30, 0, 987654321)!),
    297 		// TODO: for the tests overhaul, when internal test timezones
    298 		// are available, check for %L
    299 		//("%FT%T%z %L", "2009-06-30T18:30:00+0200 Europe/Amsterdam", [amst],
    300 		//	new(amst, 2 * time::HOUR, 2009, 6, 30, 18, 30)!),
    301 
    302 		("%Y", "a", [], (0z, 'a'): parsefail),
    303 		("%X", "2008", [], (0z, '2'): parsefail),
    304 	];
    305 
    306 	let buf: [64]u8 = [0...];
    307 	for (let tc .. testcases) {
    308 		const expect = tc.3;
    309 		const actual = from_str(tc.0, tc.1, tc.2...);
    310 
    311 		match (expect) {
    312 		case let e: date =>
    313 			assert(actual is date, "wanted 'date', got 'error'");
    314 			assert(chrono::simultaneous(&(actual as date), &e)!,
    315 				"incorrect 'date' value");
    316 		case let e: parsefail =>
    317 			assert(actual is parsefail,
    318 				"wanted 'parsefail', got other");
    319 		case insufficient =>
    320 			assert(actual is insufficient,
    321 				"wanted 'insufficient', got other");
    322 		case invalid =>
    323 			assert(actual is invalid,
    324 				"wanted 'invalid', got other");
    325 		};
    326 	};
    327 };