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