reckon.ha (13782B)
1 // License: MPL-2.0 2 // (c) 2023 Byron Torres <b@torresjrjr.com> 3 use time; 4 use time::chrono; 5 6 // Specifies the behaviour of [[reckon]] when doing chronological arithmetic. 7 // 8 // The FLOOR, CEIL, HOP, and FOLD specifies how to resolve sub-significant 9 // overflows -- when a field's change in value causes any sub-significant 10 // field's range to shrink below its current value and become invalid. For 11 // example, adding 1 month to January 31st results in February 31st, a date with 12 // an unresolved day field, since February permits only 28 or 29 days. 13 export type calculus = enum uint { 14 // The default behaviour. Equivalent to CEIL. 15 DEFAULT = 0, 16 17 // Apply units in reverse order, from least to most significant. 18 REVSIG = 1 << 0, 19 20 // When a sub-significant overflow occurs, the unresolved field is set 21 // to its minimum valid value. 22 // 23 // Feb 31 -> Feb 01 24 // Aug 64 -> Aug 01 25 FLOOR = 1 << 1, 26 27 // When a sub-significant overflow occurs, the unresolved field is set 28 // to its maximum valid value. 29 // 30 // Feb 31 -> Feb 28 / Feb 29 (leap year dependent) 31 // Aug 64 -> Aug 31 32 CEIL = 1 << 2, 33 34 // When a sub-significant overflow occurs, the unresolved field is set 35 // to its new minimum valid value after the next super-significant field 36 // increments by one. 37 // 38 // Feb 31 -> Mar 01 39 // Aug 64 -> Sep 01 40 HOP = 1 << 3, 41 42 // When a sub-significant overflow occurs, the unresolved field's 43 // maximum valid value is subtracted from it's current value, and the 44 // next super-significant field increments by one. This process repeats 45 // until the unresolved field's value becomes valid (falls in range). 46 // 47 // Feb 31 -> Mar 03 / Mar 02 (leap year dependent) 48 // Aug 64 -> Sep 33 -> Oct 03 49 FOLD = 1 << 4, 50 }; 51 52 // Reckons from a given [[datetime]] to a new one, via a set of [[period]]s. 53 // This is a chronology-wise arithmetic operation. Each period is reckoned 54 // independently in succession, applying (adding) their units from most to least 55 // significant. 56 // 57 // The [[calculus]] parameter determines arithmetic and resolution behaviour 58 // when encountering deviations (e.g. overflows). 59 // 60 // let dest = datetime::reckon( 61 // start, // 2000-02-29 09:00:00 62 // 0, // calculus::DEFAULT 63 // datetime::period { 64 // years = 1, // becomes: 2001-02-28 09:00:00 65 // months = -2, // becomes: 2000-12-28 09:00:00 66 // days = 4, // becomes: 2001-01-01 09:00:00 67 // }, 68 // ); 69 // 70 // See [[add]] for a timescale-wise arithmetic operation which uses 71 // [[time::duration]]. 72 export fn reckon(dt: datetime, calc: calculus, ps: period...) datetime = { 73 let r = newvirtual(); // our reckoner 74 r.vloc = dt.loc; 75 r.zoff = chrono::mzone(&dt).zoff; 76 r.year = _year(&dt); 77 r.month = _month(&dt); 78 r.day = _day(&dt); 79 r.hour = _hour(&dt); 80 r.minute = _minute(&dt); 81 r.second = _second(&dt); 82 r.nanosecond = _nanosecond(&dt); 83 84 if (calc == calculus::DEFAULT) { 85 calc |= calculus::CEIL; 86 }; 87 88 for (let i = 0z; i < len(ps); i += 1) if (calc & calculus::REVSIG == 0) { 89 const p = ps[i]; 90 const fold = calculus::FOLD; 91 92 r.year = r.year as int + p.years: int; 93 reckon_days(&r, 0, calc); // bubble up potential Feb 29 overflow 94 95 reckon_months(&r, p.months); 96 reckon_days(&r, 0, calc); // bubble up potential overflows 97 98 reckon_days(&r, p.weeks * 7, fold); 99 reckon_days(&r, p.days, fold); 100 101 // TODO: These functions aren't aware of top-down overflows. 102 // Handle overflows (e.g. [[zone]] changes). 103 reckon_hours(&r, p.hours, fold); 104 reckon_minutes(&r, p.minutes, fold); 105 reckon_seconds(&r, p.seconds, fold); 106 reckon_nanoseconds(&r, p.nanoseconds, fold); 107 } else { 108 const p = ps[i]; 109 const fold = calculus::FOLD | calculus::REVSIG; 110 111 reckon_nanoseconds(&r, p.nanoseconds, fold); 112 reckon_seconds(&r, p.seconds, fold); 113 reckon_minutes(&r, p.minutes, fold); 114 reckon_hours(&r, p.hours, fold); 115 reckon_days(&r, p.days, fold); 116 reckon_days(&r, p.weeks * 7, fold); 117 118 reckon_months(&r, p.months); 119 reckon_days(&r, 0, calc); // bubble up potential overflows 120 121 r.year = r.year as int + p.years: int; 122 reckon_days(&r, 0, calc); // bubble up potential Feb 29 overflow 123 }; 124 125 return realize(r)!; 126 }; 127 128 fn reckon_months(r: *virtual, months: i64) void = { 129 let year = r.year as int; 130 let month = r.month as int; 131 132 month += months: int; 133 134 // month overflow 135 for (month > 12) { 136 month -= 12; 137 year += 1; 138 }; 139 for (month < 1) { 140 month += 12; 141 year -= 1; 142 }; 143 144 r.year = year; 145 r.month = month; 146 }; 147 148 fn reckon_days(r: *virtual, days: i64, calc: calculus) void = { 149 let year = r.year as int; 150 let month = r.month as int; 151 let day = r.day as int; 152 153 day += days: int; 154 155 // day overflow 156 let month_daycnt = calc_month_daycnt(year, month); 157 for (day > month_daycnt) { 158 if (calc & calculus::FLOOR != 0) { 159 day = 1; 160 } else if (calc & calculus::CEIL != 0) { 161 day = month_daycnt; 162 } else if (calc & calculus::HOP != 0) { 163 r.year = year; 164 r.month = month; 165 166 reckon_months(r, 1); 167 168 year = r.year as int; 169 month = r.month as int; 170 day = 1; 171 } else if (calc & calculus::FOLD != 0) { 172 r.year = year; 173 r.month = month; 174 175 reckon_months(r, 1); 176 177 year = r.year as int; 178 month = r.month as int; 179 day -= month_daycnt; 180 }; 181 month_daycnt = calc_month_daycnt(year, month); 182 }; 183 for (day < 1) { 184 r.year = year; 185 r.month = month; 186 187 reckon_months(r, -1); 188 189 year = r.year as int; 190 month = r.month as int; 191 day += calc_month_daycnt(year, month); 192 }; 193 194 r.year = year; 195 r.month = month; 196 r.day = day; 197 }; 198 199 fn reckon_hours(r: *virtual, hours: i64, calc: calculus) void = { 200 let hour = r.hour as int; 201 202 hour += hours: int; 203 204 // hour overflow 205 for (hour >= 24) { 206 reckon_days(r, 1, calc); 207 hour -= 24; 208 }; 209 for (hour < 0) { 210 reckon_days(r, -1, calc); 211 hour += 24; 212 }; 213 214 r.hour = hour; 215 }; 216 217 fn reckon_minutes(r: *virtual, mins: i64, calc: calculus) void = { 218 let min = r.minute as int; 219 220 min += mins: int; 221 222 // minute overflow 223 for (min >= 60) { 224 reckon_hours(r, 1, calc); 225 min -= 60; 226 }; 227 for (min < 0) { 228 reckon_hours(r, -1, calc); 229 min += 60; 230 }; 231 232 r.minute = min; 233 }; 234 235 fn reckon_seconds(r: *virtual, secs: i64, calc: calculus) void = { 236 let s = r.second as int; 237 238 s += secs: int; 239 240 // second overflow 241 for (s >= 60) { 242 reckon_minutes(r, 1, calc); 243 s -= 60; 244 }; 245 for (s < 0) { 246 reckon_minutes(r, -1, calc); 247 s += 60; 248 }; 249 250 r.second = s; 251 }; 252 253 fn reckon_nanoseconds(r: *virtual, nsecs: i64, calc: calculus) void = { 254 let ns = r.nanosecond as int; 255 256 ns += nsecs: int; 257 258 // nanosecond overflow 259 for (ns >= 1000000000) { // 1E9 nanoseconds (1 second) 260 reckon_seconds(r, 1, calc); 261 ns -= 1000000000; 262 }; 263 for (ns < 0) { 264 reckon_seconds(r, -1, calc); 265 ns += 1000000000; 266 }; 267 268 r.nanosecond = ns; 269 }; 270 271 @test fn reckon() void = { 272 const Amst = chrono::tz("Europe/Amsterdam")!; 273 defer chrono::timezone_free(Amst); 274 275 // no-op period, calculus::CEIL 276 277 let p = period { ... }; 278 279 let a = new(chrono::UTC, 0)!; 280 let r = reckon(a, 0, p); 281 assert(chrono::eq(&a, &r)!, "01. incorrect result"); 282 283 let a = new(chrono::UTC, 0, 2019, 12, 27, 21, 7, 8, 0)!; 284 let r = reckon(a, 0, p); 285 assert(chrono::eq(&a, &r)!, "02. incorrect result"); 286 287 let a = new(Amst, 1 * time::HOUR, 2019, 12, 27, 22, 7, 8, 0)!; 288 let r = reckon(a, 0, p); 289 assert(chrono::eq(&a, &r)!, "03. incorrect result"); 290 291 // generic periods, calculus::CEIL 292 293 let a = new(chrono::UTC, 0, 2019, 12, 27, 21, 7, 8, 0)!; 294 295 let r = reckon(a, 0, period { 296 years = 1, 297 months = 1, 298 days = 1, 299 hours = 1, 300 minutes = 1, 301 seconds = 1, 302 nanoseconds = 1, 303 ... 304 }); 305 let b = new(chrono::UTC, 0, 2021, 1, 28, 22, 8, 9, 1)!; 306 assert(chrono::eq(&b, &r)!, "04. incorrect result"); 307 308 let r = reckon(a, 0, period { 309 years = -1, 310 months = -1, 311 days = -1, 312 hours = -1, 313 minutes = -1, 314 seconds = -1, 315 nanoseconds = -1, 316 ... 317 }); 318 let b = new(chrono::UTC, 0, 2018, 11, 26, 20, 6, 6, 999999999)!; 319 assert(chrono::eq(&b, &r)!, "05. incorrect result"); 320 321 let r = reckon(a, 0, period { 322 years = 100, 323 months = 100, 324 days = 100, 325 hours = 100, 326 minutes = 100, 327 seconds = 100, 328 nanoseconds = 100, 329 ... 330 }); 331 let b = new(chrono::UTC, 0, 2128, 8, 10, 2, 48, 48, 100)!; 332 assert(chrono::eq(&b, &r)!, "06. incorrect result"); 333 334 let r = reckon(a, 0, period { 335 years = -100, 336 months = -100, 337 days = -100, 338 hours = -100, 339 minutes = -100, 340 seconds = -100, 341 nanoseconds = -100, 342 ... 343 }); 344 let b = new(chrono::UTC, 0, 1911, 5, 15, 15, 25, 27, 999999900)!; 345 assert(chrono::eq(&b, &r)!, "07. incorrect result"); 346 347 let r = reckon(a, 0, period { 348 weeks = 100, 349 ... 350 }); 351 let b = new(chrono::UTC, 0, 2021, 11, 26, 21, 7, 8, 0)!; 352 assert(chrono::eq(&b, &r)!, "08. incorrect result"); 353 354 // calculus, February 29 overflows 355 356 let a = new(chrono::UTC, 0, 2000, 1, 31)!; // leap year 357 let p = period { months = 1, ... }; 358 359 let r = reckon(a, calculus::FLOOR, p); 360 let b = new(chrono::UTC, 0, 2000, 2, 1)!; 361 assert(chrono::eq(&b, &r)!, "09. incorrect result"); 362 363 let r = reckon(a, calculus::CEIL, p); 364 let b = new(chrono::UTC, 0, 2000, 2, 29)!; 365 assert(chrono::eq(&b, &r)!, "10. incorrect result"); 366 367 let r = reckon(a, calculus::HOP, p); 368 let b = new(chrono::UTC, 0, 2000, 3, 1)!; 369 assert(chrono::eq(&b, &r)!, "11. incorrect result"); 370 371 let r = reckon(a, calculus::FOLD, p); 372 let b = new(chrono::UTC, 0, 2000, 3, 2)!; 373 assert(chrono::eq(&b, &r)!, "12. incorrect result"); 374 375 // calculus, February 28 overflows 376 377 let a = new(chrono::UTC, 0, 2000, 1, 31)!; // leap year 378 let p = period { years = 1, months = 1, ... }; 379 380 let r = reckon(a, calculus::FLOOR, p); 381 let b = new(chrono::UTC, 0, 2001, 2, 1)!; 382 assert(chrono::eq(&b, &r)!, "13. incorrect result"); 383 384 let r = reckon(a, calculus::CEIL, p); 385 let b = new(chrono::UTC, 0, 2001, 2, 28)!; 386 assert(chrono::eq(&b, &r)!, "14. incorrect result"); 387 388 let r = reckon(a, calculus::HOP, p); 389 let b = new(chrono::UTC, 0, 2001, 3, 1)!; 390 assert(chrono::eq(&b, &r)!, "15. incorrect result"); 391 392 let r = reckon(a, calculus::FOLD, p); 393 let b = new(chrono::UTC, 0, 2001, 3, 3)!; 394 assert(chrono::eq(&b, &r)!, "16. incorrect result"); 395 396 // multiple periods 397 398 let a = new(chrono::UTC, 0, 2000, 12, 31)!; 399 let ps = [ 400 period { years = +1, months = +1, days = +1, ... }, 401 period { years = -1, months = -1, days = -1, ... }, 402 period { years = -1, months = -1, days = -1, ... }, 403 period { years = +1, months = +1, days = +1, ... }, 404 period { hours = +1, minutes = +1, seconds = +1, ... }, 405 period { hours = -1, minutes = -1, seconds = -1, ... }, 406 period { hours = -1, minutes = -1, seconds = -1, ... }, 407 period { hours = +1, minutes = +1, seconds = +1, ... }, 408 ]; 409 410 let r = reckon(a, 0, ps[..1]...); 411 let b = new(chrono::UTC, 0, 2002, 2, 1)!; 412 assert(chrono::eq(&b, &r)!, "17. incorrect result"); 413 414 let r = reckon(a, 0, ps[..2]...); 415 let b = new(chrono::UTC, 0, 2000, 12, 31)!; 416 assert(chrono::eq(&b, &r)!, "18. incorrect result"); 417 418 let r = reckon(a, 0, ps[..3]...); 419 let b = new(chrono::UTC, 0, 1999, 11, 29)!; 420 assert(chrono::eq(&b, &r)!, "19. incorrect result"); 421 422 let r = reckon(a, 0, ps[..4]...); 423 let b = new(chrono::UTC, 0, 2000, 12, 30)!; 424 assert(chrono::eq(&b, &r)!, "20. incorrect result"); 425 426 let r = reckon(a, 0, ps[..5]...); 427 let b = new(chrono::UTC, 0, 2000, 12, 30, 1, 1, 1)!; 428 assert(chrono::eq(&b, &r)!, "21. incorrect result"); 429 430 let r = reckon(a, 0, ps[..6]...); 431 let b = new(chrono::UTC, 0, 2000, 12, 30)!; 432 assert(chrono::eq(&b, &r)!, "22. incorrect result"); 433 434 let r = reckon(a, 0, ps[..7]...); 435 let b = new(chrono::UTC, 0, 2000, 12, 29, 22, 58, 59)!; 436 assert(chrono::eq(&b, &r)!, "23. incorrect result"); 437 438 let r = reckon(a, 0, ps[..8]...); 439 let b = new(chrono::UTC, 0, 2000, 12, 30)!; 440 assert(chrono::eq(&b, &r)!, "24. incorrect result"); 441 442 // multiple periods, calculus::REVSIG 443 444 let a = new(chrono::UTC, 0, 2000, 12, 31)!; 445 let ps = [ 446 period { years = +1, months = +1, days = +1, ... }, 447 period { years = -1, months = -1, days = -1, ... }, 448 period { years = -1, months = -1, days = -1, ... }, 449 period { years = +1, months = +1, days = +1, ... }, 450 period { hours = +1, minutes = +1, seconds = +1, ... }, 451 period { hours = -1, minutes = -1, seconds = -1, ... }, 452 period { hours = -1, minutes = -1, seconds = -1, ... }, 453 period { hours = +1, minutes = +1, seconds = +1, ... }, 454 ]; 455 456 let r = reckon(a, calculus::REVSIG, ps[..1]...); 457 let b = new(chrono::UTC, 0, 2002, 2, 1)!; 458 assert(chrono::eq(&b, &r)!, "25. incorrect result"); 459 460 let r = reckon(a, calculus::REVSIG, ps[..2]...); 461 let b = new(chrono::UTC, 0, 2000, 12, 31)!; 462 assert(chrono::eq(&b, &r)!, "26. incorrect result"); 463 464 let r = reckon(a, calculus::REVSIG, ps[..3]...); 465 let b = new(chrono::UTC, 0, 1999, 11, 30)!; 466 assert(chrono::eq(&b, &r)!, "27. incorrect result"); 467 468 let r = reckon(a, calculus::REVSIG, ps[..4]...); 469 let b = new(chrono::UTC, 0, 2001, 1, 1)!; 470 assert(chrono::eq(&b, &r)!, "28. incorrect result"); 471 472 let r = reckon(a, calculus::REVSIG, ps[..5]...); 473 let b = new(chrono::UTC, 0, 2001, 1, 1, 1, 1, 1)!; 474 assert(chrono::eq(&b, &r)!, "29. incorrect result"); 475 476 let r = reckon(a, calculus::REVSIG, ps[..6]...); 477 let b = new(chrono::UTC, 0, 2001, 1, 1)!; 478 assert(chrono::eq(&b, &r)!, "30. incorrect result"); 479 480 let r = reckon(a, calculus::REVSIG, ps[..7]...); 481 let b = new(chrono::UTC, 0, 2000, 12, 31, 22, 58, 59)!; 482 assert(chrono::eq(&b, &r)!, "31. incorrect result"); 483 484 let r = reckon(a, calculus::REVSIG, ps[..8]...); 485 let b = new(chrono::UTC, 0, 2001, 1, 1)!; 486 assert(chrono::eq(&b, &r)!, "32. incorrect result"); 487 488 return; 489 };