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 };