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