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