1 module nxt.datetime_ex;
2 
3 /** UTC Offset.
4     See_Also: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets
5     See_Also: http://forum.dlang.org/post/csurwdcdyfocrotojons@forum.dlang.org
6 */
7 struct UTCOffset
8 {
9     enum minHour = -12, maxHour = +14;
10     enum minMinute = 0, maxMinute = 45;
11 
12     static immutable hourNames = [`-12`, `-11`, `-10`, `-09`, `-08`, `-07`, `-06`, `-05`, `-04`, `-03`, `-02`, `-01`,
13                                   `±00`,
14                                   `+01`, `+02`, `+03`, `+04`, `+05`, `+06`, `+07`, `+08`, `+09`, `+10`, `+11`, `+12`,
15                                   `+13`, `+14`];
16 
17     static immutable quarterValues = [00, 15, 30, 45];
18     static immutable quarterNames = ["00", "15", "30", "45"];
19 
20     @property void toString(scope void delegate(scope const(char)[]) sink) const @trusted
21     {
22         if (isDefined)
23         {
24             // tag prefix
25             sink(`UTC`);
26 
27             sink(hourNames[_hour + 12]);
28 
29             sink(`:`);
30 
31             // minute
32             immutable minute = quarterNames[_quarter];
33             sink(minute);
34         }
35         else
36         {
37             sink("<Uninitialized UTCOffset>");
38         }
39     }
40 
41     @safe:
42 
43     @property string toString() const @trusted pure
44     {
45         import nxt.assuming : assumePure;
46         return assumePure(&toStringUnpure)(); // TODO can we avoid this?
47     }
48 
49     string toStringUnpure() const @safe
50     {
51         import std.conv : to;
52         return to!string(this);
53     }
54 
55     @safe pure:
56 
57     this(scope const(char)[] code, bool strictFormat = false)
58     {
59         import std.conv : to;
60 
61         import nxt.skip_ex : skipOverEither;
62         import nxt.array_algorithm : startsWith, skipOver;
63 
64         // TODO support and use CT-arguments in skipOverEither()
65         if (strictFormat && !code.startsWith("UTC"))
66         {
67             this(0, 0);
68             _initializedFlag = false;
69         }
70         else
71         {
72             code.skipOverEither("UTC", "GMT");
73 
74             code.skipOver(" ");
75             code.skipOverEither("+", "±", `\u00B1`, "\u00B1");
76             // try in order of probability
77             immutable sign = code.skipOverEither(`-`,
78                                                  "\u2212", "\u2011", "\u2013", // quoting
79                                                  `\u2212`, `\u2011`, `\u2013`,  // UTF-8
80                                                  `&minus;`, `&dash;`, `&ndash;`) ? -1 : +1; // HTML
81             code.skipOver(" ");
82 
83             import std.algorithm.comparison : among;
84 
85             if (code.length == 4 && code[1].among!(':', '.')) // H:MM
86             {
87                 immutable hour = sign*(code[0 .. 1].to!byte);
88                 this(cast(byte)hour,
89                      code[2 .. $].to!ubyte);
90             }
91             else if (code.length == 5 && code[2].among!(':', '.')) // HH:MM
92             {
93                 immutable hour = sign*(code[0 .. 2].to!byte);
94                 this(cast(byte)hour,
95                      code[3 .. $].to!ubyte);
96             }
97             else
98             {
99                 try
100                 {
101                     immutable hour = sign*code.to!byte;
102                     this(cast(byte)hour, 0);
103                 }
104                 catch (Exception E)
105                 {
106                     _initializedFlag = false;
107                 }
108             }
109         }
110 
111     }
112 
113     nothrow:
114 
115     this(byte hour, ubyte minute)
116     {
117         if (hour >= minHour &&
118             hour <= maxHour &&
119             minute >= minMinute &&
120             minute <= maxMinute)
121         {
122             _hour = hour;
123             _initializedFlag = true;
124             switch (minute)
125             {
126             case 0: _quarter = 0; break;
127             case 15: _quarter = 1; break;
128             case 30: _quarter = 2; break;
129             case 45: _quarter = 3; break;
130             default: _initializedFlag = false; break;
131             }
132         }
133         else
134         {
135             _hour = 0;
136             _quarter = 0;
137             _initializedFlag = false;
138         }
139     }
140 
141     /// Cast to `bool`, meaning 'true' if defined, `false` otherwise.
142     bool opCast(U : bool)() const
143     {
144         version(D_Coverage) {} else pragma(inline, true);
145         return isDefined();
146     }
147 
148     int opCmp(in typeof(this) that) const @trusted
149     {
150         version(D_Coverage) {} else pragma(inline, true);
151         immutable a = *cast(ubyte*)&this;
152         immutable b = *cast(ubyte*)&that;
153         return a < b ? -1 : a > b ? 1 : 0;
154     }
155 
156     @property auto hour()      const { return _hour; }
157     @property auto minute()    const { return _quarter * 15; }
158     @property bool isDefined() const { return _initializedFlag; }
159 
160 private:
161     import std.bitmanip : bitfields;
162     mixin(bitfields!(byte, "_hour", 5, // Hours: [-12 up to +14]
163                      ubyte, "_quarter", 2, // Minutes in Groups of 15: 0, 15, 30, 45
164                      bool, "_initializedFlag", 1));
165 }
166 
167 @safe pure // nothrow
168 unittest
169 {
170     static assert(UTCOffset.sizeof == 1); // assert packet storage
171 
172     assert(UTCOffset(-12, 0));
173 
174     assert(UTCOffset(-12, 0) !=
175            UTCOffset(  0, 0));
176 
177     assert(UTCOffset(-12, 0) ==
178            UTCOffset(-12, 0));
179 
180     assert(UTCOffset(-12, 0) <
181            UTCOffset(-11, 0));
182 
183     assert(UTCOffset(-11, 0) <=
184            UTCOffset(-11, 0));
185 
186     assert(UTCOffset(-11, 0) <=
187            UTCOffset(-11, 15));
188 
189     assert(UTCOffset(-12, 0) <
190            UTCOffset(+14, 15));
191 
192     assert(UTCOffset(+14, 15) <=
193            UTCOffset(+14, 15));
194 
195     assert(UTCOffset(-12, 0).hour == -12);
196     assert(UTCOffset(+14, 0).hour == +14);
197 
198     assert(UTCOffset("").toString == "<Uninitialized UTCOffset>");
199     assert(UTCOffset(UTCOffset.minHour - 1, 0).toString == "<Uninitialized UTCOffset>");
200     assert(UTCOffset(UTCOffset.maxHour + 1, 0).toString == "<Uninitialized UTCOffset>");
201     assert(UTCOffset(UTCOffset.minHour, 1).toString == "<Uninitialized UTCOffset>");
202     assert(UTCOffset(UTCOffset.minHour, 46).toString == "<Uninitialized UTCOffset>");
203 
204     assert(UTCOffset(-12,  0).toString == "UTC-12:00");
205     assert(UTCOffset(-11,  0).toString == "UTC-11:00");
206     assert(UTCOffset(-10,  0).toString == "UTC-10:00");
207     assert(UTCOffset(- 9,  0).toString == "UTC-09:00");
208     assert(UTCOffset(- 8,  0).toString == "UTC-08:00");
209     assert(UTCOffset(- 7,  0).toString == "UTC-07:00");
210     assert(UTCOffset(- 6,  0).toString == "UTC-06:00");
211     assert(UTCOffset(- 5,  0).toString == "UTC-05:00");
212     assert(UTCOffset(- 4,  0).toString == "UTC-04:00");
213     assert(UTCOffset(- 3,  0).toString == "UTC-03:00");
214     assert(UTCOffset(- 2,  0).toString == "UTC-02:00");
215     assert(UTCOffset(- 1,  0).toString == "UTC-01:00");
216     assert(UTCOffset(+ 0,  0).toString == "UTC±00:00");
217     assert(UTCOffset(+ 1,  0).toString == "UTC+01:00");
218     assert(UTCOffset(+ 2,  0).toString == "UTC+02:00");
219     assert(UTCOffset(+ 3,  0).toString == "UTC+03:00");
220     assert(UTCOffset(+ 4,  0).toString == "UTC+04:00");
221     assert(UTCOffset(+ 5,  0).toString == "UTC+05:00");
222     assert(UTCOffset(+ 6,  0).toString == "UTC+06:00");
223     assert(UTCOffset(+ 7,  0).toString == "UTC+07:00");
224     assert(UTCOffset(+ 8, 15).toString == "UTC+08:15");
225     assert(UTCOffset(+ 9, 15).toString == "UTC+09:15");
226     assert(UTCOffset(+10, 15).toString == "UTC+10:15");
227     assert(UTCOffset(+11, 15).toString == "UTC+11:15");
228     assert(UTCOffset(+12, 15).toString == "UTC+12:15");
229     assert(UTCOffset(+13, 15).toString == "UTC+13:15");
230     assert(UTCOffset(+14,  0).toString == "UTC+14:00");
231 
232     import std.conv : to;
233     // assert(UTCOffset(+14, 0).to!string == "UTC+14:00");
234 
235     assert(UTCOffset("-1"));
236     assert(UTCOffset(-12, 0) == UTCOffset("-12"));
237     assert(UTCOffset(-12, 0) == UTCOffset("\u221212"));
238     assert(UTCOffset(+14, 0) == UTCOffset("+14"));
239 
240     assert(UTCOffset(+14, 0) == UTCOffset("UTC+14"));
241 
242     assert(UTCOffset(+03, 30) == UTCOffset("+3:30"));
243     assert(UTCOffset(+03, 30) == UTCOffset("+03:30"));
244     assert(UTCOffset(+14, 00) == UTCOffset("UTC+14:00"));
245 
246     assert(UTCOffset(+14, 00) == "UTC+14:00".to!UTCOffset);
247 
248     assert(!UTCOffset("+14:00", true)); // strict faiure
249     assert(UTCOffset("UTC+14:00", true)); // strict pass
250 }
251 
252 /** Year and Month.
253 
254     If month is specified we probably aren't interested in years before 0 so
255     store only years 0 .. 2^12-1 (4095). This makes this struct fit in 2 bytes.
256  */
257 struct YearMonth
258 {
259     import std.datetime : Month;
260 
261     import std.bitmanip : bitfields;
262     mixin(bitfields!(ushort, "year", 12,
263                      Month, "month", 4));
264 
265     pragma(inline) this(int year, Month month)
266         @safe pure nothrow @nogc
267     {
268         assert(0 <= year && year <= 2^^12 - 1); // assert within range
269         this.year = cast(ushort)year;
270         this.month = month;
271     }
272 
273     /// No explicit destruction needed.
274     ~this() @safe pure nothrow @nogc {} // needed for @nogc use
275 
276     @safe pure:
277 
278     this(scope const(char)[] s)
279     {
280         import std.algorithm.searching : findSplit;
281         auto parts = s.findSplit(` `); // TODO s.findSplitAtElement(' ')
282         if (parts &&
283             parts[0].length >= 3) // at least three letters in month
284         {
285             import std.conv : to;
286 
287             // decode month
288             import core.internal.traits : Unqual;
289             Unqual!(typeof(s[0])[3]) tmp = parts[0][0 .. 3]; // TODO functionize to parts[0].staticSubArray!(0, 3)
290             import std.ascii : toLower;
291             tmp[0] = tmp[0].toLower;
292             month = tmp.to!Month;
293 
294             // decode year
295             year = parts[2].to!(typeof(year));
296 
297             return;
298         }
299 
300         import std.conv;
301         throw new std.conv.ConvException("Couldn't decode year and month from string");
302     }
303 
304     @property string toString() const
305     {
306         import std.conv : to;
307         return year.to!string ~ `-` ~ (cast(ubyte)month).to!string; // TODO avoid GC allocation
308     }
309 
310     hash_t toHash() const @trusted nothrow @nogc
311     {
312         alias ThisUnsigned = short;
313         assert(this.sizeof == ThisUnsigned.sizeof);
314         return *((cast(ThisUnsigned*)&this));
315     }
316 
317 pragma(inline):
318 
319     int opCmp(in typeof(this) that) const nothrow @nogc
320     {
321         if (this.year < that.year)
322         {
323             return -1;
324         }
325         else if (this.year > that.year)
326         {
327             return +1;
328         }
329         else
330         {
331             if (this.month < that.month)
332             {
333                 return -1;
334             }
335             else if (this.month > that.month)
336             {
337                 return +1;
338             }
339             else
340             {
341                 return 0;
342             }
343         }
344     }
345 }
346 
347 @safe pure /*TODO @nogc*/ unittest
348 {
349     import std.datetime : Month;
350     Month month;
351 
352     static assert(YearMonth.sizeof == 2); // assert packed storage
353 
354     const a = YearMonth(`April 2016`);
355 
356     assert(a != YearMonth.init);
357 
358     assert(a == YearMonth(2016, Month.apr));
359     assert(a != YearMonth(2016, Month.may));
360     assert(a != YearMonth(2015, Month.apr));
361 
362     assert(a.year == 2016);
363     assert(a.month == Month.apr);
364 
365     assert(YearMonth(`April 1900`) == YearMonth(1900, Month.apr));
366     assert(YearMonth(`april 1900`) == YearMonth(1900, Month.apr));
367     assert(YearMonth(`apr 1900`) == YearMonth(1900, Month.apr));
368     assert(YearMonth(`Apr 1900`) == YearMonth(1900, Month.apr));
369 
370     assert(YearMonth(`Apr 1900`) != YearMonth(1901, Month.apr));
371     assert(YearMonth(`Apr 1900`) < YearMonth(1901, Month.apr));
372     assert(YearMonth(`Apr 1901`) > YearMonth(1900, Month.apr));
373 
374     assert(YearMonth(`Apr 1900`) < YearMonth(1901, Month.may));
375 
376     assert(YearMonth(`Apr 1900`) < YearMonth(1901, Month.may));
377     assert(YearMonth(`May 1900`) < YearMonth(1901, Month.apr));
378 }