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 `−`, `‐`, `–`) ? -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 }