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