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