The OpenD Programming Language

1 /++
2 Timestamp
3 +/
4 module mir.timestamp;
5 
6 private alias isDigit = (dchar c) => uint(c - '0') < 10;
7 import mir.serde: serdeIgnore, serdeRegister;
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("Timestamp: Invalid Month");
29     private static immutable InvalidDay = new DateTimeException("Timestamp: Invalid Day");
30     private static immutable InvalidISOString = new DateTimeException("Timestamp: Invalid ISO String");
31     private static immutable InvalidISOExtendedString = new DateTimeException("Timestamp: Invalid ISO Extended String");
32     private static immutable InvalidYamlString = new DateTimeException("Timestamp: Invalid YAML String");
33     private static immutable InvalidString = new DateTimeException("Timestamp: Invalid String");
34     private static immutable ExpectedDuration = new DateTimeException("Timestamp: Expected Duration");
35 }
36 
37 /++
38 Timestamp
39 
40 Note: The component values in the binary encoding are always in UTC or local time with unknown offset,
41 while components in the text encoding are in a some timezone with known offset.
42 This means that transcoding requires a conversion between UTC and a timezone.
43 
44 `Timestamp` precision is up to picosecond (second/10^12).
45 +/
46 @serdeRegister
47 struct Timestamp
48 {
49     import std.traits: isSomeChar;
50 
51     ///
52     enum Precision : ubyte
53     {
54         ///
55         year,
56         ///
57         month,
58         ///
59         day,
60         ///
61         minute,
62         ///
63         second,
64         ///
65         fraction,
66     }
67 
68 @serdeIgnore:
69 
70     ///
71     this(scope const(char)[] str) @safe pure @nogc
72     {
73         this = fromString(str);
74     }
75 
76     ///
77     version (mir_test)
78     @safe pure @nogc unittest
79     {
80         assert(Timestamp("2010-07-04") == Timestamp(2010, 7, 4));
81         assert(Timestamp("20100704") == Timestamp(2010, 7, 4));
82         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730"));
83         static assert(Timestamp(2021, 01, 29,  4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30"));
84 
85         assert(Timestamp("T0740") == Timestamp.onlyTime(7, 40));
86         assert(Timestamp("T074030Z") == Timestamp.onlyTime(7, 40, 30).withOffset(0));
87         assert(Timestamp("T074030.056") == Timestamp.onlyTime(7, 40, 30, -3, 56));
88 
89         assert(Timestamp("07:40") == Timestamp.onlyTime(7, 40));
90         assert(Timestamp("07:40:30") == Timestamp.onlyTime(7, 40, 30));
91         assert(Timestamp("T07:40:30.056Z") == Timestamp.onlyTime(7, 40, 30, -3, 56).withOffset(0));
92     }
93 
94     private short _offset = short.min;
95 
96     /++
97     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”.
98     This differs semantically from an offset of “Z” or “+00:00”, which imply that UTC is the preferred reference point for the specified time.
99     RFC2822 describes a similar convention for email.
100     +/
101     /++
102     Timezone offset in minutes
103     +/
104     short offset()() const @safe pure nothrow @nogc @property
105     {
106         return isLocalTime ? 0 : _offset;
107     }
108 
109     ///
110     void offset()(ushort offset) @safe pure nothrow @nogc @property
111     {
112         _offset = offset;
113     }
114 
115     /++
116     Returns: true if it is a local time
117     +/
118     bool isLocalTime()() const @safe pure nothrow @nogc @property
119     {
120         return _offset == _offset.min;
121     }
122 
123     /++
124     +/
125     void setLocalTimezone()() @safe pure nothrow @nogc @property
126     {
127         _offset = _offset.min;
128     }
129 
130 
131     /++
132     Year
133     +/
134     short year;
135     /++
136     +/
137     Precision precision;
138 
139     /++
140     Month
141     
142     If the value equals to thero then this and all the following members are undefined.
143     +/
144     ubyte month;
145     /++
146     Day
147     
148     If the value equals to thero then this and all the following members are undefined.
149     +/
150     ubyte day;
151     /++
152     Hour
153     +/
154     ubyte hour;
155 
156     version(D_Ddoc)
157     {
158     
159         /++
160         Minute
161 
162         Note: the field is implemented as property.
163         +/
164         ubyte minute;
165         /++
166         Second
167 
168         Note: the field is implemented as property.
169         +/
170         ubyte second;
171         /++
172         Fraction
173 
174         The `fractionExponent` and `fractionCoefficient` denote the fractional seconds of the timestamp as a decimal value
175         The fractional seconds’ value is `coefficient * 10 ^ exponent`.
176         It must be greater than or equal to zero and less than 1.
177         A missing coefficient defaults to zero.
178         Fractions whose coefficient is zero and exponent is greater than -1 are ignored.
179         
180         'fractionCoefficient' allowed values are [0 ... 10^12-1].
181         'fractionExponent' allowed values are [-12 ... 0].
182 
183         Note: the fields are implemented as property.
184         +/
185         byte fractionExponent;
186         /// ditto
187         ulong fractionCoefficient;
188     }
189     else
190     {
191         import mir.bitmanip: bitfields;
192         version (LittleEndian)
193         {
194 
195             mixin(bitfields!(
196                     ubyte, "minute", 8,
197                     ubyte, "second", 8,
198                     byte, "fractionExponent", 8,
199                     ulong, "fractionCoefficient", 40,
200             ));
201         }
202         else
203         {
204             mixin(bitfields!(
205                     ulong, "fractionCoefficient", 40,
206                     byte, "fractionExponent", 8,
207                     ubyte, "second", 8,
208                     ubyte, "minute", 8,
209             ));
210         }
211     }
212 
213     ///
214     @safe pure nothrow @nogc
215     this(short year)
216     {
217         this.year = year;
218         this.precision = Precision.year;
219     }
220 
221     ///
222     @safe pure nothrow @nogc
223     this(short year, ubyte month)
224     {
225         this.year = year;
226         this.month = month;
227         this.precision = Precision.month;
228     }
229 
230     ///
231     @safe pure nothrow @nogc
232     this(short year, ubyte month, ubyte day)
233     {
234         this.year = year;
235         this.month = month;
236         this.day = day;
237         this.precision = Precision.day;
238     }
239 
240     ///
241     @safe pure nothrow @nogc
242     this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute)
243     {
244         this.year = year;
245         this.month = month;
246         this.day = day;
247         this.hour = hour;
248         this.minute = minute;
249         this.precision = Precision.minute;
250     }
251 
252     ///
253     @safe pure nothrow @nogc
254     this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second)
255     {
256         this.year = year;
257         this.month = month;
258         this.day = day;
259         this.hour = hour;
260         this.day = day;
261         this.minute = minute;
262         this.second = second;
263         this.precision = Precision.second;
264     }
265 
266     ///
267     @safe pure nothrow @nogc
268     this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient)
269     {
270         this.year = year;
271         this.month = month;
272         this.day = day;
273         this.hour = hour;
274         this.day = day;
275         this.minute = minute;
276         this.second = second;
277         assert(fractionExponent < 0);
278         this.fractionExponent = fractionExponent;
279         this.fractionCoefficient = fractionCoefficient;
280         this.precision = Precision.fraction;
281     }
282 
283     ///
284     @safe pure nothrow @nogc
285     static Timestamp onlyTime(ubyte hour, ubyte minute)
286     {
287         return Timestamp(0, 0, 0, hour, minute);
288     }
289 
290     ///
291     @safe pure nothrow @nogc
292     static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second)
293     {
294         return Timestamp(0, 0, 0, hour, minute, second);
295     }
296 
297     ///
298     @safe pure nothrow @nogc
299     static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient)
300     {
301         return Timestamp(0, 0, 0, hour, minute, second, fractionExponent, fractionCoefficient);
302     }
303 
304     ///
305     this(Date)(const Date datetime)
306         if (Date.stringof == "Date" || Date.stringof == "date")
307     {
308         static if (__traits(hasMember, Date, "yearMonthDay"))
309             with(datetime.yearMonthDay) this(year, cast(ubyte)month, day);
310         else
311             with(datetime) this(year, month, day);
312     }
313 
314     ///
315     version (mir_test)
316     @safe unittest {
317         import mir.date : Date;
318         auto dt = Date(1982, 4, 1);
319         Timestamp ts = dt;
320         assert(ts.opCmp(ts) == 0);
321         assert(dt.toISOExtString == ts.toString);
322         assert(dt == cast(Date) ts);
323     }
324 
325     ///
326     version (mir_test)
327     @safe unittest {
328         import std.datetime.date : Date;
329         auto dt = Date(1982, 4, 1);
330         Timestamp ts = dt;
331         assert(dt.toISOExtString == ts.toString);
332         assert(dt == cast(Date) ts);
333     }
334 
335     ///
336     this(TimeOfDay)(const TimeOfDay timeOfDay)
337         if (TimeOfDay.stringof == "TimeOfDay")
338     {
339         with(timeOfDay) this = onlyTime(hour, minute, second);
340     }
341 
342     ///
343     version (mir_test)
344     @safe unittest {
345         import mir.test: should;
346         import std.datetime.date : TimeOfDay;
347         auto dt = TimeOfDay(7, 14, 30);
348         Timestamp ts = dt;
349         (dt.toISOExtString ~ "-00:00").should == ts.toString;
350         assert(dt == cast(TimeOfDay) ts);
351     }
352 
353     ///
354     this(DateTime)(const DateTime datetime)
355         if (DateTime.stringof == "DateTime")
356     {
357         with(datetime) this(year, cast(ubyte)month, day, hour, minute, second);
358     }
359 
360     ///
361     version (mir_test)
362     @safe unittest {
363         import std.datetime.date : DateTime;
364         auto dt = DateTime(1982, 4, 1, 20, 59, 22);
365         Timestamp ts = dt;
366         assert(dt.toISOExtString ~ "-00:00" == ts.toString);
367         assert(dt == cast(DateTime) ts);
368     }
369 
370     ///
371     this(SysTime)(const SysTime systime) pure
372         if (SysTime.stringof == "SysTime")
373     {
374         import std.datetime.timezone : LocalTime;
375         auto offset = assumePureSafe(()=>systime.utcOffset)();
376         auto isLocal = systime.timezone is LocalTime();
377         auto thisTimes = isLocal ? systime + offset : systime.toUTC;
378         this = fromUnixTime(thisTimes.toUnixTime);
379         this.fractionExponent = -7;
380         this.fractionCoefficient = assumePureSafe(()=>thisTimes.fracSecs)().total!"hnsecs";
381         this.precision = Precision.fraction;
382         if (!isLocal)
383            this.offset = cast(short) offset.total!"minutes";
384     }
385 
386     ///
387     version (mir_test)
388     @safe unittest {
389         import core.time : hnsecs, minutes;
390         import std.datetime.date : DateTime;
391         import std.datetime.timezone : SimpleTimeZone;
392         import std.datetime.systime : SysTime;
393 
394         auto dt = DateTime(1982, 4, 1, 20, 59, 22);
395         auto tz = new immutable SimpleTimeZone(-330.minutes);
396         auto st = SysTime(dt, 1234567.hnsecs, tz);
397         Timestamp ts = st;
398 
399         assert(st.toISOExtString == ts.toString);
400         assert(st == cast(SysTime) ts);
401     }
402 
403     /++
404     Creates a fake timestamp from a Duration using `total!"hnsecs"` method.
405     For positive and zero timestamps the format is
406         `wwww-dd-88Thh:mm:ss.nnnnnnn`
407         and for negative timestamps
408         `wwww-dd-99Thh:mm:ss.nnnnnnn`.
409     +/
410     this(Duration)(const Duration duration)
411         if (Duration.stringof == "Duration")
412     {
413         auto hnsecs = duration.total!"hnsecs";
414         ulong abs = hnsecs < 0 ? -hnsecs : hnsecs;
415         precision = Precision.fraction;
416         day = hnsecs >= 0 ? 88 : 99;
417         fractionExponent = -7;
418         fractionCoefficient = abs % 10_000_000U;
419         abs /= 10_000_000U;
420         second = abs % 60;
421         abs /= 60;
422         minute = abs % 60;
423         abs /= 60;
424         hour = abs % 24;
425         abs /= 24;
426         month = abs % 7;
427         abs /= 7;
428         year = cast(typeof(year)) abs;
429     }
430 
431     ///
432     version (mir_test)
433     @safe unittest {
434         import core.time : Duration, weeks, days, hours, minutes, seconds, hnsecs;
435 
436         auto duration = 5.weeks + 2.days + 7.hours + 40.minutes + 4.seconds + 9876543.hnsecs;
437         Timestamp ts = duration;
438 
439         assert(ts.toISOExtString == `0005-02-88T07:40:04.9876543-00:00`);
440         assert(duration == cast(Duration) ts);
441 
442         duration = -duration;
443         ts = Timestamp(duration);
444         assert(ts.toISOExtString == `0005-02-99T07:40:04.9876543-00:00`);
445         assert(duration == cast(Duration) ts);
446 
447         assert(Timestamp(Duration.zero).toISOExtString == `0000-00-88T00:00:00.0000000-00:00`);
448     }
449 
450     /++
451     Decomposes Timestamp to an algebraic type.
452     Supported types up to T.stringof equivalence:
453 
454     $(UL
455     $(LI `Year`)
456     $(LI `YearMonth`)
457     $(LI `YearMonthDay`)
458     $(LI `Date`)
459     $(LI `date`)
460     $(LI `TimeOfDay`)
461     $(LI `DateTime`)
462     $(LI `SysTime`)
463     $(LI `Timestamp` as fallback type)
464     )
465 
466 
467     Throws: an exception if timestamp cannot be converted to an algebraic type and there is no `Timestamp` type in the Algebraic set.
468     +/
469     T opCast(T)() const
470         if (__traits(hasMember, T, "AllowedTypes"))
471     {
472         foreach (AT; T.AllowedTypes)
473             static if (AT.stringof == "Year")
474                 if (precision == precision.year)
475                     return T(opCast!AT);
476 
477         foreach (AT; T.AllowedTypes)
478             static if (AT.stringof == "YearMonth")
479                 if (precision == precision.month)
480                     return T(opCast!AT);
481 
482         foreach (AT; T.AllowedTypes)
483             static if (AT.stringof == "Duration")
484                 if (isDuration)
485                     return T(opCast!AT);
486 
487         foreach (AT; T.AllowedTypes)
488             static if (AT.stringof == "YearMonthDay" || AT.stringof == "Date" ||  AT.stringof == "date")
489                 if (precision == precision.day)
490                     return T(opCast!AT);
491 
492         foreach (AT; T.AllowedTypes)
493             static if (AT.stringof == "TimeOfDay")
494                 if (isOnlyTime)
495                     return T(opCast!AT);
496 
497         if (!isOnlyTime && precision >= precision.day)
498         {
499             foreach (AT; T.AllowedTypes)
500                 static if (AT.stringof == "DateTime")
501                     if (offset == 0 && precision <= precision.second)
502                         return T(opCast!AT);
503 
504             foreach (AT; T.AllowedTypes)
505                 static if (AT.stringof == "SysTime")
506                     return T(opCast!AT);
507         }
508 
509         import std.meta: staticIndexOf;
510         static if (staticIndexOf!(Timestamp, T.AllowedTypes) < 0)
511         {
512             static immutable exc = new Exception("Cannot cast Timestamp to " ~ T.stringof);
513             { import mir.exception : toMutable; throw exc.toMutable; }
514         }
515         else
516         {
517             return T(this);
518         }
519     }
520 
521     ///
522     version (mir_test)
523     @safe unittest
524     {
525         import core.time : hnsecs, minutes, Duration;
526         import mir.algebraic;
527         import mir.date: Date; // Can be other Date type as well
528         import std.datetime.date : TimeOfDay, DateTime;
529         import std.datetime.systime : SysTime;
530         import std.datetime.timezone: UTC, SimpleTimeZone;
531 
532         alias A = Variant!(Date, TimeOfDay, DateTime, Duration, SysTime, Timestamp, string); // non-date-time types is OK
533         assert(cast(A) Timestamp(1023) == Timestamp(1023)); // Year isn't represented in the algebraic, use fallback type
534         assert(cast(A) Timestamp.onlyTime(7, 40, 30) == TimeOfDay(7, 40, 30));
535         assert(cast(A) Timestamp(1982, 4, 1, 20, 59, 22) == DateTime(1982, 4, 1, 20, 59, 22));
536 
537         auto dt = DateTime(1982, 4, 1, 20, 59, 22);
538         auto tz = new immutable SimpleTimeZone(-330.minutes);
539         auto st = SysTime(dt, 1234567.hnsecs, tz);
540         assert(cast(A) Timestamp(st) == st);
541     }
542 
543     /++
544     Casts timestamp to a date-time type.
545 
546     Supported types up to T.stringof equivalence:
547 
548     $(UL
549     $(LI `Year`)
550     $(LI `YearMonth`)
551     $(LI `YearMonthDay`)
552     $(LI `Date`)
553     $(LI `date`)
554     $(LI `TimeOfDay`)
555     $(LI `DateTime`)
556     $(LI `SysTime`)
557     )
558     +/
559     T opCast(T)() const
560         if (
561             T.stringof == "Year"
562          || T.stringof == "YearMonth"
563          || T.stringof == "YearMonthDay"
564          || T.stringof == "Date"
565          || T.stringof == "date"
566          || T.stringof == "TimeOfDay"
567          || T.stringof == "Duration"
568          || T.stringof == "DateTime"
569          || T.stringof == "SysTime")
570     {
571         static if (T.stringof == "YearMonth")
572         {
573             return T(year, month);
574         }
575         else
576         static if (T.stringof == "Date" || T.stringof == "date" || T.stringof == "YearMonthDay")
577         {
578             return T(year, month, day);
579         }
580         else
581         static if (T.stringof == "DateTime")
582         {
583             return T(year, month, day, hour, minute, second);
584         }
585         else
586         static if (T.stringof == "TimeOfDay")
587         {
588             return T(hour, minute, second);
589         }
590         else
591         static if (T.stringof == "SysTime")
592         {
593             import core.time : hnsecs, minutes;
594             import std.datetime.date: DateTime;
595             import std.datetime.systime: SysTime;
596             import std.datetime.timezone: UTC, LocalTime, SimpleTimeZone;
597             auto ret = SysTime.fromUnixTime(toUnixTime, UTC()) + getFraction!7.hnsecs;
598             if (isLocalTime)
599             {
600                 ret = ret.toLocalTime;
601                 ret -= assumePureSafe(()=>ret.utcOffset)();
602             }
603             else
604             if (offset)
605             {
606                 ret.timezone = new immutable SimpleTimeZone(offset.minutes);
607             }
608             return ret;
609         }
610         else
611         static if (T.stringof == "Duration")
612         {
613             if (!isDuration)
614                 { import mir.exception : toMutable; throw ExpectedDuration.toMutable; }
615             auto coeff = ((((long(year) * 7 + month) * 24 + hour) * 60 + minute) * 60 + second) * 10_000_000 + getFraction!7;
616             if (isNegativeDuration)
617                 coeff = -coeff;
618 
619             import mir.conv: to;
620             import core.time: hnsecs;
621             return coeff.hnsecs.to!T;
622         }
623     }
624 
625     /++
626     +/
627     long getFraction(int digits)() @property const @safe pure nothrow @nogc
628         if (digits >= 1 && digits <= 12)
629     {
630         long coeff;
631         if (fractionCoefficient)
632         {
633             coeff = fractionCoefficient;
634             int exp = fractionExponent;
635             while (exp > -digits)
636             {
637                 exp--;
638                 coeff *= 10;
639             }
640             while (exp < -digits)
641             {
642                 exp++;
643                 coeff /= 10;
644             }
645         }
646         return coeff;
647     }
648 
649     /++
650     Returns: true if timestamp represent a time only value.
651     +/
652     bool isOnlyTime() @property const @safe pure nothrow @nogc
653     {
654         return precision > Precision.day && day == 0;
655     }
656 
657     ///
658     int opCmp(Timestamp rhs) const @safe pure nothrow @nogc
659     {
660         import std.meta: AliasSeq;
661         static foreach (member; [
662             "year",
663             "month",
664             "day",
665             "hour",
666             "minute",
667             "second",
668         ])
669             if (auto d = int(__traits(getMember, this, member)) - int(__traits(getMember, rhs, member)))
670                 return d;
671         int frel = this.fractionExponent;
672         int frer = rhs.fractionExponent;
673         ulong frcl = this.fractionCoefficient;
674         ulong frcr = rhs.fractionCoefficient;
675         while(frel > frer)
676         {
677             frel--;
678             frcl *= 10;
679         }
680         while(frer > frel)
681         {
682             frer--;
683             frcr *= 10;
684         }
685         if (frcl < frcr) return -1;
686         if (frcl > frcr) return +1;
687         if (auto d = int(this.fractionExponent) - int(rhs.fractionExponent))
688             return d;
689         return int(this.offset) - int(rhs.offset);
690     }
691 
692     /++
693     Attaches local offset, doesn't adjust other fields.
694     Local-time offsets may be represented as either `hour*60+minute` offsets from UTC,
695     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.
696     +/
697     @safe pure nothrow @nogc const
698     Timestamp withOffset(short minutes)
699     {
700         assert(-24 * 60 <= minutes && minutes <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60");
701         assert(precision >= Precision.minute, "Offsets are not allowed on date values.");
702         Timestamp ret = this;
703         ret.offset = minutes;
704         return ret;
705     }
706 
707     version(D_BetterC){} else
708     private string toStringImpl(alias fun)() const @safe pure nothrow
709     {
710         import mir.appender: UnsafeArrayBuffer;
711         char[64] buffer = void;
712         auto w = UnsafeArrayBuffer!char(buffer);
713         fun(w);
714         return w.data.idup;
715     }
716 
717     /++
718     Converts this $(LREF Timestamp) to a string with the format `yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm`.
719 
720     If `w` writer is set, the resulting string will be written directly
721     to it.
722 
723     Returns:
724         A `string` when not using an output range; `void` otherwise.
725     +/
726     alias toString = toISOExtString;
727 
728     ///
729     version (mir_test)
730     @safe pure nothrow unittest
731     {
732         import mir.test;
733         Timestamp.init.toString.should == "0000T";
734         Timestamp(2010, 7, 4).toString.should == "2010-07-04";
735         Timestamp(1998, 12, 25).toString.should == "1998-12-25";
736         Timestamp(0, 1, 5).toString.should == "0000-01-05";
737         Timestamp(-4, 1, 5).toString.should == "-0004-01-05";
738 
739         // yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm
740         Timestamp(2021).toString.should == "2021T";
741         Timestamp(2021, 01).toString.should == "2021-01T";
742         Timestamp(2021, 01, 29).toString.should == "2021-01-29";
743         Timestamp(2021, 01, 29, 19, 42).withOffset(0).toString.should == "2021-01-29T19:42Z";
744         Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toString.should == "2021-01-29T19:42:44+07";
745         Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toString.should == "2021-01-29T20:12:44+07:30";
746 
747         Timestamp.onlyTime(7, 40).toString.should == "07:40-00:00";
748         Timestamp.onlyTime(7, 40, 30).toString.should == "07:40:30-00:00";
749         Timestamp.onlyTime(7, 40, 30, -3, 56).withOffset(0).toString.should == "07:40:30.056Z";
750     }
751 
752     ///
753     version (mir_test)
754     @safe unittest
755     {
756         // Test A.D.
757         assert(Timestamp(9, 12, 4).toISOExtString == "0009-12-04");
758         assert(Timestamp(99, 12, 4).toISOExtString == "0099-12-04");
759         assert(Timestamp(999, 12, 4).toISOExtString == "0999-12-04");
760         assert(Timestamp(9999, 7, 4).toISOExtString == "9999-07-04");
761         assert(Timestamp(10000, 10, 20).toISOExtString == "+10000-10-20");
762 
763         // Test B.C.
764         assert(Timestamp(0, 12, 4).toISOExtString == "0000-12-04");
765         assert(Timestamp(-9, 12, 4).toISOExtString == "-0009-12-04");
766         assert(Timestamp(-99, 12, 4).toISOExtString == "-0099-12-04");
767         assert(Timestamp(-999, 12, 4).toISOExtString == "-0999-12-04");
768         assert(Timestamp(-9999, 7, 4).toISOExtString == "-9999-07-04");
769         assert(Timestamp(-10000, 10, 20).toISOExtString == "-10000-10-20");
770 
771         assert(Timestamp.onlyTime(7, 40).toISOExtString == "07:40-00:00");
772         assert(Timestamp.onlyTime(7, 40, 30).toISOExtString == "07:40:30-00:00");
773         assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOExtString == "07:40:30.056-00:00");
774 
775         const cdate = Timestamp(1999, 7, 6);
776         immutable idate = Timestamp(1999, 7, 6);
777         assert(cdate.toISOExtString == "1999-07-06");
778         assert(idate.toISOExtString == "1999-07-06");
779     }
780 
781     /// ditto
782     alias toISOExtString = toISOStringImp!true;
783 
784     /++
785     Converts this $(LREF Timestamp) to a string with the format `YYYYMMDDThhmmss±hhmm`.
786 
787     If `w` writer is set, the resulting string will be written directly
788     to it.
789 
790     Returns:
791         A `string` when not using an output range; `void` otherwise.
792     +/
793     alias toISOString = toISOStringImp!false;
794 
795     ///
796     version (mir_test)
797     @safe pure nothrow unittest
798     {
799         import mir.test;
800         Timestamp.init.toISOString.should == "0000T";
801         Timestamp(2010, 7, 4).toISOString.should == "20100704";
802         Timestamp(1998, 12, 25).toISOString.should == "19981225";
803         Timestamp(0, 1, 5).toISOString.should == "00000105";
804         Timestamp(-4, 1, 5).toISOString.should == "-00040105";
805 
806         // YYYYMMDDThhmmss±hhmm
807         Timestamp(2021).toISOString.should == "2021T";
808         Timestamp(2021, 01).toISOString.should == "2021-01T"; // always extended
809         Timestamp(2021, 01, 29).toISOString.should == "20210129";
810         Timestamp(2021, 01, 29, 19, 42).toISOString.should == "20210129T1942";
811         Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toISOString.should == "20210129T194244+07";
812         Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString.should == "20210129T201244+0730";
813         static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString == "20210129T201244+0730");
814 
815         assert(Timestamp.onlyTime(7, 40).toISOString == "T0740");
816         assert(Timestamp.onlyTime(7, 40, 30).toISOString == "T074030");
817         assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOString == "T074030.056");
818     }
819 
820     ///
821     long toUnixTime() const @safe pure nothrow @nogc
822     {
823         import mir.date: Date;
824         long result;
825         if (!isDuration && !isOnlyTime)
826         {
827             enum fistDay = Date.trustedCreate(1970, 1, 1);
828             result = Date.trustedCreate(year, month ? month : 1, day ? day : 1) - fistDay;
829             result *= 24;
830         }
831         result += hour;
832         result *= 60;
833         result += minute;
834         result *= 60;
835         result += second;
836         return result;
837     }
838 
839     ///
840     version(mir_test)
841     @safe pure nothrow @nogc unittest
842     {
843         assert(Timestamp(1970, 1, 1).toUnixTime == 0);
844         assert(Timestamp(2007, 12, 22, 8, 14, 45).toUnixTime == 1_198_311_285);
845         assert(Timestamp(2007, 12, 22, 8, 14, 45).withOffset(90).toUnixTime == 1_198_311_285);
846         assert(Timestamp(1969, 12, 31, 23, 59, 59).toUnixTime == -1);
847     }
848 
849     ///
850     static Timestamp fromUnixTime(long time) @safe pure nothrow @nogc
851     {
852         import mir.date: Date;
853         auto days = time / (24 * 60 * 60); 
854         time -= days * (24 * 60 * 60);
855         if (time < 0)
856         {
857             days--;
858             time += 24 * 60 * 60;
859         }
860         assert(time >= 0);
861         auto second = time % 60;
862         time /= 60;
863         auto minute = time % 60;
864         time /= 60;
865         auto hour = cast(ubyte) time;
866         enum fistDay = Date.trustedCreate(1970, 1, 1);
867         with((fistDay + cast(int)days).yearMonthDay)
868             return Timestamp(year, cast(ubyte)month, day, hour, cast(ubyte)minute, cast(ubyte)second);
869     }
870 
871     ///
872     version(mir_test)
873     @safe pure nothrow @nogc unittest
874     {
875         import mir.format;
876         assert(Timestamp.fromUnixTime(0) == Timestamp(1970, 1, 1, 0, 0, 0));
877         assert(Timestamp.fromUnixTime(1_198_311_285) == Timestamp(2007, 12, 22, 8, 14, 45));
878         assert(Timestamp.fromUnixTime(-1) == Timestamp(1969, 12, 31, 23, 59, 59));
879     }
880 
881     /// Helpfer for time zone offsets
882     void addMinutes(short minutes) @safe pure nothrow @nogc
883     {
884         int totalMinutes = minutes + (this.minute + this.hour * 60u);
885         auto h = totalMinutes / 60;
886 
887         int dayShift;
888 
889         while (totalMinutes < 0)
890         {
891             totalMinutes += 24 * 60;
892             dayShift--;
893         }
894 
895         while (totalMinutes >= 24 * 60)
896         {
897             totalMinutes -= 24 * 60;
898             dayShift++;
899         }
900 
901         if (dayShift)
902         {
903             import mir.date: Date;
904             auto ymd = (Date.trustedCreate(year, month, day) + dayShift).yearMonthDay;
905             year = ymd.year;
906             month = cast(ubyte)ymd.month;
907             day = ymd.day;
908         }
909 
910         hour = cast(ubyte) (totalMinutes / 60);
911         minute = cast(ubyte) (totalMinutes % 60);
912     }
913 
914     template toISOStringImp(bool ext)
915     {
916         version(D_BetterC){} else
917         string toISOStringImp() const @safe pure nothrow
918         {
919             return toStringImpl!toISOStringImp;
920         }
921 
922         /// ditto
923         void toISOStringImp(W)(ref scope W w) const scope
924             // if (isOutputRange!(W, char))
925         {
926             import mir.format: printZeroPad;
927             // yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm
928             Timestamp t = this;
929 
930             if (t.offset)
931             {
932                 assert(-24 * 60 <= t.offset && t.offset <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60");
933                 assert(precision >= Precision.minute, "Offsets are not allowed on date values.");
934                 t.addMinutes(t.offset);
935             }
936 
937             if (!t.isOnlyTime)
938             {
939                 if (t.year >= 10_000)
940                     w.put('+');
941                 printZeroPad(w, t.year, t.year >= 0 ? t.year < 10_000 ? 4 : 5 : t.year > -10_000 ? 5 : 6);
942                 if (precision == Precision.year)
943                 {
944                     w.put('T');
945                     return;
946                 }
947                 if (ext || precision == Precision.month) w.put('-');
948 
949                 printZeroPad(w, cast(uint)t.month, 2);
950                 if (precision == Precision.month)
951                 {
952                     w.put('T');
953                     return;
954                 }
955                 static if (ext) w.put('-');
956 
957                 printZeroPad(w, t.day, 2);
958                 if (precision == Precision.day)
959                     return;
960             }
961 
962             if (!ext || !t.isOnlyTime)
963                 w.put('T');
964 
965             printZeroPad(w, t.hour, 2);
966             static if (ext) w.put(':');
967             printZeroPad(w, t.minute, 2);
968 
969             if (precision >= Precision.second)
970             {
971                 static if (ext) w.put(':');
972                 printZeroPad(w, t.second, 2);
973 
974                 if (precision > Precision.second && (t.fractionExponent < 0 || t.fractionCoefficient))
975                 {
976                     w.put('.');
977                     printZeroPad(w, t.fractionCoefficient, -int(t.fractionExponent));
978                 }
979             }
980 
981             if (t.isLocalTime)
982             {
983                 static if (ext)
984                     w.put("-00:00");
985                 return;
986             }
987 
988             if (t.offset == 0)
989             {
990                 w.put('Z');
991                 return;
992             }
993 
994             bool sign = t.offset < 0;
995             uint absoluteOffset = !sign ? t.offset : -int(t.offset);
996             uint offsetHour = absoluteOffset / 60u;
997             uint offsetMinute = absoluteOffset % 60u;
998 
999             w.put(sign ? '-' : '+');
1000             printZeroPad(w, offsetHour, 2);
1001             if (offsetMinute)
1002             {
1003                 static if (ext) w.put(':');
1004                 printZeroPad(w, offsetMinute, 2);
1005             }
1006         }
1007     }
1008 
1009     /++
1010     Creates a $(LREF Timestamp) from a string with the format `YYYYMMDDThhmmss±hhmm
1011     or its leading part allowed by the standard.
1012 
1013     or its leading part allowed by the standard.
1014 
1015     Params:
1016         str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates.
1017         value = (optional) result value.
1018 
1019     Throws:
1020         $(LREF DateTimeException) if the given string is
1021         not in the correct format. Two arguments overload is `nothrow`.
1022     Returns:
1023         `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload.
1024     +/
1025     alias fromISOString = fromISOStringImpl!false;
1026 
1027     ///
1028     version (mir_test)
1029     @safe unittest
1030     {
1031         assert(Timestamp.fromISOString("20100704") == Timestamp(2010, 7, 4));
1032         assert(Timestamp.fromISOString("19981225") == Timestamp(1998, 12, 25));
1033         assert(Timestamp.fromISOString("00000105") == Timestamp(0, 1, 5));
1034         // assert(Timestamp.fromISOString("-00040105") == Timestamp(-4, 1, 5));
1035 
1036         assert(Timestamp(2021) == Timestamp.fromISOString("2021"));
1037         assert(Timestamp(2021) == Timestamp.fromISOString("2021T"));
1038         // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01"));
1039         // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01T"));
1040         assert(Timestamp(2021, 01, 29) == Timestamp.fromISOString("20210129"));
1041         assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOString("20210129T1942"));
1042         assert(Timestamp(2021, 01, 29, 19, 42).withOffset(0) == Timestamp.fromISOString("20210129T1942Z"));
1043         assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOString("20210129T194212"));
1044         assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67).withOffset(0) == Timestamp.fromISOString("20210129T194212.067Z"));
1045         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOString("20210129T194244+07"));
1046         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730"));
1047         static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730"));
1048         static assert(Timestamp(2021, 01, 29,  4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOString("20210128T211244-0730"));
1049     }
1050 
1051     version (mir_test)
1052     @safe unittest
1053     {
1054         import std.exception: assertThrown;
1055         assertThrown!DateTimeException(Timestamp.fromISOString(""));
1056         assertThrown!DateTimeException(Timestamp.fromISOString("990704"));
1057         assertThrown!DateTimeException(Timestamp.fromISOString("0100704"));
1058         assertThrown!DateTimeException(Timestamp.fromISOString("2010070"));
1059         assertThrown!DateTimeException(Timestamp.fromISOString("120100704"));
1060         assertThrown!DateTimeException(Timestamp.fromISOString("-0100704"));
1061         assertThrown!DateTimeException(Timestamp.fromISOString("+0100704"));
1062         assertThrown!DateTimeException(Timestamp.fromISOString("2010070a"));
1063         assertThrown!DateTimeException(Timestamp.fromISOString("20100a04"));
1064         assertThrown!DateTimeException(Timestamp.fromISOString("2010a704"));
1065 
1066         assertThrown!DateTimeException(Timestamp.fromISOString("99-07-04"));
1067         assertThrown!DateTimeException(Timestamp.fromISOString("010-07-04"));
1068         assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0"));
1069         assertThrown!DateTimeException(Timestamp.fromISOString("12010-07-04"));
1070         assertThrown!DateTimeException(Timestamp.fromISOString("-010-07-04"));
1071         assertThrown!DateTimeException(Timestamp.fromISOString("+010-07-04"));
1072         assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0a"));
1073         assertThrown!DateTimeException(Timestamp.fromISOString("2010-0a-04"));
1074         assertThrown!DateTimeException(Timestamp.fromISOString("2010-a7-04"));
1075         assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/04"));
1076         assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/04"));
1077         assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/4"));
1078         assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/4"));
1079         assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-04"));
1080         assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-4"));
1081         assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-4"));
1082 
1083         assertThrown!DateTimeException(Timestamp.fromISOString("99Jul04"));
1084         assertThrown!DateTimeException(Timestamp.fromISOString("010Jul04"));
1085         assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0"));
1086         assertThrown!DateTimeException(Timestamp.fromISOString("12010Jul04"));
1087         assertThrown!DateTimeException(Timestamp.fromISOString("-010Jul04"));
1088         assertThrown!DateTimeException(Timestamp.fromISOString("+010Jul04"));
1089         assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0a"));
1090         assertThrown!DateTimeException(Timestamp.fromISOString("2010Jua04"));
1091         assertThrown!DateTimeException(Timestamp.fromISOString("2010aul04"));
1092 
1093         assertThrown!DateTimeException(Timestamp.fromISOString("99-Jul-04"));
1094         assertThrown!DateTimeException(Timestamp.fromISOString("010-Jul-04"));
1095         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0"));
1096         assertThrown!DateTimeException(Timestamp.fromISOString("12010-Jul-04"));
1097         assertThrown!DateTimeException(Timestamp.fromISOString("-010-Jul-04"));
1098         assertThrown!DateTimeException(Timestamp.fromISOString("+010-Jul-04"));
1099         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0a"));
1100         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jua-04"));
1101         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jal-04"));
1102         assertThrown!DateTimeException(Timestamp.fromISOString("2010-aul-04"));
1103 
1104         // assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-04"));
1105         assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-04"));
1106 
1107         assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6));
1108         // assert(Timestamp.fromISOString("-19990706") == Timestamp(-1999, 7, 6));
1109         // assert(Timestamp.fromISOString("+019990706") == Timestamp(1999, 7, 6));
1110         assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6));
1111     }
1112 
1113     // bug# 17801
1114     version (mir_test)
1115     @safe unittest
1116     {
1117         import std.conv : to;
1118         import std.meta : AliasSeq;
1119         static foreach (C; AliasSeq!(char, wchar, dchar))
1120         {
1121             static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[]))
1122                 assert(Timestamp.fromISOString(to!S("20121221")) == Timestamp(2012, 12, 21));
1123         }
1124     }
1125 
1126     /++
1127     Creates a $(LREF Timestamp) from a string with the format `yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm`
1128     or its leading part allowed by the standard.
1129 
1130     Params:
1131         str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates.
1132         value = (optional) result value.
1133 
1134     Throws:
1135         $(LREF DateTimeException) if the given string is
1136         not in the correct format. Two arguments overload is `nothrow`.
1137     Returns:
1138         `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload.
1139     +/
1140     alias fromISOExtString = fromISOStringImpl!true;
1141 
1142 
1143     ///
1144     version (mir_test)
1145     @safe unittest
1146     {
1147         assert(Timestamp.fromISOExtString("2010-07-04") == Timestamp(2010, 7, 4));
1148         assert(Timestamp.fromISOExtString("1998-12-25") == Timestamp(1998, 12, 25));
1149         assert(Timestamp.fromISOExtString("0000-01-05") == Timestamp(0, 1, 5));
1150         assert(Timestamp.fromISOExtString("-0004-01-05") == Timestamp(-4, 1, 5));
1151 
1152         assert(Timestamp(2021) == Timestamp.fromISOExtString("2021"));
1153         assert(Timestamp(2021) == Timestamp.fromISOExtString("2021T"));
1154         assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01"));
1155         assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01T"));
1156         assert(Timestamp(2021, 01, 29) == Timestamp.fromISOExtString("2021-01-29"));
1157         assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOExtString("2021-01-29T19:42"));
1158         assert(Timestamp(2021, 01, 29, 19, 42).withOffset(0) == Timestamp.fromISOExtString("2021-01-29T19:42Z"));
1159         assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOExtString("2021-01-29T19:42:12"));
1160         assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67).withOffset(0) == Timestamp.fromISOExtString("2021-01-29T19:42:12.067Z"));
1161         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOExtString("2021-01-29T19:42:44+07"));
1162         assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30"));
1163         static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30"));
1164         static assert(Timestamp(2021, 01, 29,  4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30"));
1165     }
1166 
1167     /++
1168     Creates a $(LREF Timestamp) from a YAML string format
1169     or its leading part allowed by the standard.
1170 
1171     Params:
1172         str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates.
1173         value = (optional) result value.
1174 
1175     Throws:
1176         $(LREF DateTimeException) if the given string is
1177         not in the correct format. Two arguments overload is `nothrow`.
1178     Returns:
1179         `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload.
1180     +/
1181     alias fromYamlString = fromISOStringImpl!(true, true);
1182 
1183     ///
1184     version (mir_test)
1185     @safe unittest
1186     {
1187         // canonical
1188         assert(Timestamp.fromYamlString("2001-12-15T02:59:43.1Z") == Timestamp("2001-12-15T02:59:43.1Z"));
1189         // with lower 't' separator
1190         assert(Timestamp.fromYamlString("2001-12-14t21:59:43.1-05:30") == Timestamp("2001-12-14T21:59:43.1-05:30"));
1191         // yaml space separated
1192         assert(Timestamp.fromYamlString("2001-12-14 21:59:43.1 -5") == Timestamp("2001-12-14T21:59:43.1-05"));
1193         // no time zone (Z)
1194         assert(Timestamp.fromYamlString("2001-12-15 2:59:43.10") == Timestamp("2001-12-15T02:59:43.10"));
1195         // date 00:00:00Z
1196         assert(Timestamp.fromYamlString("2002-12-14") == Timestamp("2002-12-14"));
1197     }
1198 
1199     version (mir_test)
1200     @safe unittest
1201     {
1202         import std.exception: assertThrown;
1203 
1204         assertThrown!DateTimeException(Timestamp.fromISOExtString(""));
1205         assertThrown!DateTimeException(Timestamp.fromISOExtString("990704"));
1206         assertThrown!DateTimeException(Timestamp.fromISOExtString("0100704"));
1207         assertThrown!DateTimeException(Timestamp.fromISOExtString("120100704"));
1208         assertThrown!DateTimeException(Timestamp.fromISOExtString("-0100704"));
1209         assertThrown!DateTimeException(Timestamp.fromISOExtString("+0100704"));
1210         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010070a"));
1211         assertThrown!DateTimeException(Timestamp.fromISOExtString("20100a04"));
1212         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010a704"));
1213 
1214         assertThrown!DateTimeException(Timestamp.fromISOExtString("99-07-04"));
1215         assertThrown!DateTimeException(Timestamp.fromISOExtString("010-07-04"));
1216         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0"));
1217         assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-07-04"));
1218         assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-07-04"));
1219         assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-07-04"));
1220         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0a"));
1221         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-0a-04"));
1222         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-a7-04"));
1223         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/04"));
1224         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/04"));
1225         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/4"));
1226         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/4"));
1227         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-04"));
1228         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-4"));
1229         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-4"));
1230 
1231         assertThrown!DateTimeException(Timestamp.fromISOExtString("99Jul04"));
1232         assertThrown!DateTimeException(Timestamp.fromISOExtString("010Jul04"));
1233         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0"));
1234         assertThrown!DateTimeException(Timestamp.fromISOExtString("12010Jul04"));
1235         assertThrown!DateTimeException(Timestamp.fromISOExtString("-010Jul04"));
1236         assertThrown!DateTimeException(Timestamp.fromISOExtString("+010Jul04"));
1237         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0a"));
1238         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jua04"));
1239         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010aul04"));
1240 
1241         assertThrown!DateTimeException(Timestamp.fromISOExtString("99-Jul-04"));
1242         assertThrown!DateTimeException(Timestamp.fromISOExtString("010-Jul-04"));
1243         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0"));
1244         assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-Jul-04"));
1245         assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-Jul-04"));
1246         assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-Jul-04"));
1247         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0a"));
1248         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jua-04"));
1249         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jal-04"));
1250         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-aul-04"));
1251 
1252         assertThrown!DateTimeException(Timestamp.fromISOExtString("20100704"));
1253         assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-04"));
1254 
1255         assert(Timestamp.fromISOExtString("1999-07-06") == Timestamp(1999, 7, 6));
1256         assert(Timestamp.fromISOExtString("-1999-07-06") == Timestamp(-1999, 7, 6));
1257         assert(Timestamp.fromISOExtString("+01999-07-06") == Timestamp(1999, 7, 6));
1258     }
1259 
1260     // bug# 17801
1261     version (mir_test)
1262     @safe unittest
1263     {
1264         import std.conv : to;
1265         import std.meta : AliasSeq;
1266         static foreach (C; AliasSeq!(char, wchar, dchar))
1267         {
1268             static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[]))
1269                 assert(Timestamp.fromISOExtString(to!S("2012-12-21")) == Timestamp(2012, 12, 21));
1270         }
1271     }
1272 
1273     /++
1274     Creates a $(LREF Timestamp) from a string.
1275 
1276     Params:
1277         str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) and $(LREF .Timestamp.toISOString) format dates, also YAML like spaces seprated strings are accepted. The function is case sensetive.
1278         value = (optional) result value.
1279 
1280     Throws:
1281         $(LREF DateTimeException) if the given string is
1282         not in the correct format. Two arguments overload is `nothrow`.
1283     Returns:
1284         `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload.
1285     +/
1286     static bool fromString(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc
1287     {
1288         return fromYamlString(str, value)
1289             || fromISOString(str, value);
1290     }
1291 
1292     ///
1293     version (mir_test)
1294     @safe pure @nogc unittest
1295     {
1296         assert(Timestamp.fromString("2010-07-04") == Timestamp(2010, 7, 4));
1297         assert(Timestamp.fromString("20100704") == Timestamp(2010, 7, 4));
1298     }
1299 
1300     /// ditto
1301     static Timestamp fromString(C)(scope const(C)[] str) @safe pure
1302         if (isSomeChar!C)
1303     {
1304         Timestamp ret;
1305         if (fromString(str, ret))
1306             return ret;
1307         { import mir.exception : toMutable; throw InvalidString.toMutable; }
1308     }
1309 
1310     template fromISOStringImpl(bool ext, bool yaml = false)
1311     {
1312         static Timestamp fromISOStringImpl(C)(scope const(C)[] str) @safe pure
1313             if (isSomeChar!C)
1314         {
1315             Timestamp ret;
1316             if (fromISOStringImpl(str, ret))
1317                 return ret;
1318             static if (yaml)
1319                 { import mir.exception : toMutable; throw InvalidYamlString.toMutable; }
1320             else
1321             static if (ext)
1322                 { import mir.exception : toMutable; throw InvalidISOExtendedString.toMutable; }
1323             else
1324                 { import mir.exception : toMutable; throw InvalidISOString.toMutable; }
1325         }
1326 
1327         static bool fromISOStringImpl(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc
1328             if (isSomeChar!C)
1329         {
1330             import mir.parse: fromString, parse;
1331 
1332             if (str.length < 4)
1333                 return false;
1334 
1335             static if (ext)
1336                 auto isOnlyTime = (str[0] == 'T' || yaml && (str[0] == 't')) || str[2] == ':';
1337             else
1338                 auto isOnlyTime = str[0] == 'T' || yaml && (str[0] == 't');
1339 
1340             if (!isOnlyTime)
1341             {
1342                 // YYYY
1343                 static if (ext)
1344                 {{
1345                     auto startIsDigit = str.length && str[0].isDigit;
1346                     auto strOldLength = str.length;
1347                     if (!parse(str, value.year))
1348                         return false;
1349                     auto l = strOldLength - str.length;
1350                     if ((l == 4) != startIsDigit)
1351                         return false;
1352                 }}
1353                 else
1354                 {
1355                     if (str.length < 4 || !str[0].isDigit || !fromString(str[0 .. 4], value.year))
1356                         return false;
1357                     str = str[4 .. $];
1358                 }
1359 
1360                 value.precision = Precision.year;
1361                 if (str.length == 0 || str == "T")
1362                     return true;
1363                 
1364                 static if (ext)
1365                 {
1366                     if (str[0] != '-')
1367                         return false;
1368                     str = str[1 .. $];
1369                 }
1370 
1371                 // MM
1372                 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.month))
1373                     return false;
1374                 str = str[2 .. $];
1375                 value.precision = Precision.month;
1376                 if (str.length == 0 || str.length == 1 && (str[0] == 'T' || (yaml && (str[0] == 't' || str[0] == ' ' || str[0] == '\t'))))
1377                     return ext;
1378 
1379                 static if (ext)
1380                 {
1381                     if (str[0] != '-')
1382                         return false;
1383                     str = str[1 .. $];
1384                 }
1385 
1386                 // DD
1387                 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.day))
1388                     return false;
1389                 str = str[2 .. $];
1390                 value.precision = Precision.day;
1391                 if (str.length == 0)
1392                     return true;
1393             }
1394 
1395             // str isn't empty here
1396             // T
1397             if ((str[0] == 'T' || (yaml && (str[0] == 't' || str[0] == ' ' || str[0] == '\t'))))
1398             {
1399                 str = str[1 .. $];
1400                 // OK, onlyTime requires length >= 3
1401                 if (str.length == 0)
1402                     return true;
1403             }
1404             else 
1405             {
1406                 if (!(ext && isOnlyTime))
1407                     return false;
1408             }
1409 
1410             value.precision = Precision.minute; // we don't have hour precision
1411 
1412             // hh
1413             if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.hour))
1414             {
1415                 static if (yaml)
1416                 {
1417                     if (str.length < 1 || !str[0].isDigit || !fromString(str[0 .. 1], value.hour))
1418                         return false;
1419                     else
1420                         str = str[1 .. $];
1421                 }
1422                 else
1423                     return false;
1424             }
1425             else
1426                 str = str[2 .. $];
1427 
1428             if (str.length == 0)
1429                 return true;
1430 
1431             static if (ext)
1432             {
1433                 if (str[0] != ':')
1434                     return false;
1435                 str = str[1 .. $];
1436             }
1437 
1438             // mm
1439             {
1440                 uint minute;
1441                 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute))
1442                     return false;
1443                 value.minute = cast(ubyte) minute;
1444                 str = str[2 .. $];
1445                 if (str.length == 0)
1446                     return true;
1447             }
1448 
1449             static if (ext)
1450             {
1451                 if (str[0] != ':')
1452                     goto TZ;
1453                 str = str[1 .. $];
1454             }
1455 
1456             // ss
1457             {
1458                 uint second;
1459                 if (str.length < 2 || !str[0].isDigit)
1460                     goto TZ;
1461                 if (!fromString(str[0 .. 2], second))
1462                     return false;
1463                 value.second = cast(ubyte) second;
1464                 str = str[2 .. $];
1465                 value.precision = Precision.second;
1466                 if (str.length == 0)
1467                     return true;
1468             }
1469 
1470             // .
1471             if (str[0] != '.')
1472                 goto TZ;
1473             str = str[1 .. $];
1474             value.precision = Precision.fraction;
1475 
1476             // fraction
1477             {
1478                 const strOldLength = str.length;
1479                 ulong fractionCoefficient;
1480                 if (str.length < 1 || !str[0].isDigit || !parse!ulong(str, fractionCoefficient))
1481                     return false;
1482                 sizediff_t fractionExponent = str.length - strOldLength;
1483                 if (fractionExponent < -12)
1484                     return false;
1485                 value.fractionExponent = cast(byte)fractionExponent;
1486                 value.fractionCoefficient = fractionCoefficient;
1487                 if (str.length == 0)
1488                     return true;
1489             }
1490 
1491         TZ:
1492 
1493             static if (yaml)
1494             {
1495                 if (str.length && (str[0] == ' ' || str[0] == '\t'))
1496                     str = str[1 .. $];
1497             }
1498 
1499             if (str == "Z")
1500             {
1501                 value.offset = 0;
1502                 return true;
1503             }
1504 
1505             bool neg = str[0] == '-';
1506 
1507             int hour;
1508             int minute;
1509             if (str.length < 3 || str[0].isDigit || !fromString(str[0 .. 3], hour))
1510             {
1511                 static if (yaml)
1512                 {
1513                     if (str.length < 2 || str[0].isDigit || !fromString(str[0 .. 2], hour))
1514                         return false;
1515                     str = str[2 .. $];
1516                 }
1517                 else
1518                     return false;
1519             }
1520             else
1521             {
1522                 str = str[3 .. $];
1523             }
1524 
1525             if (str.length)
1526             {
1527                 static if (ext)
1528                 {
1529                     if (str[0] != ':')
1530                         return false;
1531                     str = str[1 .. $];
1532                 }
1533                 if (str.length != 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute))
1534                     return false;
1535             }
1536 
1537             if (neg && hour == 0 && minute == 0)
1538                 value.setLocalTimezone;
1539             else
1540                 value.offset = cast(short)(hour * 60 + (hour < 0 ? -minute : minute));
1541             value.addMinutes(cast(short)-int(value.offset));
1542             return true;
1543         }
1544     }
1545 
1546     ///
1547     bool isDuration() const @safe pure nothrow @nogc @property
1548     {
1549         return day == 88 || day == 99;
1550     }
1551 
1552     ///
1553     bool isNegativeDuration() const @safe pure nothrow @nogc @property
1554     {
1555         return day == 99;
1556     }
1557 }
1558 
1559 version(mir_test)
1560 unittest
1561 {
1562     long sec = -2208988800;
1563     uint nanosec = 0;
1564     auto ts = Timestamp.fromUnixTime(sec);
1565     if (nanosec >= 0)
1566     {
1567         ts.precision = Timestamp.Precision.fraction;
1568         ts.fractionCoefficient = nanosec;
1569         ts.fractionExponent = -9;
1570     }
1571     auto ts2 = "1900-01-01T00:00:00.000000000".Timestamp;
1572     assert(ts == ts2);
1573 }
1574 
1575 version(mir_test)
1576 @safe pure unittest
1577 {
1578     import std.datetime.systime : SysTime;
1579     import mir.test;
1580     auto ts = "2001-12-15T2:59:43.1234567".Timestamp;
1581     // ts.toString.should == "2001-12-15T02:59:43.1234567-00:00";
1582     auto st = cast(SysTime)ts;
1583     // st.toISOExtString.should == "2001-12-15T02:59:43.1234567";
1584     st.Timestamp.should == ts;
1585 }
1586 
1587 private auto assumePureSafe(T)(T t) @trusted
1588     // if (isFunctionPointer!T || isDelegate!T)
1589 {
1590     import std.traits;
1591     enum attrs = (functionAttributes!T | FunctionAttribute.pure_ | FunctionAttribute.safe) & ~FunctionAttribute.system;
1592     return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t;
1593 }