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