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