1 /** 2 * Time parsing functions. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.time.parse; 15 16 import core.time : minutes, seconds, dur; 17 18 import std.exception : enforce; 19 import std.conv : to; 20 import std.ascii : isDigit, isWhite; 21 import std.datetime; 22 import std.string : indexOf; 23 import std.typecons : Rebindable; 24 import std.string : strip, startsWith; 25 26 import ae.utils.time.common; 27 28 private struct ParseContext(Char, bool checked) 29 { 30 int year=0, month=1, day=1, hour=0, minute=0, second=0, usecs=0; 31 int hour12 = 0; bool pm; 32 Rebindable!(immutable(TimeZone)) tz; 33 int dow = -1; 34 Char[] t; 35 bool escaping; 36 37 void need(size_t n)() 38 { 39 static if (checked) 40 enforce(t.length >= n, "Not enough characters in date string"); 41 } 42 43 auto take(size_t n)() 44 { 45 need!n(); 46 auto result = t[0..n]; 47 t = t[n..$]; 48 return result; 49 } 50 51 char takeOne() 52 { 53 need!1(); 54 auto result = t[0]; 55 t = t[1..$]; 56 return result; 57 } 58 59 R takeNumber(size_t n, sizediff_t maxP = -1, R = int)() 60 { 61 enum max = maxP == -1 ? n : maxP; 62 need!n(); 63 foreach (i, c; t[0..n]) 64 enforce((i==0 && c=='-') || isDigit(c) || isWhite(c), "Number expected"); 65 static if (n == max) 66 enum i = n; 67 else 68 { 69 auto i = n; 70 while (i < max && (checked ? i < t.length : true) && isDigit(t[i])) 71 i++; 72 } 73 auto s = t[0..i]; 74 t = t[i..$]; 75 return s.strip().to!R(); 76 } 77 78 int takeWord(in string[] words, string name) 79 { 80 foreach (idx, string word; words) 81 { 82 static if (checked) 83 bool b = t.startsWith(word); 84 else 85 bool b = t[0..word.length] == word; 86 if (b) 87 { 88 t = t[word.length..$]; 89 return cast(int)idx; 90 } 91 } 92 throw new Exception(name ~ " expected"); 93 } 94 95 char peek() 96 { 97 need!1(); 98 return *t.ptr; 99 } 100 } 101 102 private void parseToken(alias c, alias context)() 103 { 104 with (context) 105 { 106 // TODO: check if the compiler optimizes this check away 107 // in the compile-time version. If not, "escaping" needs to 108 // be moved into an alias parameter. 109 if (escaping) 110 { 111 enforce(takeOne() == c, c ~ " expected"); 112 escaping = false; 113 return; 114 } 115 116 switch (c) 117 { 118 // Day 119 case 'd': 120 day = takeNumber!(2)(); 121 break; 122 case 'D': 123 dow = takeWord(WeekdayShortNames, "Weekday"); 124 break; 125 case 'j': 126 day = takeNumber!(1, 2); 127 break; 128 case 'l': 129 dow = takeWord(WeekdayLongNames, "Weekday"); 130 break; 131 case 'N': 132 dow = takeNumber!1 % 7; 133 break; 134 case 'S': // ordinal suffix 135 take!2; 136 break; 137 case 'w': 138 dow = takeNumber!1; 139 break; 140 //case 'z': TODO 141 142 // Week 143 //case 'W': TODO 144 145 // Month 146 case 'F': 147 month = takeWord(MonthLongNames, "Month") + 1; 148 break; 149 case 'm': 150 month = takeNumber!2; 151 break; 152 case 'M': 153 month = takeWord(MonthShortNames, "Month") + 1; 154 break; 155 case 'n': 156 month = takeNumber!(1, 2); 157 break; 158 case 't': 159 takeNumber!(1, 2); // TODO: validate DIM? 160 break; 161 162 // Year 163 case 'L': 164 takeNumber!1; // TODO: validate leapness? 165 break; 166 // case 'o': TODO (ISO 8601 year number) 167 case 'Y': 168 year = takeNumber!4; 169 break; 170 case 'y': 171 year = takeNumber!2; 172 if (year > 50) // TODO: find correct logic for this 173 year += 1900; 174 else 175 year += 2000; 176 break; 177 178 // Time 179 case 'a': 180 pm = takeWord(["am", "pm"], "am/pm")==1; 181 break; 182 case 'A': 183 pm = takeWord(["AM", "PM"], "AM/PM")==1; 184 break; 185 // case 'B': TODO (Swatch Internet time) 186 case 'g': 187 hour12 = takeNumber!(1, 2); 188 break; 189 case 'G': 190 hour = takeNumber!(1, 2); 191 break; 192 case 'h': 193 hour12 = takeNumber!2; 194 break; 195 case 'H': 196 hour = takeNumber!2; 197 break; 198 case 'i': 199 minute = takeNumber!2; 200 break; 201 case 's': 202 second = takeNumber!2; 203 break; 204 case 'u': 205 usecs = takeNumber!6; 206 break; 207 case 'E': // not standard 208 usecs = 1000 * takeNumber!3; 209 break; 210 211 // Timezone 212 // case 'e': ??? 213 case 'I': 214 takeNumber!1; 215 break; 216 case 'O': 217 { 218 if (peek() == 'Z') 219 { 220 t = t[1..$]; 221 tz = UTC(); 222 } 223 else 224 if (peek() == 'G') 225 { 226 enforce(take!3() == "GMT", "GMT expected"); 227 tz = UTC(); 228 } 229 else 230 { 231 auto tzStr = take!5(); 232 enforce(tzStr[0]=='-' || tzStr[0]=='+', "- / + expected"); 233 auto n = (to!int(tzStr[1..3]) * 60 + to!int(tzStr[3..5])) * (tzStr[0]=='-' ? -1 : 1); 234 tz = new immutable(SimpleTimeZone)(minutes(n)); 235 } 236 break; 237 } 238 case 'P': 239 { 240 auto tzStr = take!6(); 241 enforce(tzStr[0]=='-' || tzStr[0]=='+', "- / + expected"); 242 enforce(tzStr[3]==':', ": expected"); 243 auto n = (to!int(tzStr[1..3]) * 60 + to!int(tzStr[4..6])) * (tzStr[0]=='-' ? -1 : 1); 244 tz = new immutable(SimpleTimeZone)(minutes(n)); 245 break; 246 } 247 case 'T': 248 version(Posix) 249 tz = PosixTimeZone.getTimeZone(t.idup); 250 else 251 version(Windows) 252 tz = WindowsTimeZone.getTimeZone(t.idup); 253 254 t = null; 255 break; 256 case 'Z': 257 { 258 // TODO: is this correct? 259 auto n = takeNumber!(1, 6); 260 tz = new immutable(SimpleTimeZone)(seconds(n)); 261 break; 262 } 263 264 // Full date/time 265 //case 'c': TODO 266 //case 'r': TODO 267 //case 'U': TODO 268 269 // Escape next character 270 case '\\': 271 escaping = true; 272 break; 273 274 // Other characters (whitespace, delimiters) 275 default: 276 { 277 enforce(t.length && t[0]==c, c~ " expected or unsupported format character"); 278 t = t[1..$]; 279 } 280 } 281 } 282 } 283 284 import ae.utils.meta; 285 286 private SysTime parseTimeImpl(alias fmt, bool checked, C)(C[] t, immutable TimeZone defaultTZ = null) 287 { 288 ParseContext!(C, checked) context; 289 context.t = t; 290 context.tz = defaultTZ; 291 292 foreach (c; CTIterate!fmt) 293 parseToken!(c, context)(); 294 295 enforce(context.t.length == 0, "Left-over characters: " ~ context.t); 296 297 SysTime result; 298 299 with (context) 300 { 301 if (hour12) 302 hour = hour12%12 + (pm ? 12 : 0); 303 304 // Compatibility with both <=2.066 and >=2.067 305 static if (__traits(hasMember, SysTime, "fracSecs")) 306 auto frac = dur!"usecs"(usecs); 307 else 308 auto frac = FracSec.from!"usecs"(usecs); 309 310 result = SysTime( 311 DateTime(year, month, day, hour, minute, second), 312 frac, 313 tz); 314 315 if (dow >= 0) 316 enforce(result.dayOfWeek == dow, "Mismatching weekday"); 317 } 318 319 return result; 320 } 321 322 /// Parse the given string into a SysTime, using the format spec fmt. 323 /// This version generates specialized code for the given fmt. 324 SysTime parseTime(string fmt, C)(C[] t, immutable TimeZone tz = null) 325 { 326 // Omit length checks if we know the input string is long enough 327 enum maxLength = timeFormatSize(fmt); 328 if (t.length < maxLength) 329 return parseTimeImpl!(fmt, true )(t, tz); 330 else 331 return parseTimeImpl!(fmt, false)(t, tz); 332 } 333 334 /// Parse the given string into a SysTime, using the format spec fmt. 335 /// This version parses fmt at runtime. 336 SysTime parseTimeUsing(C)(C[] t, in char[] fmt) 337 { 338 return parseTimeImpl!(fmt, true)(t); 339 } 340 341 deprecated SysTime parseTime(C)(const(char)[] fmt, C[] t) 342 { 343 return t.parseTimeUsing(fmt); 344 } 345 346 version(unittest) import ae.utils.time.format; 347 348 unittest 349 { 350 const s0 = "Tue Jun 07 13:23:19 GMT+0100 2011"; 351 //enum t = s0.parseTime!(TimeFormats.STD_DATE); // https://d.puremagic.com/issues/show_bug.cgi?id=12042 352 auto t = s0.parseTime!(TimeFormats.STD_DATE); 353 auto s1 = t.formatTime(TimeFormats.STD_DATE); 354 assert(s0 == s1, s0 ~ "/" ~ s1); 355 auto t1 = s0.parseTimeUsing(TimeFormats.STD_DATE); 356 assert(t == t1); 357 } 358 359 unittest 360 { 361 "Tue, 21 Nov 2006 21:19:46 +0000".parseTime!(TimeFormats.RFC2822); 362 "Tue, 21 Nov 2006 21:19:46 +0000".parseTimeUsing(TimeFormats.RFC2822); 363 } 364 365 unittest 366 { 367 const char[] s = "Tue, 21 Nov 2006 21:19:46 +0000"; 368 s.parseTime!(TimeFormats.RFC2822); 369 } 370 371 /// Parse log timestamps generated by ae.sys.log, 372 /// including all previous versions of it. 373 SysTime parseLogTimestamp(string s) 374 { 375 enforce(s.length, "Empty line"); 376 377 if (s[0] == '[') // Input is an entire line 378 { 379 auto i = s.indexOf(']'); 380 enforce(i > 0, "Unmatched ["); 381 s = s[1..i]; 382 } 383 384 switch (s.length) 385 { 386 case 33: // Fri Jun 29 05:44:13 GMT+0300 2007 387 return s.parseTime!(TimeFormats.STD_DATE)(UTC()); 388 case 23: 389 if (s[4] == '.') // 2015.02.24 21:03:01.868 390 return s.parseTime!"Y.m.d H:i:s.E"(UTC()); 391 else // 2015-11-04 00:00:45.964 392 return s.parseTime!"Y-m-d H:i:s.E"(UTC()); 393 case 26: // 2015-11-04 00:00:45.964983 394 return s.parseTime!"Y-m-d H:i:s.u"(UTC()); 395 default: 396 throw new Exception("Unknown log timestamp format: " ~ s); 397 } 398 }