1 /++
2 Timestamp
3 +/
4 module mir.timestamp;
5 
6 private alias isDigit = (dchar c) => uint(c - '0') < 10;
7 import mir.serde: serdeIgnore;
8 
9 version(D_Exceptions)
10 ///
11 class DateTimeException : Exception
12 {
13     ///
14     @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null)
15     {
16         super(msg, file, line, nextInChain);
17     }
18 
19     /// ditto
20     @nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__)
21     {
22         super(msg, file, line, nextInChain);
23     }
24 }
25 
26 version(D_Exceptions)
27 {
28     private static immutable InvalidMonth = new DateTimeException("Invalid Month");
29     private static immutable InvalidDay = new DateTimeException("Invalid Day");
30     private static immutable InvalidISOString = new DateTimeException("Invalid ISO String");
31     private static immutable InvalidISOExtendedString = new DateTimeException("Invalid ISO Extended String");
32     private static immutable InvalidString = new DateTimeException("Invalid String");
33 }
34 
35 /++
36 Timestamp
37 
38 Note: The component values in the binary encoding are always in UTC, while components in the text encoding are in the local time!
39 This means that transcoding requires a conversion between UTC and local time.
40 
41 `Timestamp` precision is up to picosecond (second/10^12).
42 +/
43 struct Timestamp
44 {
45     import std.traits: isSomeChar;
46 
47     ///
48     enum Precision : ubyte
49     {
50         ///
51         year,
52         ///
53         month,
54         ///
55         day,
56         ///
57         minute,
58         ///
59         second,
60         ///
61         fraction,
62     }
63 
64     ///
65     this(scope const(char)[] str) @safe pure @nogc
66     {
67         this = fromString(str);
68     }
69 
70     ///
71     version (mir_test)
72     @safe pure @nogc unittest
73     {
74         assert(Timestamp("2010-07-04") == Timestamp(2010, 7, 4));
75         assert(Timestamp("20100704") == Timestamp(2010, 7, 4));
76         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730"));
77         static assert(Timestamp(2021, 01, 29,  4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30"));
78 
79         assert(Timestamp("T0740Z") == Timestamp.onlyTime(7, 40));
80         assert(Timestamp("T074030Z") == Timestamp.onlyTime(7, 40, 30));
81         assert(Timestamp("T074030.056Z") == Timestamp.onlyTime(7, 40, 30, -3, 56));
82 
83         assert(Timestamp("07:40Z") == Timestamp.onlyTime(7, 40));
84         assert(Timestamp("07:40:30Z") == Timestamp.onlyTime(7, 40, 30));
85         assert(Timestamp("T07:40:30.056Z") == Timestamp.onlyTime(7, 40, 30, -3, 56));
86     }
87 
88     version(all)
89     {
90         short offset;
91     }
92     else
93     /+
94     If the time in UTC is known, but the offset to local time is unknown, this can be represented with an offset of “-00:00”.
95     This differs semantically from an offset of “Z” or “+00:00”, which imply that UTC is the preferred reference point for the specified time.
96     RFC2822 describes a similar convention for email.
97     private short _offset;
98     +/
99     {
100 
101         /++
102         Timezone offset in minutes
103         +/
104         short offset() const @safe pure nothrow @nogc @property
105         {
106             return _offset >> 1;
107         }
108 
109         /++
110         Returns: true if timezone has offset
111         +/
112         bool hasOffset() const @safe pure nothrow @nogc @property
113         {
114             return _offset & 1;
115         }
116     }
117 
118 @serdeIgnore:
119 
120     /++
121     Year
122     +/
123     short year;
124     /++
125     +/
126     Precision precision;
127 
128     /++
129     Month
130     
131     If the value equals to thero then this and all the following members are undefined.
132     +/
133     ubyte month;
134     /++
135     Day
136     
137     If the value equals to thero then this and all the following members are undefined.
138     +/
139     ubyte day;
140     /++
141     Hour
142     +/
143     ubyte hour;
144 
145     version(D_Ddoc)
146     {
147     
148         /++
149         Minute
150 
151         Note: the field is implemented as property.
152         +/
153         ubyte minute;
154         /++
155         Second
156 
157         Note: the field is implemented as property.
158         +/
159         ubyte second;
160         /++
161         Fraction
162 
163         The `fraction_exponent` and `fraction_coefficient` denote the fractional seconds of the timestamp as a decimal value
164         The fractional seconds’ value is `coefficient * 10 ^ exponent`.
165         It must be greater than or equal to zero and less than 1.
166         A missing coefficient defaults to zero.
167         Fractions whose coefficient is zero and exponent is greater than -1 are ignored.
168         
169         'fractionCoefficient' allowed values are [0 ... 10^12-1].
170         'fractionExponent' allowed values are [-12 ... 0].
171 
172         Note: the fields are implemented as property.
173         +/
174         byte fractionExponent;
175         /// ditto
176         long fractionCoefficient;
177     }
178     else
179     {
180         import mir.bitmanip: bitfields;
181         version (LittleEndian)
182         {
183 
184             mixin(bitfields!(
185                     ubyte, "minute", 8,
186                     ubyte, "second", 8,
187                     byte, "fractionExponent", 8,
188                     long, "fractionCoefficient", 40,
189             ));
190         }
191         else
192         {
193             mixin(bitfields!(
194                     long, "fractionCoefficient", 40,
195                     byte, "fractionExponent", 8,
196                     ubyte, "second", 8,
197                     ubyte, "minute", 8,
198             ));
199         }
200     }
201 
202     ///
203     @safe pure nothrow @nogc
204     this(short year)
205     {
206         this.year = year;
207         this.precision = Precision.year;
208     }
209 
210     ///
211     @safe pure nothrow @nogc
212     this(short year, ubyte month)
213     {
214         this.year = year;
215         this.month = month;
216         this.precision = Precision.month;
217     }
218 
219     ///
220     @safe pure nothrow @nogc
221     this(short year, ubyte month, ubyte day)
222     {
223         this.year = year;
224         this.month = month;
225         this.day = day;
226         this.precision = Precision.day;
227     }
228 
229     ///
230     @safe pure nothrow @nogc
231     this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute)
232     {
233         this.year = year;
234         this.month = month;
235         this.day = day;
236         this.hour = hour;
237         this.minute = minute;
238         this.precision = Precision.minute;
239     }
240 
241     ///
242     @safe pure nothrow @nogc
243     this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second)
244     {
245         this.year = year;
246         this.month = month;
247         this.day = day;
248         this.hour = hour;
249         this.day = day;
250         this.minute = minute;
251         this.second = second;
252         this.precision = Precision.second;
253     }
254 
255     ///
256     @safe pure nothrow @nogc
257     this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient)
258     {
259         this.year = year;
260         this.month = month;
261         this.day = day;
262         this.hour = hour;
263         this.day = day;
264         this.minute = minute;
265         this.second = second;
266         assert(fractionExponent < 0);
267         this.fractionExponent = fractionExponent;
268         this.fractionCoefficient = fractionCoefficient;
269         this.precision = Precision.fraction;
270     }
271 
272     ///
273     @safe pure nothrow @nogc
274     static Timestamp onlyTime(ubyte hour, ubyte minute)
275     {
276         return Timestamp(0, 0, 0, hour, minute);
277     }
278 
279     ///
280     @safe pure nothrow @nogc
281     static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second)
282     {
283         return Timestamp(0, 0, 0, hour, minute, second);
284     }
285 
286     ///
287     @safe pure nothrow @nogc
288     static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient)
289     {
290         return Timestamp(0, 0, 0, hour, minute, second, fractionExponent, fractionCoefficient);
291     }
292 
293     ///
294     this(Date)(const Date datetime)
295         if (Date.stringof == "Date" || Date.stringof == "date")
296     {
297         static if (__traits(hasMember, Date, "yearMonthDay"))
298             with(datetime.yearMonthDay) this(year, cast(ubyte)month, day);
299         else
300             with(datetime) this(year, month, day);
301     }
302 
303     ///
304     version (mir_test)
305     @safe unittest {
306         import mir.date : Date;
307         auto dt = Date(1982, 4, 1);
308         Timestamp ts = dt;
309         assert(ts.opCmp(ts) == 0);
310         assert(dt.toISOExtString == ts.toString);
311         assert(dt == cast(Date) ts);
312     }
313 
314     ///
315     version (mir_test)
316     @safe unittest {
317         import std.datetime.date : Date;
318         auto dt = Date(1982, 4, 1);
319         Timestamp ts = dt;
320         assert(dt.toISOExtString == ts.toString);
321         assert(dt == cast(Date) ts);
322     }
323 
324     ///
325     this(TimeOfDay)(const TimeOfDay timeOfDay)
326         if (TimeOfDay.stringof == "TimeOfDay")
327     {
328         with(timeOfDay) this = onlyTime(hour, minute, second);
329     }
330 
331     ///
332     version (mir_test)
333     @safe unittest {
334         import std.datetime.date : TimeOfDay;
335         auto dt = TimeOfDay(7, 14, 30);
336         Timestamp ts = dt;
337         assert(dt.toISOExtString ~ "Z" == ts.toString);
338         assert(dt == cast(TimeOfDay) ts);
339     }
340 
341     ///
342     this(DateTime)(const DateTime datetime)
343         if (DateTime.stringof == "DateTime")
344     {
345         with(datetime) this(year, cast(ubyte)month, day, hour, minute, second);
346     }
347 
348     ///
349     version (mir_test)
350     @safe unittest {
351         import std.datetime.date : DateTime;
352         auto dt = DateTime(1982, 4, 1, 20, 59, 22);
353         Timestamp ts = dt;
354         assert(dt.toISOExtString ~ "Z" == ts.toString);
355         assert(dt == cast(DateTime) ts);
356     }
357 
358     ///
359     this(SysTime)(const SysTime systime)
360         if (SysTime.stringof == "SysTime")
361     {
362         with(systime.toUTC) this(year, month, day, hour, minute, second, -7, fracSecs.total!"hnsecs");
363         offset = cast(short) systime.utcOffset.total!"minutes";
364     }
365 
366     ///
367     version (mir_test)
368     @safe unittest {
369         import core.time : hnsecs, minutes;
370         import std.datetime.date : DateTime;
371         import std.datetime.timezone : SimpleTimeZone;
372         import std.datetime.systime : SysTime;
373 
374         auto dt = DateTime(1982, 4, 1, 20, 59, 22);
375         auto tz = new immutable SimpleTimeZone(-330.minutes);
376         auto st = SysTime(dt, 1234567.hnsecs, tz);
377         Timestamp ts = st;
378 
379         assert(st.toISOExtString == ts.toString);
380         assert(st == cast(SysTime) ts);
381     }
382 
383     ///
384     T opCast(T)() const
385         if (T.stringof == "YearMonth"
386          || T.stringof == "YearMonthDay"
387          || T.stringof == "Date"
388          || T.stringof == "TimeOfDay"
389          || T.stringof == "date"
390          || T.stringof == "DateTime"
391          || T.stringof == "SysTime")
392     {
393         static if (T.stringof == "YearMonth")
394         {
395             return T(year, month, day);
396         }
397         else
398         static if (T.stringof == "Date" || T.stringof == "date" || T.stringof == "YearMonthDay")
399         {
400             return T(year, month, day);
401         }
402         else
403         static if (T.stringof == "DateTime")
404         {
405             return T(year, month, day, hour, minute, second);
406         }
407         else
408         static if (T.stringof == "TimeOfDay")
409         {
410             return T(hour, minute, second);
411         }
412         else
413         static if (T.stringof == "SysTime")
414         {
415             import core.time : hnsecs, minutes;
416             import std.datetime.date: DateTime;
417             import std.datetime.systime: SysTime;
418             import std.datetime.timezone: UTC, SimpleTimeZone;
419             auto ret = SysTime(DateTime(year, month, day, hour, minute, second), UTC());
420             if (fractionCoefficient)
421             {
422                 long coeff = fractionCoefficient;
423                 int exp = fractionExponent;
424                 while (exp > -7)
425                 {
426                     exp--;
427                     coeff *= 10;
428                 }
429                 while (exp < -7)
430                 {
431                     exp++;
432                     coeff /= 10;
433                 }
434                 ret.fracSecs = coeff.hnsecs;
435             }
436             if (offset)
437             {
438                 ret = ret.toOtherTZ(new immutable SimpleTimeZone(offset.minutes));
439             }
440             return ret;
441         }
442     }
443 
444     /++
445     Returns: true if timestamp represent a time only value.
446     +/
447     bool isOnlyTime() @property const @safe pure nothrow @nogc
448     {
449         return precision > Precision.day && day == 0;
450     }
451 
452     ///
453     int opCmp(Timestamp rhs) const @safe pure nothrow @nogc
454     {
455         import std.meta: AliasSeq;
456         static foreach (member; [
457             "year",
458             "month",
459             "day",
460             "hour",
461             "minute",
462             "second",
463         ])
464             if (auto d = int(__traits(getMember, this, member)) - int(__traits(getMember, rhs, member)))
465                 return d;
466         int frel = this.fractionExponent;
467         int frer = rhs.fractionExponent;
468         ulong frcl = this.fractionCoefficient;
469         ulong frcr = rhs.fractionCoefficient;
470         while(frel > frer)
471         {
472             frel--;
473             frcl *= 10;
474         }
475         while(frer > frel)
476         {
477             frer--;
478             frcr *= 10;
479         }
480         if (frcl < frcr) return -1;
481         if (frcl > frcr) return +1;
482         if (auto d = int(this.fractionExponent) - int(rhs.fractionExponent))
483             return d;
484         return int(this.offset) - int(rhs.offset);
485     }
486 
487     /++
488     Attaches local offset, doesn't adjust other fields.
489     Local-time offsets may be represented as either `hour*60+minute` offsets from UTC,
490     or as the zero to denote a local time of UTC. They are required on timestamps with time and are not allowed on date values.
491     +/
492     @safe pure nothrow @nogc const
493     Timestamp withOffset(short minutes)
494     {
495         assert(-24 * 60 <= minutes && minutes <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60");
496         assert(precision >= Precision.minute, "Offsets are not allowed on date values.");
497         Timestamp ret = this;
498         ret.offset = minutes;
499         return ret;
500     }
501 
502     version(D_BetterC){} else
503     private string toStringImpl(alias fun)() const @safe pure nothrow
504     {
505         import mir.appender: UnsafeArrayBuffer;
506         char[64] buffer = void;
507         auto w = UnsafeArrayBuffer!char(buffer);
508         fun(w);
509         return w.data.idup;
510     }
511 
512     /++
513     Converts this $(LREF Timestamp) to a string with the format `YYYY-MM-DDThh:mm:ss±hh:mm`.
514 
515     If `w` writer is set, the resulting string will be written directly
516     to it.
517 
518     Returns:
519         A `string` when not using an output range; `void` otherwise.
520     +/
521     alias toString = toISOExtString;
522 
523     ///
524     version (mir_test)
525     @safe pure nothrow unittest
526     {
527         assert(Timestamp.init.toString == "0000T");
528         assert(Timestamp(2010, 7, 4).toString == "2010-07-04");
529         assert(Timestamp(1998, 12, 25).toString == "1998-12-25");
530         assert(Timestamp(0, 1, 5).toString == "0000-01-05");
531         assert(Timestamp(-4, 1, 5).toString == "-0004-01-05");
532 
533         // YYYY-MM-DDThh:mm:ss±hh:mm
534         assert(Timestamp(2021).toString == "2021T");
535         assert(Timestamp(2021, 01).toString == "2021-01T", Timestamp(2021, 01).toString);
536         assert(Timestamp(2021, 01, 29).toString == "2021-01-29");
537         assert(Timestamp(2021, 01, 29, 19, 42).toString == "2021-01-29T19:42Z");
538         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toString == "2021-01-29T19:42:44+07", Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toString);
539         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toString == "2021-01-29T20:12:44+07:30");
540 
541         assert(Timestamp.onlyTime(7, 40).toString == "07:40Z");
542         assert(Timestamp.onlyTime(7, 40, 30).toString == "07:40:30Z");
543         assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toString == "07:40:30.056Z");
544     }
545 
546     ///
547     version (mir_test)
548     @safe unittest
549     {
550         // Test A.D.
551         assert(Timestamp(9, 12, 4).toISOExtString == "0009-12-04");
552         assert(Timestamp(99, 12, 4).toISOExtString == "0099-12-04");
553         assert(Timestamp(999, 12, 4).toISOExtString == "0999-12-04");
554         assert(Timestamp(9999, 7, 4).toISOExtString == "9999-07-04");
555         assert(Timestamp(10000, 10, 20).toISOExtString == "+10000-10-20");
556 
557         // Test B.C.
558         assert(Timestamp(0, 12, 4).toISOExtString == "0000-12-04");
559         assert(Timestamp(-9, 12, 4).toISOExtString == "-0009-12-04");
560         assert(Timestamp(-99, 12, 4).toISOExtString == "-0099-12-04");
561         assert(Timestamp(-999, 12, 4).toISOExtString == "-0999-12-04");
562         assert(Timestamp(-9999, 7, 4).toISOExtString == "-9999-07-04");
563         assert(Timestamp(-10000, 10, 20).toISOExtString == "-10000-10-20");
564 
565         assert(Timestamp.onlyTime(7, 40).toISOExtString == "07:40Z");
566         assert(Timestamp.onlyTime(7, 40, 30).toISOExtString == "07:40:30Z");
567         assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOExtString == "07:40:30.056Z");
568 
569         const cdate = Timestamp(1999, 7, 6);
570         immutable idate = Timestamp(1999, 7, 6);
571         assert(cdate.toISOExtString == "1999-07-06");
572         assert(idate.toISOExtString == "1999-07-06");
573     }
574 
575     /// ditto
576     alias toISOExtString = toISOStringImp!true;
577 
578     /++
579     Converts this $(LREF Timestamp) to a string with the format `YYYYMMDDThhmmss±hhmm`.
580 
581     If `w` writer is set, the resulting string will be written directly
582     to it.
583 
584     Returns:
585         A `string` when not using an output range; `void` otherwise.
586     +/
587     alias toISOString = toISOStringImp!false;
588 
589     ///
590     version (mir_test)
591     @safe pure nothrow unittest
592     {
593         assert(Timestamp.init.toISOString == "0000T");
594         assert(Timestamp(2010, 7, 4).toISOString == "20100704");
595         assert(Timestamp(1998, 12, 25).toISOString == "19981225");
596         assert(Timestamp(0, 1, 5).toISOString == "00000105");
597         assert(Timestamp(-4, 1, 5).toISOString == "-00040105");
598 
599         // YYYYMMDDThhmmss±hhmm
600         assert(Timestamp(2021).toISOString == "2021T");
601         assert(Timestamp(2021, 01).toISOString == "2021-01T"); // always extended
602         assert(Timestamp(2021, 01, 29).toISOString == "20210129");
603         assert(Timestamp(2021, 01, 29, 19, 42).toISOString == "20210129T1942Z");
604         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toISOString == "20210129T194244+07");
605         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString == "20210129T201244+0730");
606         static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString == "20210129T201244+0730");
607 
608         assert(Timestamp.onlyTime(7, 40).toISOString == "T0740Z");
609         assert(Timestamp.onlyTime(7, 40, 30).toISOString == "T074030Z");
610         assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOString == "T074030.056Z");
611     }
612 
613     /// Helpfer for time zone offsets
614     void addMinutes(short minutes) @safe pure nothrow @nogc
615     {
616         int totalMinutes = minutes + (this.minute + this.hour * 60u);
617         auto h = totalMinutes / 60;
618 
619         int dayShift;
620 
621         while (totalMinutes < 0)
622         {
623             totalMinutes += 24 * 60;
624             dayShift--;
625         }
626 
627         while (totalMinutes >= 24 * 60)
628         {
629             totalMinutes -= 24 * 60;
630             dayShift++;
631         }
632 
633         if (dayShift)
634         {
635             import mir.date: Date;
636             auto ymd = (Date.trustedCreate(year, month, day) + dayShift).yearMonthDay;
637             year = ymd.year;
638             month = cast(ubyte)ymd.month;
639             day = ymd.day;
640         }
641 
642         hour = cast(ubyte) (totalMinutes / 60);
643         minute = cast(ubyte) (totalMinutes % 60);
644     }
645 
646     template toISOStringImp(bool ext)
647     {
648         version(D_BetterC){} else
649         string toISOStringImp() const @safe pure nothrow
650         {
651             return toStringImpl!toISOStringImp;
652         }
653 
654         /// ditto
655         void toISOStringImp(W)(scope ref W w) const scope
656             // if (isOutputRange!(W, char))
657         {
658             import mir.format: printZeroPad;
659             // YYYY-MM-DDThh:mm:ss±hh:mm
660             Timestamp t = this;
661 
662             if (t.offset)
663             {
664                 assert(-24 * 60 <= t.offset && t.offset <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60");
665                 assert(precision >= Precision.minute, "Offsets are not allowed on date values.");
666                 t.addMinutes(t.offset);
667             }
668 
669             if (!t.isOnlyTime)
670             {
671                 if (t.year >= 10_000)
672                     w.put('+');
673                 printZeroPad(w, t.year, t.year >= 0 ? t.year < 10_000 ? 4 : 5 : t.year > -10_000 ? 5 : 6);
674                 if (precision == Precision.year)
675                 {
676                     w.put('T');
677                     return;
678                 }
679                 if (ext || precision == Precision.month) w.put('-');
680 
681                 printZeroPad(w, cast(uint)t.month, 2);
682                 if (precision == Precision.month)
683                 {
684                     w.put('T');
685                     return;
686                 }
687                 static if (ext) w.put('-');
688 
689                 printZeroPad(w, t.day, 2);
690                 if (precision == Precision.day)
691                     return;
692             }
693 
694             if (!ext || !t.isOnlyTime)
695                 w.put('T');
696 
697             printZeroPad(w, t.hour, 2);
698             static if (ext) w.put(':');
699             printZeroPad(w, t.minute, 2);
700 
701             if (precision >= Precision.second)
702             {
703                 static if (ext) w.put(':');
704                 printZeroPad(w, t.second, 2);
705 
706                 if (precision > Precision.second && (t.fractionExponent < 0 || t.fractionCoefficient))
707                 {
708                     w.put('.');
709                     printZeroPad(w, t.fractionCoefficient, -int(t.fractionExponent));
710                 }
711             }
712 
713             if (t.offset == 0)
714             {
715                 w.put('Z');
716                 return;
717             }
718 
719             bool sign = t.offset < 0;
720             uint absoluteOffset = !sign ? t.offset : -int(t.offset);
721             uint offsetHour = absoluteOffset / 60u;
722             uint offsetMinute = absoluteOffset % 60u;
723 
724             w.put(sign ? '-' : '+');
725             printZeroPad(w, offsetHour, 2);
726             if (offsetMinute)
727             {
728                 static if (ext) w.put(':');
729                 printZeroPad(w, offsetMinute, 2);
730             }
731         }
732     }
733 
734     /++
735     Creates a $(LREF Timestamp) from a string with the format `YYYYMMDDThhmmss±hhmm
736     or its leading part allowed by the standard.
737 
738     or its leading part allowed by the standard.
739 
740     Params:
741         str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates.
742         value = (optional) result value.
743 
744     Throws:
745         $(LREF DateTimeException) if the given string is
746         not in the correct format. Two arguments overload is `nothrow`.
747     Returns:
748         `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload.
749     +/
750     alias fromISOString = fromISOStringImpl!false;
751 
752     ///
753     version (mir_test)
754     @safe unittest
755     {
756         assert(Timestamp.fromISOString("20100704") == Timestamp(2010, 7, 4));
757         assert(Timestamp.fromISOString("19981225") == Timestamp(1998, 12, 25));
758         assert(Timestamp.fromISOString("00000105") == Timestamp(0, 1, 5));
759         // assert(Timestamp.fromISOString("-00040105") == Timestamp(-4, 1, 5));
760 
761         assert(Timestamp(2021) == Timestamp.fromISOString("2021"));
762         assert(Timestamp(2021) == Timestamp.fromISOString("2021T"));
763         // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01"));
764         // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01T"));
765         assert(Timestamp(2021, 01, 29) == Timestamp.fromISOString("20210129"));
766         assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOString("20210129T1942"));
767         assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOString("20210129T1942Z"));
768         assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOString("20210129T194212"));
769         assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67) == Timestamp.fromISOString("20210129T194212.067Z"));
770         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOString("20210129T194244+07"));
771         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730"));
772         static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730"));
773         static assert(Timestamp(2021, 01, 29,  4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOString("20210128T211244-0730"));
774     }
775 
776     version (mir_test)
777     @safe unittest
778     {
779         import std.exception: assertThrown;
780         assertThrown!DateTimeException(Timestamp.fromISOString(""));
781         assertThrown!DateTimeException(Timestamp.fromISOString("990704"));
782         assertThrown!DateTimeException(Timestamp.fromISOString("0100704"));
783         assertThrown!DateTimeException(Timestamp.fromISOString("2010070"));
784         assertThrown!DateTimeException(Timestamp.fromISOString("120100704"));
785         assertThrown!DateTimeException(Timestamp.fromISOString("-0100704"));
786         assertThrown!DateTimeException(Timestamp.fromISOString("+0100704"));
787         assertThrown!DateTimeException(Timestamp.fromISOString("2010070a"));
788         assertThrown!DateTimeException(Timestamp.fromISOString("20100a04"));
789         assertThrown!DateTimeException(Timestamp.fromISOString("2010a704"));
790 
791         assertThrown!DateTimeException(Timestamp.fromISOString("99-07-04"));
792         assertThrown!DateTimeException(Timestamp.fromISOString("010-07-04"));
793         assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0"));
794         assertThrown!DateTimeException(Timestamp.fromISOString("12010-07-04"));
795         assertThrown!DateTimeException(Timestamp.fromISOString("-010-07-04"));
796         assertThrown!DateTimeException(Timestamp.fromISOString("+010-07-04"));
797         assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0a"));
798         assertThrown!DateTimeException(Timestamp.fromISOString("2010-0a-04"));
799         assertThrown!DateTimeException(Timestamp.fromISOString("2010-a7-04"));
800         assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/04"));
801         assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/04"));
802         assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/4"));
803         assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/4"));
804         assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-04"));
805         assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-4"));
806         assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-4"));
807 
808         assertThrown!DateTimeException(Timestamp.fromISOString("99Jul04"));
809         assertThrown!DateTimeException(Timestamp.fromISOString("010Jul04"));
810         assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0"));
811         assertThrown!DateTimeException(Timestamp.fromISOString("12010Jul04"));
812         assertThrown!DateTimeException(Timestamp.fromISOString("-010Jul04"));
813         assertThrown!DateTimeException(Timestamp.fromISOString("+010Jul04"));
814         assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0a"));
815         assertThrown!DateTimeException(Timestamp.fromISOString("2010Jua04"));
816         assertThrown!DateTimeException(Timestamp.fromISOString("2010aul04"));
817 
818         assertThrown!DateTimeException(Timestamp.fromISOString("99-Jul-04"));
819         assertThrown!DateTimeException(Timestamp.fromISOString("010-Jul-04"));
820         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0"));
821         assertThrown!DateTimeException(Timestamp.fromISOString("12010-Jul-04"));
822         assertThrown!DateTimeException(Timestamp.fromISOString("-010-Jul-04"));
823         assertThrown!DateTimeException(Timestamp.fromISOString("+010-Jul-04"));
824         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0a"));
825         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jua-04"));
826         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jal-04"));
827         assertThrown!DateTimeException(Timestamp.fromISOString("2010-aul-04"));
828 
829         // assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-04"));
830         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-04"));
831 
832         assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6));
833         // assert(Timestamp.fromISOString("-19990706") == Timestamp(-1999, 7, 6));
834         // assert(Timestamp.fromISOString("+019990706") == Timestamp(1999, 7, 6));
835         assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6));
836     }
837 
838     // bug# 17801
839     version (mir_test)
840     @safe unittest
841     {
842         import std.conv : to;
843         import std.meta : AliasSeq;
844         static foreach (C; AliasSeq!(char, wchar, dchar))
845         {
846             static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[]))
847                 assert(Timestamp.fromISOString(to!S("20121221")) == Timestamp(2012, 12, 21));
848         }
849     }
850 
851     /++
852     Creates a $(LREF Timestamp) from a string with the format `YYYY-MM-DDThh:mm:ss±hh:mm`
853     or its leading part allowed by the standard.
854 
855 
856     Params:
857         str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates.
858         value = (optional) result value.
859 
860     Throws:
861         $(LREF DateTimeException) if the given string is
862         not in the correct format. Two arguments overload is `nothrow`.
863     Returns:
864         `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload.
865     +/
866     alias fromISOExtString = fromISOStringImpl!true;
867 
868 
869     ///
870     version (mir_test)
871     @safe unittest
872     {
873         assert(Timestamp.fromISOExtString("2010-07-04") == Timestamp(2010, 7, 4));
874         assert(Timestamp.fromISOExtString("1998-12-25") == Timestamp(1998, 12, 25));
875         assert(Timestamp.fromISOExtString("0000-01-05") == Timestamp(0, 1, 5));
876         assert(Timestamp.fromISOExtString("-0004-01-05") == Timestamp(-4, 1, 5));
877 
878         assert(Timestamp(2021) == Timestamp.fromISOExtString("2021"));
879         assert(Timestamp(2021) == Timestamp.fromISOExtString("2021T"));
880         assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01"));
881         assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01T"));
882         assert(Timestamp(2021, 01, 29) == Timestamp.fromISOExtString("2021-01-29"));
883         assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOExtString("2021-01-29T19:42"));
884         assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOExtString("2021-01-29T19:42Z"));
885         assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOExtString("2021-01-29T19:42:12"));
886         assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67) == Timestamp.fromISOExtString("2021-01-29T19:42:12.067Z"));
887         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOExtString("2021-01-29T19:42:44+07"));
888         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30"));
889         static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30"));
890         static assert(Timestamp(2021, 01, 29,  4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30"));
891     }
892 
893     version (mir_test)
894     @safe unittest
895     {
896         import std.exception: assertThrown;
897 
898         assertThrown!DateTimeException(Timestamp.fromISOExtString(""));
899         assertThrown!DateTimeException(Timestamp.fromISOExtString("990704"));
900         assertThrown!DateTimeException(Timestamp.fromISOExtString("0100704"));
901         assertThrown!DateTimeException(Timestamp.fromISOExtString("120100704"));
902         assertThrown!DateTimeException(Timestamp.fromISOExtString("-0100704"));
903         assertThrown!DateTimeException(Timestamp.fromISOExtString("+0100704"));
904         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010070a"));
905         assertThrown!DateTimeException(Timestamp.fromISOExtString("20100a04"));
906         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010a704"));
907 
908         assertThrown!DateTimeException(Timestamp.fromISOExtString("99-07-04"));
909         assertThrown!DateTimeException(Timestamp.fromISOExtString("010-07-04"));
910         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0"));
911         assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-07-04"));
912         assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-07-04"));
913         assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-07-04"));
914         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0a"));
915         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-0a-04"));
916         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-a7-04"));
917         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/04"));
918         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/04"));
919         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/4"));
920         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/4"));
921         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-04"));
922         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-4"));
923         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-4"));
924 
925         assertThrown!DateTimeException(Timestamp.fromISOExtString("99Jul04"));
926         assertThrown!DateTimeException(Timestamp.fromISOExtString("010Jul04"));
927         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0"));
928         assertThrown!DateTimeException(Timestamp.fromISOExtString("12010Jul04"));
929         assertThrown!DateTimeException(Timestamp.fromISOExtString("-010Jul04"));
930         assertThrown!DateTimeException(Timestamp.fromISOExtString("+010Jul04"));
931         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0a"));
932         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jua04"));
933         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010aul04"));
934 
935         assertThrown!DateTimeException(Timestamp.fromISOExtString("99-Jul-04"));
936         assertThrown!DateTimeException(Timestamp.fromISOExtString("010-Jul-04"));
937         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0"));
938         assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-Jul-04"));
939         assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-Jul-04"));
940         assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-Jul-04"));
941         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0a"));
942         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jua-04"));
943         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jal-04"));
944         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-aul-04"));
945 
946         assertThrown!DateTimeException(Timestamp.fromISOExtString("20100704"));
947         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-04"));
948 
949         assert(Timestamp.fromISOExtString("1999-07-06") == Timestamp(1999, 7, 6));
950         assert(Timestamp.fromISOExtString("-1999-07-06") == Timestamp(-1999, 7, 6));
951         assert(Timestamp.fromISOExtString("+01999-07-06") == Timestamp(1999, 7, 6));
952     }
953 
954     // bug# 17801
955     version (mir_test)
956     @safe unittest
957     {
958         import std.conv : to;
959         import std.meta : AliasSeq;
960         static foreach (C; AliasSeq!(char, wchar, dchar))
961         {
962             static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[]))
963                 assert(Timestamp.fromISOExtString(to!S("2012-12-21")) == Timestamp(2012, 12, 21));
964         }
965     }
966 
967     /++
968     Creates a $(LREF Timestamp) from a string with the format YYYY-MM-DD, YYYYMMDD, or YYYY-Mon-DD.
969 
970     Params:
971         str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) and $(LREF .Timestamp.toISOString) format dates. The function is case sensetive.
972         value = (optional) result value.
973 
974     Throws:
975         $(LREF DateTimeException) if the given string is
976         not in the correct format. Two arguments overload is `nothrow`.
977     Returns:
978         `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload.
979     +/
980     static bool fromString(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc
981     {
982         return fromISOExtString(str, value)
983             || fromISOString(str, value);
984     }
985 
986     ///
987     version (mir_test)
988     @safe pure @nogc unittest
989     {
990         assert(Timestamp.fromString("2010-07-04") == Timestamp(2010, 7, 4));
991         assert(Timestamp.fromString("20100704") == Timestamp(2010, 7, 4));
992     }
993 
994     /// ditto
995     static Timestamp fromString(C)(scope const(C)[] str) @safe pure
996         if (isSomeChar!C)
997     {
998         Timestamp ret;
999         if (fromString(str, ret))
1000             return ret;
1001         throw InvalidString;
1002     }
1003 
1004     template fromISOStringImpl(bool ext)
1005     {
1006         static Timestamp fromISOStringImpl(C)(scope const(C)[] str) @safe pure
1007             if (isSomeChar!C)
1008         {
1009             Timestamp ret;
1010             if (fromISOStringImpl(str, ret))
1011                 return ret;
1012             throw InvalidISOExtendedString;
1013         }
1014 
1015         static bool fromISOStringImpl(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc
1016             if (isSomeChar!C)
1017         {
1018             import mir.parse: fromString, parse;
1019 
1020             static if (ext)
1021                 auto isOnlyTime = str.length >= 3 && (str[0] == 'T' || str[2] == ':');
1022             else
1023                 auto isOnlyTime = str.length >= 3 && str[0] == 'T';
1024 
1025             if (!isOnlyTime)
1026             {
1027                 // YYYY
1028                 static if (ext)
1029                 {{
1030                     auto startIsDigit = str.length && str[0].isDigit;
1031                     auto strOldLength = str.length;
1032                     if (!parse(str, value.year))
1033                         return false;
1034                     auto l = strOldLength - str.length;
1035                     if ((l == 4) != startIsDigit)
1036                         return false;
1037                 }}
1038                 else
1039                 {
1040                     if (str.length < 4 || !str[0].isDigit || !fromString(str[0 .. 4], value.year))
1041                         return false;
1042                     str = str[4 .. $];
1043                 }
1044 
1045                 value.precision = Precision.year;
1046                 if (str.length == 0 || str == "T")
1047                     return true;
1048                 
1049                 static if (ext)
1050                 {
1051                     if (str[0] != '-')
1052                         return false;
1053                     str = str[1 .. $];
1054                 }
1055 
1056                 // MM
1057                 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.month))
1058                     return false;
1059                 str = str[2 .. $];
1060                 value.precision = Precision.month;
1061                 if (str.length == 0 || str == "T")
1062                     return ext;
1063 
1064                 static if (ext)
1065                 {
1066                     if (str[0] != '-')
1067                         return false;
1068                     str = str[1 .. $];
1069                 }
1070 
1071                 // DD
1072                 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.day))
1073                     return false;
1074                 str = str[2 .. $];
1075                 value.precision = Precision.day;
1076                 if (str.length == 0)
1077                     return true;
1078             }
1079 
1080             // str isn't empty here
1081             // T
1082             if (str[0] == 'T')
1083             {
1084                 str = str[1 .. $];
1085                 // OK, onlyTime requires length >= 3
1086                 if (str.length == 0)
1087                     return true;
1088             }
1089             else 
1090             {
1091                 if (!(ext && isOnlyTime))
1092                     return false;
1093             }
1094 
1095             value.precision = Precision.minute; // we don't have hour precision
1096 
1097             // hh
1098             if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.hour))
1099                 return false;
1100             str = str[2 .. $];
1101             if (str.length == 0)
1102                 return true;
1103 
1104             static if (ext)
1105             {
1106                 if (str[0] != ':')
1107                     return false;
1108                 str = str[1 .. $];
1109             }
1110 
1111             // mm
1112             {
1113                 uint minute;
1114                 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute))
1115                     return false;
1116                 value.minute = cast(ubyte) minute;
1117                 str = str[2 .. $];
1118                 if (str.length == 0)
1119                     return true;
1120             }
1121 
1122             static if (ext)
1123             {
1124                 if (str[0] != ':')
1125                     goto TZ;
1126                 str = str[1 .. $];
1127             }
1128 
1129             // ss
1130             {
1131                 uint second;
1132                 if (str.length < 2 || !str[0].isDigit)
1133                     goto TZ;
1134                 if (!fromString(str[0 .. 2], second))
1135                     return false;
1136                 value.second = cast(ubyte) second;
1137                 str = str[2 .. $];
1138                 value.precision = Precision.second;
1139                 if (str.length == 0)
1140                     return true;
1141             }
1142 
1143             // .
1144             if (str[0] != '.')
1145                 goto TZ;
1146             str = str[1 .. $];
1147             value.precision = Precision.fraction;
1148 
1149             // fraction
1150             {
1151                 const strOldLength = str.length;
1152                 ulong fractionCoefficient;
1153                 if (str.length < 1 || !str[0].isDigit || !parse!ulong(str, fractionCoefficient))
1154                     return false;
1155                 sizediff_t fractionExponent = str.length - strOldLength;
1156                 if (fractionExponent < -12)
1157                     return false;
1158                 value.fractionExponent = cast(byte)fractionExponent;
1159                 value.fractionCoefficient = fractionCoefficient;
1160                 if (str.length == 0)
1161                     return true;
1162             }
1163 
1164         TZ:
1165 
1166             if (str == "Z")
1167                 return true;
1168 
1169             int hour;
1170             int minute;
1171             if (str.length < 3 || str[0].isDigit || !fromString(str[0 .. 3], hour))
1172                 return false;
1173             str = str[3 .. $];
1174 
1175             if (str.length)
1176             {
1177                 static if (ext)
1178                 {
1179                     if (str[0] != ':')
1180                         return false;
1181                     str = str[1 .. $];
1182                 }
1183                 if (str.length != 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute))
1184                     return false;
1185             }
1186 
1187             value.offset = cast(short)(hour * 60 + (hour < 0 ? -minute : minute));
1188             value.addMinutes(cast(short)-int(value.offset));
1189             return true;
1190         }
1191     }
1192 }