arithmetic.ha (9976B)
1 // License: MPL-2.0 2 // (c) 2021-2022 Byron Torres <b@torresjrjr.com> 3 // (c) 2022 Drew DeVault <sir@cmpwn.com> 4 // (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net> 5 use fmt; 6 use math; 7 use time; 8 use time::chrono; 9 10 // The nominal units of the Gregorian chronology. Used for chronological 11 // arithmetic. 12 export type unit = enum int { 13 ERA, 14 YEAR, 15 MONTH, 16 WEEK, 17 DAY, 18 HOUR, 19 MINUTE, 20 SECOND, 21 NANOSECOND, 22 }; 23 24 // Calculates the [[period]] between two [[datetime]]s, from A to B. 25 // The returned period, provided to [[reckon]] along with A, will produce B, 26 // regardless of the [[calculus]] used. All the period's non-zero fields will 27 // have the same sign. 28 export fn pdiff(a: datetime, b: datetime) period = { 29 let p = period { ... }; 30 31 if (chrono::compare(&a, &b) == 0) { 32 return p; 33 }; 34 35 let reverse = if (chrono::compare(&a, &b) > 0) true else false; 36 if (reverse) { 37 let tmp = a; 38 a = b; 39 b = tmp; 40 }; 41 42 p.years = _year(&b) - _year(&a); 43 44 p.months = _month(&b) - _month(&a); 45 if (p.months < 0) { 46 p.years -= 1; 47 p.months += 12; 48 }; 49 50 p.days = _day(&b) - _day(&a); 51 let year = _year(&b); 52 let month = _month(&b); 53 let daycnt = calc_month_daycnt(year, month); 54 for (_day(&a) > daycnt || p.days < 0) { 55 month -= 1; 56 if (month == 0) { 57 year -= 1; 58 month = 12; 59 }; 60 daycnt = calc_month_daycnt(year, month); 61 62 p.months -= 1; 63 if (p.months < 0) { 64 p.years -= 1; 65 p.months += 12; 66 }; 67 p.days += daycnt; 68 }; 69 70 p.hours = _hour(&b) - _hour(&a); 71 if (p.hours < 0) { 72 p.days -= 1; 73 p.hours += 24; 74 }; 75 76 p.minutes = _minute(&b) - _minute(&a); 77 if (p.minutes < 0) { 78 p.hours -= 1; 79 p.minutes += 60; 80 }; 81 82 p.seconds = _second(&b) - _second(&a); 83 if (p.seconds < 0) { 84 p.minutes -= 1; 85 p.seconds += 60; 86 }; 87 88 p.nanoseconds = _nanosecond(&b) - _nanosecond(&a); 89 if (p.nanoseconds < 0) { 90 p.seconds -= 1; 91 p.nanoseconds += 1000000000; // 10E9 92 }; 93 94 return if (reverse) neg(p) else p; 95 }; 96 97 // Calculates the nominal [[unit]] difference between two [[datetime]]s. 98 export fn unitdiff(a: datetime, b: datetime, u: unit) i64 = { 99 switch (u) { 100 case unit::ERA => 101 return era(&b) - era(&a); 102 case unit::YEAR => 103 return pdiff(a, b).years; 104 case unit::MONTH => 105 const d = pdiff(a, b); 106 return d.years * 12 + d.months; 107 case unit::WEEK => 108 return unitdiff(a, b, unit::DAY) / 7; 109 case unit::DAY => 110 return chrono::date(&b) - chrono::date(&a); 111 case unit::HOUR => 112 return unitdiff(a, b, unit::DAY) * 24 + pdiff(a, b).hours; 113 case unit::MINUTE => 114 return unitdiff(a, b, unit::HOUR) * 60 + pdiff(a, b).minutes; 115 case unit::SECOND => 116 return unitdiff(a, b, unit::MINUTE) * 60 + pdiff(a, b).seconds; 117 case unit::NANOSECOND => 118 return unitdiff(a, b, unit::SECOND) * 1000000000 + pdiff(a, b).nanoseconds; 119 }; 120 }; 121 122 // Truncates the given [[datetime]] at the provided nominal [[unit]]. 123 // 124 // For example, truncating to the nearest unit::MONTH will set the 'day', 125 // 'hour', 'minute', 'second', and 'nanosecond' fields to their minimum values. 126 export fn truncate(dt: datetime, u: unit) datetime = { 127 // TODO: There exist timezones where midnight is invalid on certain 128 // days. The new()! calls will fail, but we probably don't want to '?' 129 // propagate [[invalid]] to keep this function's use simple. The minimum 130 // values (the zeroes and ones here) can't be hardcoded. They need 131 // calculation. We should either handle this here; or probably in 132 // realize(), and then use realize() here. 133 return switch (u) { 134 case unit::ERA => 135 yield new(dt.loc, chrono::mzone(&dt).zoff, 136 1, 1, 1, 137 0, 0, 0, 0, 138 )!; 139 case unit::YEAR => 140 yield new(dt.loc, chrono::mzone(&dt).zoff, 141 _year(&dt), 1, 1, 142 0, 0, 0, 0, 143 )!; 144 case unit::MONTH => 145 yield new(dt.loc, chrono::mzone(&dt).zoff, 146 _year(&dt), _month(&dt), 1, 147 0, 0, 0, 0, 148 )!; 149 case unit::WEEK => 150 const date = chrono::date(&dt) - _weekday(&dt); 151 const ymd = calc_ymd(date); 152 yield new(dt.loc, chrono::mzone(&dt).zoff, 153 ymd.0, ymd.1, ymd.2, 154 0, 0, 0, 0, 155 )!; 156 case unit::DAY => 157 yield new(dt.loc, chrono::mzone(&dt).zoff, 158 _year(&dt), _month(&dt), _day(&dt), 159 0, 0, 0, 0, 160 )!; 161 case unit::HOUR => 162 yield new(dt.loc, chrono::mzone(&dt).zoff, 163 _year(&dt), _month(&dt), _day(&dt), 164 _hour(&dt), 0, 0, 0, 165 )!; 166 case unit::MINUTE => 167 yield new(dt.loc, chrono::mzone(&dt).zoff, 168 _year(&dt), _month(&dt), _day(&dt), 169 _hour(&dt), _minute(&dt), 0, 0, 170 )!; 171 case unit::SECOND => 172 yield new(dt.loc, chrono::mzone(&dt).zoff, 173 _year(&dt), _month(&dt), _day(&dt), 174 _hour(&dt), _minute(&dt), _second(&dt), 0, 175 )!; 176 case unit::NANOSECOND => 177 yield dt; 178 }; 179 }; 180 181 @test fn pdiff() void = { 182 const cases = [ 183 ( 184 new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, 185 new(chrono::UTC, 0, 2022, 2, 16, 0, 0, 0, 0)!, 186 period { 187 years = 1, 188 months = 1, 189 days = 1, 190 ... 191 }, 192 ), 193 ( 194 new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, 195 new(chrono::UTC, 0, 2022, 3, 27, 0, 0, 0, 0)!, 196 period { 197 years = 1, 198 months = 2, 199 days = 12, 200 ... 201 }, 202 ), 203 ( 204 new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, 205 new(chrono::UTC, 0, 2022, 3, 14, 0, 0, 0, 0)!, 206 period { 207 years = 1, 208 months = 1, 209 days = 27, 210 ... 211 }, 212 ), 213 ( 214 new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, 215 new(chrono::UTC, 0, 2021, 1, 16, 0, 0, 0, 0)!, 216 period { 217 days = 1, 218 ... 219 }, 220 ), 221 ( 222 new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, 223 new(chrono::UTC, 0, 2021, 1, 16, 1, 3, 2, 4)!, 224 period { 225 days = 1, 226 hours = 1, 227 minutes = 3, 228 seconds = 2, 229 nanoseconds = 4, 230 ... 231 }, 232 ), 233 ( 234 new(chrono::UTC, 0, 2021, 1, 15, 2, 3, 2, 2)!, 235 new(chrono::UTC, 0, 2021, 1, 16, 1, 1, 2, 4)!, 236 period { 237 hours = 22, 238 minutes = 58, 239 nanoseconds = 2, 240 ... 241 }, 242 ), 243 ( 244 new(chrono::UTC, 0, 500, 1, 1, 0, 0, 0, 0)!, 245 new(chrono::UTC, 0, 3500, 1, 1, 0, 6, 0, 0)!, 246 period { 247 years = 3000, 248 minutes = 6, 249 ... 250 }, 251 ), 252 ( 253 new(chrono::UTC, 0, -500, 1, 1, 0, 0, 0, 0)!, 254 new(chrono::UTC, 0, 2500, 1, 1, 0, 6, 0, 0)!, 255 period { 256 years = 3000, 257 minutes = 6, 258 ... 259 }, 260 ), 261 ( 262 new(chrono::UTC, 0, 2000, 1, 1, 0, 0, 0, 0)!, 263 new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!, 264 period { 265 minutes = 6, 266 nanoseconds = 999999999, 267 ... 268 }, 269 ), 270 ( 271 new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!, 272 new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 1, 0)!, 273 period { 274 nanoseconds = 1, 275 ... 276 }, 277 ), 278 ( 279 new(chrono::UTC, 0, -4000, 1, 1, 0, 6, 0, 999999999)!, 280 new(chrono::UTC, 0, 4000, 1, 1, 0, 6, 1, 0)!, 281 period { 282 years = 8000, 283 nanoseconds = 1, 284 ... 285 }, 286 ), 287 ]; 288 for (let i = 0z; i < len(cases); i += 1) { 289 const dta = cases[i].0; 290 const dtb = cases[i].1; 291 const expected = cases[i].2; 292 const actual = pdiff(dta, dtb); 293 assert(peq(actual, expected), "pdiff miscalculation"); 294 }; 295 }; 296 297 @test fn unitdiff() void = { 298 const cases = [ 299 ( 300 new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!, 301 new(chrono::UTC, 0, 2022, 1, 5, 13, 53, 30, 20)!, 302 (27, 328, 1427, 9993, 239834, 14390073, 863404409i64, 303 (863404409i64 * time::SECOND) + 18), 304 ), 305 ( 306 new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, 307 new(chrono::UTC, 0, 1994, 8, 28, 11, 20, 1, 2)!, 308 (0, 0, 0, 1, 24, 1440, 86400i64, 309 (86400i64 * time::SECOND) + 2), 310 ), 311 ( 312 new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, 313 new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, 314 (0, 0, 0, 0, 0, 0, 0i64, 0i64), 315 ), 316 ( 317 new(chrono::UTC, 0, -500, 1, 1, 0, 59, 1, 0)!, 318 new(chrono::UTC, 0, 2000, 1, 1, 23, 1, 1, 0)!, 319 (2500, 30000, 130443, 913106, 913106 * 24 + 22, 320 (913106 * 24 + 22) * 60 + 2, 321 ((913106 * 24 + 22) * 60 + 2) * 60i64, 322 (((913106 * 24 + 22) * 60 + 2) * 60i64 * 323 time::SECOND)), 324 ), 325 ]; 326 for (let i = 0z; i < len(cases); i += 1) { 327 const dta = cases[i].0; 328 const dtb = cases[i].1; 329 const expected = cases[i].2; 330 assert(unitdiff(dta, dtb, unit::YEAR) == expected.0, 331 "invalid diff_in_years() result"); 332 assert(unitdiff(dta, dtb, unit::MONTH) == expected.1, 333 "invalid diff_in_months() result"); 334 assert(unitdiff(dta, dtb, unit::WEEK) == expected.2, 335 "invalid diff_in_weeks() result"); 336 assert(unitdiff(dta, dtb, unit::DAY) == expected.3, 337 "invalid diff_in_days() result"); 338 assert(unitdiff(dta, dtb, unit::HOUR) == expected.4, 339 "invalid diff_in_hours() result"); 340 assert(unitdiff(dta, dtb, unit::MINUTE) == expected.5, 341 "invalid diff_in_minutes() result"); 342 assert(unitdiff(dta, dtb, unit::SECOND) == expected.6, 343 "invalid diff_in_seconds() result"); 344 assert(unitdiff(dta, dtb, unit::NANOSECOND) == expected.7, 345 "invalid diff_in_nanoseconds() result"); 346 }; 347 }; 348 349 @test fn truncate() void = { 350 const dt = new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!; 351 352 assert(chrono::eq( 353 &truncate(dt, unit::ERA), 354 &new(chrono::UTC, 0, 1, 1, 1, 0, 0, 0, 0)!)!, 355 "invalid truncate() result 01"); 356 357 assert(chrono::eq( 358 &truncate(dt, unit::YEAR), 359 &new(chrono::UTC, 0, 1994, 1, 1, 0, 0, 0, 0)!)!, 360 "invalid truncate() result 02"); 361 362 assert(chrono::eq( 363 &truncate(dt, unit::MONTH), 364 &new(chrono::UTC, 0, 1994, 8, 1, 0, 0, 0, 0)!)!, 365 "invalid truncate() result 03"); 366 367 assert(chrono::eq( 368 &truncate(dt, unit::WEEK), 369 &new(chrono::UTC, 0, 1994, 8, 22, 0, 0, 0, 0)!)!, 370 "invalid truncate() result 04"); 371 372 assert(chrono::eq( 373 &truncate(dt, unit::DAY), 374 &new(chrono::UTC, 0, 1994, 8, 27, 0, 0, 0, 0)!)!, 375 "invalid truncate() result 05"); 376 377 assert(chrono::eq( 378 &truncate(dt, unit::HOUR), 379 &new(chrono::UTC, 0, 1994, 8, 27, 11, 0, 0, 0)!)!, 380 "invalid truncate() result 06"); 381 382 assert(chrono::eq( 383 &truncate(dt, unit::MINUTE), 384 &new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 0, 0)!)!, 385 "invalid truncate() result 07"); 386 387 assert(chrono::eq( 388 &truncate(dt, unit::SECOND), 389 &new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!)!, 390 "invalid truncate() result 08"); 391 392 assert(chrono::eq( 393 &truncate(dt, unit::NANOSECOND), 394 &dt)!, 395 "invalid truncate() result 09"); 396 };