1 /*******************************************************************************
2
3 Converts between native and text representations of HTTP time
4 values. Internally, time is represented as UTC with an epoch
5 fixed at Jan 1st 1970. The text representation is formatted in
6 accordance with RFC 1123, and the parser will accept one of
7 RFC 1123, RFC 850, or asctime formats.
8
9 See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html for
10 further detail.
11
12 Applying the D "import alias" mechanism to this module is highly
13 recommended, in order to limit namespace pollution:
14
15 ---
16 import TimeStamp = ocean.text.convert.TimeStamp;
17
18 auto t = TimeStamp.parse ("Sun, 06 Nov 1994 08:49:37 GMT");
19 ---
20
21 Copyright:
22 Copyright (c) 2004 Kris Bell.
23 Some parts copyright (c) 2009-2016 dunnhumby Germany GmbH.
24 All rights reserved.
25
26 License:
27 Tango Dual License: 3-Clause BSD License / Academic Free License v3.0.
28 See LICENSE_TANGO.txt for details.
29
30 Version: Initial release: May 2005
31
32 Authors: Kris
33
34 *******************************************************************************/
35
36 module ocean.text.convert.TimeStamp;
37
38 import ocean.core.ExceptionDefinitions;
39 import ocean.core.Verify;
40 import ocean.meta.types.Qualifiers;
41 import ocean.time.Time;
42 import ocean.text.convert.Formatter;
43 import ocean.time.chrono.Gregorian;
44
45 version (unittest) import ocean.core.Test;
46
47 /******************************************************************************
48
49 Parse provided input and return a UTC epoch time. An exception
50 is raised where the provided string is not fully parsed.
51
52 ******************************************************************************/
53
54 ulong toTime(T) (T[] src)
55 {
56 uint len;
57
58 auto x = parse (src, &len);
59 if (len < src.length)
60 throw new IllegalArgumentException ("unknown time format: "~src);
61 return x;
62 }
63
64 /******************************************************************************
65
66 Template wrapper to make life simpler. Returns a text version
67 of the provided value.
68
69 See format() for details
70
71 ******************************************************************************/
72
73 char[] toString (Time time)
74 {
75 char[32] tmp = void;
76
77 return format (tmp, time).dup;
78 }
79
80 /******************************************************************************
81
82 RFC1123 formatted time
83
84 Converts to the format "Sun, 06 Nov 1994 08:49:37 GMT", and
85 returns a populated slice of the provided buffer. Note that
86 RFC1123 format is always in absolute GMT time, and a thirty-
87 element buffer is sufficient for the produced output
88
89 Throws an exception where the supplied time is invalid
90
91 ******************************************************************************/
92
93 const(T)[] format(T, U=Time) (T[] output, U t)
94 {return format!(T)(output, cast(Time) t);}
95
96 const(T)[] format(T) (T[] output, Time t)
97 {
98 static immutable T[][] Months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
99 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
100 static immutable T[][] Days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
101
102 verify(output.length >= 29);
103 if (t is t.max)
104 throw new IllegalArgumentException("TimeStamp.format :: invalid Time argument");
105
106 // convert time to field values
107 const time = t.time;
108 const date = Gregorian.generic.toDate(t);
109
110 return snformat(output, "{}, {u2} {} {u4} {u2}:{u2}:{u2} GMT",
111 Days[date.dow], date.day, Months[date.month-1], date.year,
112 time.hours, time.minutes, time.seconds);
113 }
114
115 unittest
116 {
117 static immutable STR_1970 = "Thu, 01 Jan 1970 00:00:00 GMT";
118 mstring buf;
119 buf.length = 29;
120 test(format(buf, Time.epoch1970) == STR_1970);
121 char[29] static_buf;
122 test(format(static_buf, Time.epoch1970) == STR_1970);
123 }
124
125 /******************************************************************************
126
127 ISO-8601 format :: "2006-01-31T14:49:30Z"
128
129 Throws an exception where the supplied time is invalid
130
131 ******************************************************************************/
132
133 const(T)[] format8601(T, U=Time) (T[] output, U t)
134 {return format!(T)(output, cast(Time) t);}
135
136 const(T)[] format8601(T) (T[] output, Time t)
137 {
138 verify(output.length >= 29);
139 if (t is t.max)
140 throw new IllegalArgumentException("TimeStamp.format :: invalid Time argument");
141
142 // convert time to field values
143 const time = t.time;
144 const date = Gregorian.generic.toDate(t);
145
146 return snformat(output, "{u4}-{u2}-{u2}T{u2}:{u2}:{u2}Z",
147 date.year, date.month, date.day,
148 time.hours, time.minutes, time.seconds);
149 }
150
151 unittest
152 {
153 static immutable STR_1970 = "1970-01-01T00:00:00Z";
154 mstring buf;
155 buf.length = 29;
156 test(format8601(buf, Time.epoch1970) == STR_1970);
157 char[29] static_buf;
158 test(format8601(static_buf, Time.epoch1970) == STR_1970);
159 }
160
161 /******************************************************************************
162
163 Parse provided input and return a UTC epoch time. A return value
164 of Time.max (or false, respectively) indicated a parse-failure.
165
166 An option is provided to return the count of characters parsed -
167 an unchanged value here also indicates invalid input.
168
169 ******************************************************************************/
170
171 Time parse(T) (T[] src, uint* ate = null)
172 {
173 size_t len;
174 Time value;
175
176 if ((len = rfc1123 (src, value)) > 0 ||
177 (len = rfc850 (src, value)) > 0 ||
178 (len = iso8601 (src, value)) > 0 ||
179 (len = dostime (src, value)) > 0 ||
180 (len = asctime (src, value)) > 0)
181 {
182 if (ate)
183 *ate = cast(int) len;
184 return value;
185 }
186 return Time.max;
187 }
188
189
190 /******************************************************************************
191
192 Parse provided input and return a UTC epoch time. A return value
193 of Time.max (or false, respectively) indicated a parse-failure.
194
195 An option is provided to return the count of characters parsed -
196 an unchanged value here also indicates invalid input.
197
198 ******************************************************************************/
199
200 bool parse(T) (T[] src, ref TimeOfDay tod, ref Date date, uint* ate = null)
201 {
202 size_t len;
203
204 if ((len = rfc1123 (src, tod, date)) > 0 ||
205 (len = rfc850 (src, tod, date)) > 0 ||
206 (len = iso8601 (src, tod, date)) > 0 ||
207 (len = dostime (src, tod, date)) > 0 ||
208 (len = asctime (src, tod, date)) > 0)
209 {
210 if (ate)
211 *ate = len;
212 return true;
213 }
214 return false;
215 }
216
217 /******************************************************************************
218
219 RFC 822, updated by RFC 1123 :: "Sun, 06 Nov 1994 08:49:37 GMT"
220
221 Returns the number of elements consumed by the parse; zero if
222 the parse failed
223
224 ******************************************************************************/
225
226 size_t rfc1123(T) (T[] src, ref Time value)
227 {
228 TimeOfDay tod;
229 Date date;
230
231 auto r = rfc1123!(T)(src, tod, date);
232 if (r)
233 value = Gregorian.generic.toTime(date, tod);
234 return r;
235 }
236
237
238 /******************************************************************************
239
240 RFC 822, updated by RFC 1123 :: "Sun, 06 Nov 1994 08:49:37 GMT"
241
242 Returns the number of elements consumed by the parse; zero if
243 the parse failed
244
245 ******************************************************************************/
246
247 size_t rfc1123(T) (T[] src, ref TimeOfDay tod, ref Date date)
248 {
249 T* p = src.ptr;
250 T* e = p + src.length;
251
252 bool dt (ref T* p)
253 {
254 return ((date.day = parseInt(p, e)) > 0 &&
255 *p++ == ' ' &&
256 (date.month = parseMonth(p)) > 0 &&
257 *p++ == ' ' &&
258 (date.year = parseInt(p, e)) > 0);
259 }
260
261 if (parseShortDay(p) >= 0 &&
262 *p++ == ',' &&
263 *p++ == ' ' &&
264 dt (p) &&
265 *p++ == ' ' &&
266 time (tod, p, e) &&
267 *p++ == ' ' &&
268 p[0..3] == "GMT")
269 {
270 return cast(size_t) ((p+3) - src.ptr);
271 }
272 return 0;
273 }
274
275
276 /******************************************************************************
277
278 RFC 850, obsoleted by RFC 1036 :: "Sunday, 06-Nov-94 08:49:37 GMT"
279
280 Returns the number of elements consumed by the parse; zero if
281 the parse failed
282
283 ******************************************************************************/
284
285 size_t rfc850(T) (T[] src, ref Time value)
286 {
287 TimeOfDay tod;
288 Date date;
289
290 auto r = rfc850!(T)(src, tod, date);
291 if (r)
292 value = Gregorian.generic.toTime (date, tod);
293 return r;
294 }
295
296 /******************************************************************************
297
298 RFC 850, obsoleted by RFC 1036 :: "Sunday, 06-Nov-94 08:49:37 GMT"
299
300 Returns the number of elements consumed by the parse; zero if
301 the parse failed
302
303 ******************************************************************************/
304
305 size_t rfc850(T) (T[] src, ref TimeOfDay tod, ref Date date)
306 {
307 T* p = src.ptr;
308 T* e = p + src.length;
309
310 bool dt (ref T* p)
311 {
312 return ((date.day = parseInt(p, e)) > 0 &&
313 *p++ == '-' &&
314 (date.month = parseMonth(p)) > 0 &&
315 *p++ == '-' &&
316 (date.year = parseInt(p, e)) > 0);
317 }
318
319 if (parseFullDay(p) >= 0 &&
320 *p++ == ',' &&
321 *p++ == ' ' &&
322 dt (p) &&
323 *p++ == ' ' &&
324 time (tod, p, e) &&
325 *p++ == ' ' &&
326 p[0..3] == "GMT")
327 {
328 if (date.year < 70)
329 date.year += 2000;
330 else
331 if (date.year < 100)
332 date.year += 1900;
333
334 return cast(size_t) ((p+3) - src.ptr);
335 }
336 return 0;
337 }
338
339
340 /******************************************************************************
341
342 ANSI C's asctime() format :: "Sun Nov 6 08:49:37 1994"
343
344 Returns the number of elements consumed by the parse; zero if
345 the parse failed
346
347 ******************************************************************************/
348
349 size_t asctime(T) (T[] src, ref Time value)
350 {
351 TimeOfDay tod;
352 Date date;
353
354 auto r = asctime!(T)(src, tod, date);
355 if (r)
356 value = Gregorian.generic.toTime (date, tod);
357 return r;
358 }
359
360 /******************************************************************************
361
362 ANSI C's asctime() format :: "Sun Nov 6 08:49:37 1994"
363
364 Returns the number of elements consumed by the parse; zero if
365 the parse failed
366
367 ******************************************************************************/
368
369 size_t asctime(T) (T[] src, ref TimeOfDay tod, ref Date date)
370 {
371 T* p = src.ptr;
372 T* e = p + src.length;
373
374 bool dt (ref T* p)
375 {
376 return ((date.month = parseMonth(p)) > 0 &&
377 *p++ == ' ' &&
378 ((date.day = parseInt(p, e)) > 0
379 || (*p++ == ' ' && (date.day = parseInt(p, e)) > 0)));
380 }
381
382 if (parseShortDay(p) >= 0 &&
383 *p++ == ' ' &&
384 dt (p) &&
385 *p++ == ' ' &&
386 time (tod, p, e) &&
387 *p++ == ' ' &&
388 (date.year = parseInt (p, e)) > 0)
389 {
390 return cast(size_t) (p - src.ptr);
391 }
392 return 0;
393 }
394
395 /******************************************************************************
396
397 DOS time format :: "12-31-06 08:49AM"
398
399 Returns the number of elements consumed by the parse; zero if
400 the parse failed
401
402 ******************************************************************************/
403
404 size_t dostime(T) (T[] src, ref Time value)
405 {
406 TimeOfDay tod;
407 Date date;
408
409 auto r = dostime!(T)(src, tod, date);
410 if (r)
411 value = Gregorian.generic.toTime(date, tod);
412 return r;
413 }
414
415
416 /******************************************************************************
417
418 DOS time format :: "12-31-06 08:49AM"
419
420 Returns the number of elements consumed by the parse; zero if
421 the parse failed
422
423 ******************************************************************************/
424
425 size_t dostime(T) (T[] src, ref TimeOfDay tod, ref Date date)
426 {
427 T* p = src.ptr;
428 T* e = p + src.length;
429
430 bool dt (ref T* p)
431 {
432 return ((date.month = parseInt(p, e)) > 0 &&
433 *p++ == '-' &&
434 ((date.day = parseInt(p, e)) > 0 &&
435 (*p++ == '-' && (date.year = parseInt(p, e)) > 0)));
436 }
437
438 if (dt(p) >= 0 &&
439 *p++ == ' ' &&
440 (tod.hours = parseInt(p, e)) > 0 &&
441 *p++ == ':' &&
442 (tod.minutes = parseInt(p, e)) > 0 &&
443 (*p == 'A' || *p == 'P'))
444 {
445 if (*p is 'P')
446 tod.hours += 12;
447
448 if (date.year < 70)
449 date.year += 2000;
450 else
451 if (date.year < 100)
452 date.year += 1900;
453
454 return cast(size_t) ((p+2) - src.ptr);
455 }
456 return 0;
457 }
458
459 /******************************************************************************
460
461 ISO-8601 format :: "2006-01-31 14:49:30,001"
462
463 Returns the number of elements consumed by the parse; zero if
464 the parse failed
465
466 Quote from http://en.wikipedia.org/wiki/ISO_8601 (2009-09-01):
467 "Decimal fractions may also be added to any of the three time elements.
468 A decimal point, either a comma or a dot (without any preference as
469 stated most recently in resolution 10 of the 22nd General Conference
470 CGPM in 2003), is used as a separator between the time element and
471 its fraction."
472
473 ******************************************************************************/
474
475 size_t iso8601(T) (T[] src, ref Time value)
476 {
477 TimeOfDay tod;
478 Date date;
479
480 size_t r = iso8601!(T)(src, tod, date);
481 if (r)
482 value = Gregorian.generic.toTime(date, tod);
483 return r;
484 }
485
486 /******************************************************************************
487
488 ISO-8601 format :: "2006-01-31 14:49:30,001"
489
490 Returns the number of elements consumed by the parse; zero if
491 the parse failed
492
493 Quote from http://en.wikipedia.org/wiki/ISO_8601 (2009-09-01):
494 "Decimal fractions may also be added to any of the three time elements.
495 A decimal point, either a comma or a dot (without any preference as
496 stated most recently in resolution 10 of the 22nd General Conference
497 CGPM in 2003), is used as a separator between the time element and
498 its fraction."
499
500 ******************************************************************************/
501
502 size_t iso8601(T) (T[] src, ref TimeOfDay tod, ref Date date)
503 {
504 T* p = src.ptr;
505 T* e = p + src.length;
506
507 bool dt (ref T* p)
508 {
509 return ((date.year = parseInt(p, e)) > 0 &&
510 *p++ == '-' &&
511 ((date.month = parseInt(p, e)) > 0 &&
512 (*p++ == '-' &&
513 (date.day = parseInt(p, e)) > 0)));
514 }
515
516 if (dt(p) >= 0 &&
517 *p++ == ' ' &&
518 time (tod, p, e))
519 {
520 // Are there chars left? If yes, parse millis. If no, millis = 0.
521 if (p - src.ptr) {
522 // check fraction separator
523 T frac_sep = *p++;
524 if (frac_sep is ',' || frac_sep is '.')
525 // separator is ok: parse millis
526 tod.millis = parseInt (p, e);
527 else
528 // wrong separator: error
529 return 0;
530 } else
531 tod.millis = 0;
532
533 return cast(size_t) (p - src.ptr);
534 }
535 return 0;
536 }
537
538
539 /******************************************************************************
540
541 Parse a time field
542
543 ******************************************************************************/
544
545 private bool time(T) (ref TimeOfDay time, ref T* p, T* e)
546 {
547 return ((time.hours = parseInt(p, e)) >= 0 &&
548 *p++ == ':' &&
549 (time.minutes = parseInt(p, e)) >= 0 &&
550 *p++ == ':' &&
551 (time.seconds = parseInt(p, e)) >= 0);
552 }
553
554
555 /******************************************************************************
556
557 Match a month from the input
558
559 ******************************************************************************/
560
561 private int parseMonth(T) (ref T* p)
562 {
563 int month;
564
565 switch (p[0..3])
566 {
567 case "Jan":
568 month = 1;
569 break;
570 case "Feb":
571 month = 2;
572 break;
573 case "Mar":
574 month = 3;
575 break;
576 case "Apr":
577 month = 4;
578 break;
579 case "May":
580 month = 5;
581 break;
582 case "Jun":
583 month = 6;
584 break;
585 case "Jul":
586 month = 7;
587 break;
588 case "Aug":
589 month = 8;
590 break;
591 case "Sep":
592 month = 9;
593 break;
594 case "Oct":
595 month = 10;
596 break;
597 case "Nov":
598 month = 11;
599 break;
600 case "Dec":
601 month = 12;
602 break;
603 default:
604 return month;
605 }
606 p += 3;
607 return month;
608 }
609
610
611 /******************************************************************************
612
613 Match a day from the input
614
615 ******************************************************************************/
616
617 private int parseShortDay(T) (ref T* p)
618 {
619 int day;
620
621 switch (p[0..3])
622 {
623 case "Sun":
624 day = 0;
625 break;
626 case "Mon":
627 day = 1;
628 break;
629 case "Tue":
630 day = 2;
631 break;
632 case "Wed":
633 day = 3;
634 break;
635 case "Thu":
636 day = 4;
637 break;
638 case "Fri":
639 day = 5;
640 break;
641 case "Sat":
642 day = 6;
643 break;
644 default:
645 return -1;
646 }
647 p += 3;
648 return day;
649 }
650
651
652 /******************************************************************************
653
654 Match a day from the input. Sunday is 0
655
656 ******************************************************************************/
657
658 private size_t parseFullDay(T) (ref T* p)
659 {
660 static T[][] days = [
661 "Sunday",
662 "Monday",
663 "Tuesday",
664 "Wednesday",
665 "Thursday",
666 "Friday",
667 "Saturday",
668 ];
669
670 foreach (size_t i, day; days)
671 if (day == p[0..day.length])
672 {
673 p += day.length;
674 return i;
675 }
676 return -1;
677 }
678
679
680 /******************************************************************************
681
682 Extract an integer from the input
683
684 ******************************************************************************/
685
686 private static int parseInt(T) (ref T* p, T* e)
687 {
688 int value;
689
690 while (p < e && (*p >= '0' && *p <= '9'))
691 value = value * 10 + *p++ - '0';
692 return value;
693 }
694
695
696 /******************************************************************************
697
698 ******************************************************************************/
699
700 unittest
701 {
702 char[30] tmp;
703 const(char)[] s = "Sun, 06 Nov 1994 08:49:37 GMT";
704
705 auto time = parse (s);
706 auto text = format (tmp, time);
707 test (text == s);
708
709 cstring garbageTest = "Wed Jun 11 17:22:07 20088";
710 garbageTest = garbageTest[0..$-1];
711 char[128] tmp2;
712
713 time = parse(garbageTest);
714 auto text2 = format(tmp2, time);
715 test (text2 == "Wed, 11 Jun 2008 17:22:07 GMT");
716 }