1 /++ 2 Various functions that deal with [core.time.Duration|Duration]s. 3 4 Copyright: [JR](https://github.com/zorael) 5 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 6 7 Authors: 8 [JR](https://github.com/zorael) 9 +/ 10 module kameloso.time; 11 12 private: 13 14 import std.datetime.systime : SysTime; 15 import std.range.primitives : isOutputRange; 16 import std.typecons : Flag, No, Yes; 17 import core.time : Duration; 18 19 public: 20 21 22 // timeSinceInto 23 /++ 24 Express how much time has passed in a [core.time.Duration|Duration], in 25 natural (English) language. Overload that writes the result to the passed 26 output range `sink`. 27 28 Example: 29 --- 30 Appender!(char[]) sink; 31 32 immutable then = Clock.currTime; 33 Thread.sleep(1.seconds); 34 immutable now = Clock.currTime; 35 36 immutable duration = (now - then); 37 immutable inEnglish = duration.timeSinceInto(sink); 38 --- 39 40 Params: 41 abbreviate = Whether or not to abbreviate the output, using `h` instead 42 of `hours`, `m` instead of `minutes`, etc. 43 numUnits = Number of units to include in the output text, where such is 44 "weeks", "days", "hours", "minutes" and "seconds", a fake approximate 45 unit "months", and a fake "years" based on it. Passing a `numUnits` 46 of 7 will express the time difference using all units. Passing one 47 of 4 will only express it in days, hours, minutes and seconds. 48 Passing 1 will express it in only seconds. 49 truncateUnits = Number of units to skip from output, going from least 50 significant (seconds) to most significant (years). 51 roundUp = Whether to round up or floor seconds, minutes and hours. 52 Larger units are floored regardless of this setting. 53 signedDuration = A period of time. 54 sink = Output buffer sink to write to. 55 +/ 56 void timeSinceInto(uint numUnits = 7, uint truncateUnits = 0, Sink) 57 (const Duration signedDuration, 58 auto ref Sink sink, 59 const Flag!"abbreviate" abbreviate = No.abbreviate, 60 const Flag!"roundUp" roundUp = Yes.roundUp) pure 61 if (isOutputRange!(Sink, char[])) 62 { 63 import lu.conv : toAlphaInto; 64 import lu.string : plurality; 65 import std.algorithm.comparison : min; 66 import std.format : formattedWrite; 67 import std.meta : AliasSeq; 68 69 static if ((numUnits < 1) || (numUnits > 7)) 70 { 71 import std.format : format; 72 73 enum pattern = "Invalid number of units passed to `timeSinceInto`: " ~ 74 "expected `1` to `7`, got `%d`"; 75 enum message = pattern.format(numUnits); 76 static assert(0, message); 77 } 78 79 static if ((truncateUnits < 0) || (truncateUnits > 6)) 80 { 81 import std.format : format; 82 83 enum pattern = "Invalid number of units to truncate passed to `timeSinceInto`: " ~ 84 "expected `0` to `6`, got `%d`"; 85 enum message = pattern.format(truncateUnits); 86 static assert(0, message); 87 } 88 89 immutable duration = signedDuration < Duration.zero ? -signedDuration : signedDuration; 90 91 alias units = AliasSeq!("weeks", "days", "hours", "minutes", "seconds"); 92 enum daysInAMonth = 30; // The real average is 30.42 but we get unintuitive results. 93 94 immutable diff = duration.split!(units[units.length-min(numUnits, 5)..$]); 95 96 bool putSomething; 97 98 static if (numUnits >= 1) 99 { 100 immutable trailingSeconds = (diff.seconds && (truncateUnits < 1)); 101 } 102 103 static if (numUnits >= 2) 104 { 105 immutable trailingMinutes = (diff.minutes && (truncateUnits < 2)); 106 long minutes = diff.minutes; 107 108 if (roundUp) 109 { 110 if ((diff.seconds >= 30) && (truncateUnits > 0)) 111 { 112 ++minutes; 113 } 114 } 115 } 116 117 static if (numUnits >= 3) 118 { 119 immutable trailingHours = (diff.hours && (truncateUnits < 3)); 120 long hours = diff.hours; 121 122 if (roundUp) 123 { 124 if (minutes == 60) 125 { 126 minutes = 0; 127 ++hours; 128 } 129 else if ((minutes >= 30) && (truncateUnits > 1)) 130 { 131 ++hours; 132 } 133 } 134 } 135 136 static if (numUnits >= 4) 137 { 138 immutable trailingDays = (diff.days && (truncateUnits < 4)); 139 long days = diff.days; 140 141 if (roundUp) 142 { 143 if (hours == 24) 144 { 145 hours = 0; 146 ++days; 147 } 148 } 149 } 150 151 static if (numUnits >= 5) 152 { 153 immutable trailingWeeks = (diff.weeks && (truncateUnits < 5)); 154 long weeks = diff.weeks; 155 156 if (roundUp) 157 { 158 if (days == 7) 159 { 160 days = 0; 161 ++weeks; 162 } 163 } 164 } 165 166 static if (numUnits >= 6) 167 { 168 uint months; 169 170 { 171 immutable totalDays = (weeks * 7) + days; 172 months = cast(uint)(totalDays / daysInAMonth); 173 days = cast(uint)(totalDays % daysInAMonth); 174 weeks = (days / 7); 175 days %= 7; 176 } 177 } 178 179 static if (numUnits >= 7) 180 { 181 uint years; 182 183 if (months >= 12) // && (truncateUnits < 7)) 184 { 185 years = cast(uint)(months / 12); 186 months %= 12; 187 } 188 } 189 190 // ------------------------------------------------------------------------- 191 192 if (signedDuration < Duration.zero) 193 { 194 sink.put('-'); 195 } 196 197 static if (numUnits >= 7) 198 { 199 if (years) 200 { 201 years.toAlphaInto(sink); 202 203 if (abbreviate) 204 { 205 //sink.formattedWrite("%dy", years); 206 sink.put('y'); 207 } 208 else 209 { 210 /*sink.formattedWrite("%d %s", years, 211 years.plurality("year", "years"));*/ 212 sink.put(years.plurality(" year", " years")); 213 } 214 215 putSomething = true; 216 } 217 } 218 219 static if (numUnits >= 6) 220 { 221 if (months && (!putSomething || (truncateUnits < 6))) 222 { 223 if (abbreviate) 224 { 225 static if (numUnits >= 7) 226 { 227 if (putSomething) sink.put(' '); 228 } 229 230 //sink.formattedWrite("%dm", months); 231 months.toAlphaInto(sink); 232 sink.put('m'); 233 } 234 else 235 { 236 static if (numUnits >= 7) 237 { 238 if (putSomething) 239 { 240 if (trailingSeconds || 241 trailingMinutes || 242 trailingHours || 243 trailingDays || 244 trailingWeeks) 245 { 246 sink.put(", "); 247 } 248 else 249 { 250 sink.put(" and "); 251 } 252 } 253 } 254 255 /*sink.formattedWrite("%d %s", months, 256 months.plurality("month", "months"));*/ 257 months.toAlphaInto(sink); 258 sink.put(months.plurality(" month", " months")); 259 } 260 261 putSomething = true; 262 } 263 } 264 265 static if (numUnits >= 5) 266 { 267 if (weeks && (!putSomething || (truncateUnits < 5))) 268 { 269 if (abbreviate) 270 { 271 static if (numUnits >= 6) 272 { 273 if (putSomething) sink.put(' '); 274 } 275 276 //sink.formattedWrite("%dw", weeks); 277 weeks.toAlphaInto(sink); 278 sink.put('w'); 279 } 280 else 281 { 282 static if (numUnits >= 6) 283 { 284 if (putSomething) 285 { 286 if (trailingSeconds || 287 trailingMinutes || 288 trailingHours || 289 trailingDays) 290 { 291 sink.put(", "); 292 } 293 else 294 { 295 sink.put(" and "); 296 } 297 } 298 } 299 300 /*sink.formattedWrite("%d %s", weeks, 301 weeks.plurality("week", "weeks"));*/ 302 weeks.toAlphaInto(sink); 303 sink.put(weeks.plurality(" week", " weeks")); 304 } 305 306 putSomething = true; 307 } 308 } 309 310 static if (numUnits >= 4) 311 { 312 if (days && (!putSomething || (truncateUnits < 4))) 313 { 314 if (abbreviate) 315 { 316 static if (numUnits >= 5) 317 { 318 if (putSomething) sink.put(' '); 319 } 320 321 //sink.formattedWrite("%dd", days); 322 days.toAlphaInto(sink); 323 sink.put('d'); 324 } 325 else 326 { 327 static if (numUnits >= 5) 328 { 329 if (putSomething) 330 { 331 if (trailingSeconds || 332 trailingMinutes || 333 trailingHours) 334 { 335 sink.put(", "); 336 } 337 else 338 { 339 sink.put(" and "); 340 } 341 } 342 } 343 344 /*sink.formattedWrite("%d %s", days, 345 days.plurality("day", "days"));*/ 346 days.toAlphaInto(sink); 347 sink.put(days.plurality(" day", " days")); 348 } 349 350 putSomething = true; 351 } 352 } 353 354 static if (numUnits >= 3) 355 { 356 if (hours && (!putSomething || (truncateUnits < 3))) 357 { 358 if (abbreviate) 359 { 360 static if (numUnits >= 4) 361 { 362 if (putSomething) sink.put(' '); 363 } 364 365 //sink.formattedWrite("%dh", hours); 366 hours.toAlphaInto(sink); 367 sink.put('h'); 368 } 369 else 370 { 371 static if (numUnits >= 4) 372 { 373 if (putSomething) 374 { 375 if (trailingSeconds || 376 trailingMinutes) 377 { 378 sink.put(", "); 379 } 380 else 381 { 382 sink.put(" and "); 383 } 384 } 385 } 386 387 /*sink.formattedWrite("%d %s", hours, 388 hours.plurality("hour", "hours"));*/ 389 hours.toAlphaInto(sink); 390 sink.put(hours.plurality(" hour", " hours")); 391 } 392 393 putSomething = true; 394 } 395 } 396 397 static if (numUnits >= 2) 398 { 399 if (minutes && (!putSomething || (truncateUnits < 2))) 400 { 401 if (abbreviate) 402 { 403 static if (numUnits >= 3) 404 { 405 if (putSomething) sink.put(' '); 406 } 407 408 //sink.formattedWrite("%dm", minutes); 409 minutes.toAlphaInto(sink); 410 sink.put('m'); 411 } 412 else 413 { 414 static if (numUnits >= 3) 415 { 416 if (putSomething) 417 { 418 if (trailingSeconds) 419 { 420 sink.put(", "); 421 } 422 else 423 { 424 sink.put(" and "); 425 } 426 } 427 } 428 429 /*sink.formattedWrite("%d %s", minutes, 430 minutes.plurality("minute", "minutes"));*/ 431 minutes.toAlphaInto(sink); 432 sink.put(minutes.plurality(" minute", " minutes")); 433 } 434 435 putSomething = true; 436 } 437 } 438 439 if (trailingSeconds || !putSomething) 440 { 441 if (abbreviate) 442 { 443 if (putSomething) 444 { 445 sink.put(' '); 446 } 447 448 //sink.formattedWrite("%ds", diff.seconds); 449 diff.seconds.toAlphaInto(sink); 450 sink.put('s'); 451 } 452 else 453 { 454 if (putSomething) 455 { 456 sink.put(" and "); 457 } 458 459 /*sink.formattedWrite("%d %s", diff.seconds, 460 diff.seconds.plurality("second", "seconds"));*/ 461 diff.seconds.toAlphaInto(sink); 462 sink.put(diff.seconds.plurality(" second", " seconds")); 463 } 464 } 465 } 466 467 /// 468 unittest 469 { 470 import std.array : Appender; 471 import core.time; 472 473 Appender!(char[]) sink; 474 sink.reserve(64); // workaround for formattedWrite < 2.076 475 476 { 477 immutable dur = Duration.zero; 478 dur.timeSinceInto(sink); 479 assert((sink.data == "0 seconds"), sink.data); 480 sink.clear(); 481 dur.timeSinceInto(sink, Yes.abbreviate); 482 assert((sink.data == "0s"), sink.data); 483 sink.clear(); 484 } 485 { 486 immutable dur = 3_141_519_265.msecs; 487 dur.timeSinceInto!(4, 1)(sink, No.abbreviate, No.roundUp); 488 assert((sink.data == "36 days, 8 hours and 38 minutes"), sink.data); 489 sink.clear(); 490 dur.timeSinceInto!(4, 1)(sink, Yes.abbreviate, No.roundUp); 491 assert((sink.data == "36d 8h 38m"), sink.data); 492 sink.clear(); 493 } 494 { 495 immutable dur = 3_141_519_265.msecs; 496 dur.timeSinceInto!(4, 1)(sink, No.abbreviate, Yes.roundUp); 497 assert((sink.data == "36 days, 8 hours and 39 minutes"), sink.data); 498 sink.clear(); 499 dur.timeSinceInto!(4, 1)(sink, Yes.abbreviate, Yes.roundUp); 500 assert((sink.data == "36d 8h 39m"), sink.data); 501 sink.clear(); 502 } 503 { 504 immutable dur = 3599.seconds; 505 dur.timeSinceInto!(2, 1)(sink, No.abbreviate, No.roundUp); 506 assert((sink.data == "59 minutes"), sink.data); 507 sink.clear(); 508 dur.timeSinceInto!(2, 1)(sink, Yes.abbreviate, No.roundUp); 509 assert((sink.data == "59m"), sink.data); 510 sink.clear(); 511 } 512 { 513 immutable dur = 3599.seconds; 514 dur.timeSinceInto!(2, 1)(sink, No.abbreviate, Yes.roundUp); 515 assert((sink.data == "60 minutes"), sink.data); 516 sink.clear(); 517 dur.timeSinceInto!(2, 1)(sink, Yes.abbreviate, Yes.roundUp); 518 assert((sink.data == "60m"), sink.data); 519 sink.clear(); 520 } 521 { 522 immutable dur = 3599.seconds; 523 dur.timeSinceInto!(3, 1)(sink, No.abbreviate, Yes.roundUp); 524 assert((sink.data == "1 hour"), sink.data); 525 sink.clear(); 526 dur.timeSinceInto!(3, 1)(sink, Yes.abbreviate, Yes.roundUp); 527 assert((sink.data == "1h"), sink.data); 528 sink.clear(); 529 } 530 { 531 immutable dur = 3.days + 35.minutes; 532 dur.timeSinceInto!(4, 1)(sink, No.abbreviate, No.roundUp); 533 assert((sink.data == "3 days and 35 minutes"), sink.data); 534 sink.clear(); 535 dur.timeSinceInto!(4, 1)(sink, Yes.abbreviate, No.roundUp); 536 assert((sink.data == "3d 35m"), sink.data); 537 sink.clear(); 538 } 539 { 540 immutable dur = 3.days + 35.minutes; 541 dur.timeSinceInto!(4, 2)(sink, No.abbreviate, Yes.roundUp); 542 assert((sink.data == "3 days and 1 hour"), sink.data); 543 sink.clear(); 544 dur.timeSinceInto!(4, 2)(sink, Yes.abbreviate, Yes.roundUp); 545 assert((sink.data == "3d 1h"), sink.data); 546 sink.clear(); 547 } 548 { 549 immutable dur = 57.weeks + 1.days + 2.hours + 3.minutes + 4.seconds; 550 dur.timeSinceInto!(7, 4)(sink, No.abbreviate); 551 assert((sink.data == "1 year, 1 month and 1 week"), sink.data); 552 sink.clear(); 553 dur.timeSinceInto!(7, 4)(sink, Yes.abbreviate); 554 assert((sink.data == "1y 1m 1w"), sink.data); 555 sink.clear(); 556 } 557 { 558 immutable dur = 4.seconds; 559 dur.timeSinceInto!(7, 4)(sink, No.abbreviate); 560 assert((sink.data == "4 seconds"), sink.data); 561 sink.clear(); 562 dur.timeSinceInto!(7, 4)(sink, Yes.abbreviate); 563 assert((sink.data == "4s"), sink.data); 564 sink.clear(); 565 } 566 { 567 immutable dur = 2.hours + 28.minutes + 19.seconds; 568 dur.timeSinceInto!(7, 1)(sink, No.abbreviate); 569 assert((sink.data == "2 hours and 28 minutes"), sink.data); 570 sink.clear(); 571 dur.timeSinceInto!(7, 1)(sink, Yes.abbreviate); 572 assert((sink.data == "2h 28m"), sink.data); 573 sink.clear(); 574 } 575 { 576 immutable dur = -1.minutes + -1.seconds; 577 dur.timeSinceInto!(2, 0)(sink, No.abbreviate); 578 assert((sink.data == "-1 minute and 1 second"), sink.data); 579 sink.clear(); 580 dur.timeSinceInto!(2, 0)(sink, Yes.abbreviate); 581 assert((sink.data == "-1m 1s"), sink.data); 582 sink.clear(); 583 } 584 { 585 immutable dur = 30.seconds; 586 dur.timeSinceInto!(3, 1)(sink, No.abbreviate, No.roundUp); 587 assert((sink.data == "30 seconds"), sink.data); 588 sink.clear(); 589 dur.timeSinceInto!(3, 1)(sink, Yes.abbreviate, No.roundUp); 590 assert((sink.data == "30s"), sink.data); 591 sink.clear(); 592 } 593 { 594 immutable dur = 30.seconds; 595 dur.timeSinceInto!(3, 1)(sink, No.abbreviate, Yes.roundUp); 596 assert((sink.data == "1 minute"), sink.data); 597 sink.clear(); 598 dur.timeSinceInto!(3, 1)(sink, Yes.abbreviate, Yes.roundUp); 599 assert((sink.data == "1m"), sink.data); 600 sink.clear(); 601 } 602 { 603 immutable dur = 23.hours + 59.minutes + 59.seconds; 604 dur.timeSinceInto!(5, 3)(sink, No.abbreviate, Yes.roundUp); 605 assert((sink.data == "1 day"), sink.data); 606 sink.clear(); 607 dur.timeSinceInto!(5, 3)(sink, Yes.abbreviate, Yes.roundUp); 608 assert((sink.data == "1d"), sink.data); 609 sink.clear(); 610 } 611 { 612 immutable dur = 6.days + 23.hours + 59.minutes; 613 dur.timeSinceInto!(5, 4)(sink, No.abbreviate, No.roundUp); 614 assert((sink.data == "6 days"), sink.data); 615 sink.clear(); 616 dur.timeSinceInto!(5, 4)(sink, Yes.abbreviate, No.roundUp); 617 assert((sink.data == "6d"), sink.data); 618 sink.clear(); 619 } 620 { 621 immutable dur = 6.days + 23.hours + 59.minutes; 622 dur.timeSinceInto!(5, 4)(sink, No.abbreviate, Yes.roundUp); 623 assert((sink.data == "1 week"), sink.data); 624 sink.clear(); 625 dur.timeSinceInto!(5, 4)(sink, Yes.abbreviate, Yes.roundUp); 626 assert((sink.data == "1w"), sink.data); 627 sink.clear(); 628 } 629 } 630 631 632 // timeSince 633 /++ 634 Express how much time has passed in a [core.time.Duration|Duration], in natural 635 (English) language. Overload that returns the result as a new string. 636 637 Example: 638 --- 639 immutable then = Clock.currTime; 640 Thread.sleep(1.seconds); 641 immutable now = Clock.currTime; 642 643 immutable duration = (now - then); 644 immutable inEnglish = timeSince(duration); 645 --- 646 647 Params: 648 abbreviate = Whether or not to abbreviate the output, using `h` instead 649 of `hours`, `m` instead of `minutes`, etc. 650 numUnits = Number of units to include in the output text, where such is 651 "weeks", "days", "hours", "minutes" and "seconds", a fake approximate 652 unit "months", and a fake "years" based on it. Passing a `numUnits` 653 of 7 will express the time difference using all units. Passing one 654 of 4 will only express it in days, hours, minutes and seconds. 655 Passing 1 will express it in only seconds. 656 truncateUnits = Number of units to skip from output, going from least 657 significant (seconds) to most significant (years). 658 roundUp = Whether to round up or floor seconds, minutes and hours. 659 Larger units are floored regardless of this setting. 660 duration = A period of time. 661 662 Returns: 663 A string with the passed duration expressed in natural English language. 664 +/ 665 string timeSince(uint numUnits = 7, uint truncateUnits = 0) 666 (const Duration duration, 667 const Flag!"abbreviate" abbreviate = No.abbreviate, 668 const Flag!"roundUp" roundUp = Yes.roundUp) pure 669 { 670 import std.array : Appender; 671 672 Appender!(char[]) sink; 673 sink.reserve(64); 674 duration.timeSinceInto!(numUnits, truncateUnits)(sink, abbreviate, roundUp); 675 return sink.data; 676 } 677 678 /// 679 unittest 680 { 681 import core.time; 682 683 { 684 immutable dur = 789_383.seconds; // 1 week, 2 days, 3 hours, 16 minutes, and 23 secs 685 immutable since = dur.timeSince!(4, 1)(No.abbreviate); 686 immutable abbrev = dur.timeSince!(4, 1)(Yes.abbreviate); 687 assert((since == "9 days, 3 hours and 16 minutes"), since); 688 assert((abbrev == "9d 3h 16m"), abbrev); 689 } 690 { 691 immutable dur = 789_383.seconds; // 1 week, 2 days, 3 hours, 16 minutes, and 23 secs 692 immutable since = dur.timeSince!(5, 1)(No.abbreviate); 693 immutable abbrev = dur.timeSince!(5, 1)(Yes.abbreviate); 694 assert((since == "1 week, 2 days, 3 hours and 16 minutes"), since); 695 assert((abbrev == "1w 2d 3h 16m"), abbrev); 696 } 697 { 698 immutable dur = 789_383.seconds; 699 immutable since = dur.timeSince!(1)(No.abbreviate); 700 immutable abbrev = dur.timeSince!(1)(Yes.abbreviate); 701 assert((since == "789383 seconds"), since); 702 assert((abbrev == "789383s"), abbrev); 703 } 704 { 705 immutable dur = 789_383.seconds; 706 immutable since = dur.timeSince!(2, 0)(No.abbreviate); 707 immutable abbrev = dur.timeSince!(2, 0)(Yes.abbreviate); 708 assert((since == "13156 minutes and 23 seconds"), since); 709 assert((abbrev == "13156m 23s"), abbrev); 710 } 711 { 712 immutable dur = 3_620.seconds; // 1 hour and 20 secs 713 immutable since = dur.timeSince!(7, 1)(No.abbreviate); 714 immutable abbrev = dur.timeSince!(7, 1)(Yes.abbreviate); 715 assert((since == "1 hour"), since); 716 assert((abbrev == "1h"), abbrev); 717 } 718 { 719 immutable dur = 30.seconds; // 30 secs 720 immutable since = dur.timeSince; 721 immutable abbrev = dur.timeSince(Yes.abbreviate); 722 assert((since == "30 seconds"), since); 723 assert((abbrev == "30s"), abbrev); 724 } 725 { 726 immutable dur = 1.seconds; 727 immutable since = dur.timeSince; 728 immutable abbrev = dur.timeSince(Yes.abbreviate); 729 assert((since == "1 second"), since); 730 assert((abbrev == "1s"), abbrev); 731 } 732 { 733 immutable dur = 1.days + 1.minutes + 1.seconds; 734 immutable since = dur.timeSince!(7, 0)(No.abbreviate); 735 immutable abbrev = dur.timeSince!(7, 0)(Yes.abbreviate); 736 assert((since == "1 day, 1 minute and 1 second"), since); 737 assert((abbrev == "1d 1m 1s"), abbrev); 738 } 739 { 740 immutable dur = 3.weeks + 6.days + 10.hours; 741 immutable since = dur.timeSince(No.abbreviate); 742 immutable abbrev = dur.timeSince(Yes.abbreviate); 743 assert((since == "3 weeks, 6 days and 10 hours"), since); 744 assert((abbrev == "3w 6d 10h"), abbrev); 745 } 746 { 747 immutable dur = 377.days + 11.hours; 748 immutable since = dur.timeSince!(6)(No.abbreviate); 749 immutable abbrev = dur.timeSince!(6)(Yes.abbreviate); 750 assert((since == "12 months, 2 weeks, 3 days and 11 hours"), since); 751 assert((abbrev == "12m 2w 3d 11h"), abbrev); 752 } 753 { 754 immutable dur = 395.days + 11.seconds; 755 immutable since = dur.timeSince!(7, 1)(No.abbreviate); 756 immutable abbrev = dur.timeSince!(7, 1)(Yes.abbreviate); 757 assert((since == "1 year, 1 month and 5 days"), since); 758 assert((abbrev == "1y 1m 5d"), abbrev); 759 } 760 { 761 immutable dur = 1.weeks + 9.days; 762 immutable since = dur.timeSince!(5)(No.abbreviate); 763 immutable abbrev = dur.timeSince!(5)(Yes.abbreviate); 764 assert((since == "2 weeks and 2 days"), since); 765 assert((abbrev == "2w 2d"), abbrev); 766 } 767 { 768 immutable dur = 30.days + 1.weeks; 769 immutable since = dur.timeSince!(5)(No.abbreviate); 770 immutable abbrev = dur.timeSince!(5)(Yes.abbreviate); 771 assert((since == "5 weeks and 2 days"), since); 772 assert((abbrev == "5w 2d"), abbrev); 773 } 774 { 775 immutable dur = 30.days + 1.weeks + 1.seconds; 776 immutable since = dur.timeSince!(4, 0)(No.abbreviate); 777 immutable abbrev = dur.timeSince!(4, 0)(Yes.abbreviate); 778 assert((since == "37 days and 1 second"), since); 779 assert((abbrev == "37d 1s"), abbrev); 780 } 781 { 782 immutable dur = 267.weeks + 4.days + 9.hours + 15.minutes + 1.seconds; 783 immutable since = dur.timeSince!(7, 0)(No.abbreviate); 784 immutable abbrev = dur.timeSince!(7, 0)(Yes.abbreviate); 785 assert((since == "5 years, 2 months, 1 week, 6 days, 9 hours, 15 minutes and 1 second"), since); 786 assert((abbrev == "5y 2m 1w 6d 9h 15m 1s"), abbrev); 787 } 788 { 789 immutable dur = 360.days + 350.days; 790 immutable since = dur.timeSince!(7, 6)(No.abbreviate); 791 immutable abbrev = dur.timeSince!(7, 6)(Yes.abbreviate); 792 assert((since == "1 year"), since); 793 assert((abbrev == "1y"), abbrev); 794 } 795 { 796 immutable dur = 267.weeks + 4.days + 9.hours + 15.minutes + 1.seconds; 797 immutable since = dur.timeSince!(7, 3)(No.abbreviate); 798 immutable abbrev = dur.timeSince!(7, 3)(Yes.abbreviate); 799 assert((since == "5 years, 2 months, 1 week and 6 days"), since); 800 assert((abbrev == "5y 2m 1w 6d"), abbrev); 801 } 802 } 803 804 805 // nextMidnight 806 /++ 807 Returns a [std.datetime.systime.SysTime|SysTime] of the following midnight. 808 809 Example: 810 --- 811 immutable now = Clock.currTime; 812 immutable midnight = now.nextMidnight; 813 writeln("Time until next midnight: ", (midnight - now)); 814 --- 815 816 Params: 817 now = A [std.datetime.systime.SysTime|SysTime] of the base date from 818 which to proceed to the next midnight. 819 820 Returns: 821 A [std.datetime.systime.SysTime|SysTime] of the midnight following the date 822 passed as argument. 823 +/ 824 auto nextMidnight(const SysTime now) 825 { 826 import std.datetime : DateTime; 827 import std.datetime.systime : SysTime; 828 829 /+ 830 The difference between rolling and adding is that rolling does not affect 831 larger units. For instance, rolling a SysTime one year's worth of days 832 gets the exact same SysTime. 833 +/ 834 835 auto next = SysTime(DateTime(now.year, now.month, now.day, 0, 0, 0), now.timezone) 836 .roll!"days"(1); 837 838 if (next.day == 1) 839 { 840 next.add!"months"(1); 841 } 842 843 return next; 844 } 845 846 /// 847 unittest 848 { 849 import std.datetime : DateTime; 850 import std.datetime.systime : SysTime; 851 import std.datetime.timezone : UTC; 852 853 immutable utc = UTC(); 854 855 immutable christmasEve = SysTime(DateTime(2018, 12, 24, 12, 34, 56), utc); 856 immutable nextDay = christmasEve.nextMidnight; 857 immutable christmasDay = SysTime(DateTime(2018, 12, 25, 0, 0, 0), utc); 858 assert(nextDay.toUnixTime == christmasDay.toUnixTime); 859 860 immutable someDay = SysTime(DateTime(2018, 6, 30, 12, 27, 56), utc); 861 immutable afterSomeDay = someDay.nextMidnight; 862 immutable afterSomeDayToo = SysTime(DateTime(2018, 7, 1, 0, 0, 0), utc); 863 assert(afterSomeDay == afterSomeDayToo); 864 865 immutable newyearsEve = SysTime(DateTime(2018, 12, 31, 0, 0, 0), utc); 866 immutable newyearsDay = newyearsEve.nextMidnight; 867 immutable alsoNewyearsDay = SysTime(DateTime(2019, 1, 1, 0, 0, 0), utc); 868 assert(newyearsDay == alsoNewyearsDay); 869 870 immutable troubleDay = SysTime(DateTime(2018, 6, 30, 19, 14, 51), utc); 871 immutable afterTrouble = troubleDay.nextMidnight; 872 immutable alsoAfterTrouble = SysTime(DateTime(2018, 7, 1, 0, 0, 0), utc); 873 assert(afterTrouble == alsoAfterTrouble); 874 875 immutable novDay = SysTime(DateTime(2019, 11, 30, 12, 34, 56), utc); 876 immutable decDay = novDay.nextMidnight; 877 immutable alsoDecDay = SysTime(DateTime(2019, 12, 1, 0, 0, 0), utc); 878 assert(decDay == alsoDecDay); 879 880 immutable lastMarch = SysTime(DateTime(2005, 3, 31, 23, 59, 59), utc); 881 immutable firstApril = lastMarch.nextMidnight; 882 immutable alsoFirstApril = SysTime(DateTime(2005, 4, 1, 0, 0, 0), utc); 883 assert(firstApril == alsoFirstApril); 884 } 885 886 887 // abbreviatedDuration 888 /++ 889 Constructs a [core.time.Duration|Duration] from a string, assumed to be in a 890 `*d*h*m*s` pattern. 891 892 Params: 893 line = Abbreviated string line. 894 895 Returns: 896 A [core.time.Duration|Duration] as described in the input string. 897 898 Throws: 899 [DurationStringException] if individually negative values were passed. 900 +/ 901 auto abbreviatedDuration(const string line) 902 { 903 import lu.string : contains, nom; 904 import std.conv : to; 905 import core.time : days, hours, minutes, seconds; 906 907 static int getAbbreviatedValue(ref string slice, const char c) 908 { 909 if (slice.contains(c)) 910 { 911 immutable valueString = slice.nom(c); 912 immutable value = valueString.length ? valueString.to!int : 0; 913 914 if (value < 0) 915 { 916 throw new DurationStringException("Durations cannot have negative values mid-string"); 917 } 918 return value; 919 } 920 return 0; 921 } 922 923 string slice = line; // mutable 924 int sign = 1; 925 926 if (slice.length && (slice[0] == '-')) 927 { 928 sign = -1; 929 slice = slice[1..$]; 930 } 931 932 immutable numDays = getAbbreviatedValue(slice, 'd'); 933 immutable numHours = getAbbreviatedValue(slice, 'h'); 934 immutable numMinutes = getAbbreviatedValue(slice, 'm'); 935 int numSeconds; 936 937 if (slice.length) 938 { 939 immutable valueString = slice.nom!(Yes.inherit)('s'); 940 if (!valueString.length) throw new DurationStringException("Invalid duration pattern"); 941 numSeconds = valueString.length ? valueString.to!int : 0; 942 } 943 944 if ((numDays < 0) || (numHours < 0) || (numMinutes < 0) || (numSeconds < 0)) 945 { 946 throw new DurationStringException("Duration values must not be individually negative"); 947 } 948 949 return sign * (numDays.days + numHours.hours + numMinutes.minutes + numSeconds.seconds); 950 } 951 952 /// 953 unittest 954 { 955 import std.conv : to; 956 import std.exception : assertThrown; 957 import core.time : days, hours, minutes, seconds; 958 959 { 960 enum line = "30"; 961 immutable actual = abbreviatedDuration(line); 962 immutable expected = 30.seconds; 963 assert((actual == expected), actual.to!string); 964 } 965 { 966 enum line = "30s"; 967 immutable actual = abbreviatedDuration(line); 968 immutable expected = 30.seconds; 969 assert((actual == expected), actual.to!string); 970 } 971 { 972 enum line = "1h30s"; 973 immutable actual = abbreviatedDuration(line); 974 immutable expected = 1.hours + 30.seconds; 975 assert((actual == expected), actual.to!string); 976 } 977 { 978 enum line = "5h"; 979 immutable actual = abbreviatedDuration(line); 980 immutable expected = 5.hours; 981 assert((actual == expected), actual.to!string); 982 } 983 { 984 enum line = "1d12h39m40s"; 985 immutable actual = abbreviatedDuration(line); 986 immutable expected = 1.days + 12.hours + 39.minutes + 40.seconds; 987 assert((actual == expected), actual.to!string); 988 } 989 { 990 enum line = "1d4s"; 991 immutable actual = abbreviatedDuration(line); 992 immutable expected = 1.days + 4.seconds; 993 assert((actual == expected), actual.to!string); 994 } 995 { 996 enum line = "30s"; 997 immutable actual = abbreviatedDuration(line); 998 immutable expected = 30.seconds; 999 assert((actual == expected), actual.to!string); 1000 } 1001 { 1002 enum line = "-30s"; 1003 immutable actual = abbreviatedDuration(line); 1004 immutable expected = (-30).seconds; 1005 assert((actual == expected), actual.to!string); 1006 } 1007 { 1008 import core.time : Duration; 1009 enum line = string.init; 1010 immutable actual = abbreviatedDuration(line); 1011 immutable expected = Duration.zero; 1012 assert((actual == expected), actual.to!string); 1013 } 1014 { 1015 enum line = "s"; 1016 assertThrown(abbreviatedDuration(line)); 1017 } 1018 { 1019 enum line = "1d1h1m1z"; 1020 assertThrown(abbreviatedDuration(line)); 1021 } 1022 { 1023 enum line = "2h-30m"; 1024 assertThrown(abbreviatedDuration(line)); 1025 } 1026 } 1027 1028 1029 // DurationStringException 1030 /++ 1031 A normal [object.Exception|Exception] but where its type conveys the specific 1032 context of a call to [abbreviatedDuration] having malformed arguments. 1033 +/ 1034 final class DurationStringException : Exception 1035 { 1036 /++ 1037 Constructor. 1038 +/ 1039 this( 1040 const string message, 1041 const string file = __FILE__, 1042 const size_t line = __LINE__, 1043 Throwable nextInChain = null) pure nothrow @nogc @safe 1044 { 1045 super(message, file, line, nextInChain); 1046 } 1047 }