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 }