Mercurial > projects > ldc
comparison tango/tango/time/ISO8601.d @ 132:1700239cab2e trunk
[svn r136] MAJOR UNSTABLE UPDATE!!!
Initial commit after moving to Tango instead of Phobos.
Lots of bugfixes...
This build is not suitable for most things.
author | lindquist |
---|---|
date | Fri, 11 Jan 2008 17:57:40 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
131:5825d48b27d1 | 132:1700239cab2e |
---|---|
1 /******************************************************************************* | |
2 | |
3 copyright: Copyright (c) 2007 Deewiant. All rights reserved | |
4 | |
5 license: BSD style: $(LICENSE) | |
6 | |
7 version: Initial release: Aug 2007 | |
8 | |
9 author: Deewiant | |
10 | |
11 Based on the ISO 8601:2004 standard (described in a PDF Wikipedia | |
12 http://isotc.iso.org/livelink/livelink/4021199/ISO_8601_2004_E.zip? | |
13 func=doc.Fetch&nodeid=4021199), which has functions for parsing almost | |
14 every date/time format specified. | |
15 | |
16 The ones they don't parse are intervals, durations, and recurring | |
17 intervals, because I got too lazy to implement them. The functions | |
18 (iso8601Time, iso8601Date, and iso8601) update a Date passed instead | |
19 of a Time, as does the current iso8601, because that's too limited | |
20 a format. One can always convert to a Time if necessary, keeping | |
21 in mind that information loss might occur if the Date is outside the | |
22 interval Time can represent. | |
23 | |
24 In addition, because its dayOfWeek function only works for 1900-3-1 to | |
25 2100-2-28, it would fail by a day or two on ISO week dates outside that | |
26 interval. (Currently it asserts outside 1901-2099.) If somebody knows a | |
27 good algorithm which would fix that, by all means submit it. | |
28 | |
29 Another thing it doesn't do is conversions from local time to UTC if the | |
30 time parsed starts with a 'T'. A comment in doIso8601Time() explains why. | |
31 | |
32 Because the Date struct has no support for time zones, the module just | |
33 converts times with specified time zones into UTC. This leads to | |
34 behaviour which may or may not be a bug, as explained in a comment in | |
35 getTimeZone(). | |
36 | |
37 *******************************************************************************/ | |
38 | |
39 module tango.time.ISO8601; | |
40 | |
41 public import tango.time.Time; | |
42 | |
43 public import tango.time.chrono.Calendar; | |
44 | |
45 private import tango.time.chrono.Gregorian; | |
46 | |
47 /// Returns the number of chars used to compose a valid date: 0 if no date can be composed. | |
48 /// Fields in date will either be correct (e.g. months will be >= 1 and <= 12) or zero. | |
49 | |
50 size_t iso8601Date(T)(T[] src, ref Date date, size_t expanded = 0) { | |
51 ubyte dummy = void; | |
52 T* p = src.ptr; | |
53 return doIso8601Date(p, src, date, expanded, dummy); | |
54 } | |
55 | |
56 private size_t doIso8601Date(T)(ref T* p, T[] src, ref Date date, size_t expanded, out ubyte separators) | |
57 out { | |
58 assert (!date.month || (date.month >= 1 && date.month <= 12)); | |
59 assert (!date.day || (date.month && date.day >= 1 && date.day <= daysPerMonth(date.month, date.year))); | |
60 } body { | |
61 | |
62 // always set era to AD | |
63 date.era = Gregorian.AD_ERA; | |
64 | |
65 size_t eaten() { return p - src.ptr; } | |
66 bool done(T[] s) { return .done(eaten(), src.length, *p, s); } | |
67 | |
68 if (!parseYear(p, expanded, date.year)) | |
69 return (date.year = 0); | |
70 | |
71 auto onlyYear = eaten(); | |
72 | |
73 // /([+-]Y{expanded})?(YYYY|YY)/ | |
74 if (done("-0123W")) | |
75 return onlyYear; | |
76 | |
77 if (accept(p, '-')) | |
78 separators = true; | |
79 | |
80 if (accept(p, 'W')) { | |
81 // (year)-Www-D | |
82 | |
83 T* p2 = p; | |
84 | |
85 int i = parseIntMax(p, 3u); | |
86 | |
87 if (i) if (p - p2 == 2) { | |
88 | |
89 // (year)-Www | |
90 if (done("-")) { | |
91 if (getMonthAndDayFromWeek(date, i)) | |
92 return eaten(); | |
93 | |
94 // (year)-Www-D | |
95 } else if (demand(p, '-')) | |
96 if (getMonthAndDayFromWeek(date, i, *p++ - '0')) | |
97 return eaten(); | |
98 | |
99 } else if (p - p2 == 3) | |
100 // (year)WwwD | |
101 if (getMonthAndDayFromWeek(date, i / 10, i % 10)) | |
102 return eaten(); | |
103 | |
104 return onlyYear; | |
105 } | |
106 | |
107 // next up, MM or MM[-]DD or DDD | |
108 | |
109 T* p2 = p; | |
110 | |
111 int i = parseIntMax(p); | |
112 if (!i) | |
113 return onlyYear; | |
114 | |
115 switch (p - p2) { | |
116 case 2: | |
117 date.month = i; | |
118 | |
119 if (!(date.month >= 1 && date.month <= 12)) { | |
120 date.month = 0; | |
121 return onlyYear; | |
122 } | |
123 | |
124 auto onlyMonth = eaten(); | |
125 | |
126 // (year)-MM | |
127 if (done("-")) | |
128 return onlyMonth; | |
129 | |
130 // (year)-MM-DD | |
131 if (!( | |
132 demand(p, '-') && | |
133 (date.day = parseIntMax(p, 2u)) != 0 && date.day <= daysPerMonth(date.month, date.year) | |
134 )) { | |
135 date.day = 0; | |
136 return onlyMonth; | |
137 } | |
138 | |
139 break; | |
140 | |
141 case 4: | |
142 // e.g. 20010203, i = 203 now | |
143 | |
144 date.month = i / 100; | |
145 date.day = i % 100; | |
146 | |
147 // (year)MMDD | |
148 if (!( | |
149 date.month >= 1 && date.month <= 12 && | |
150 date.day >= 0 && date.day <= daysPerMonth(date.month, date.year) | |
151 )) { | |
152 date.month = date.day = 0; | |
153 return onlyYear; | |
154 } | |
155 | |
156 break; | |
157 | |
158 case 3: | |
159 // (year)-DDD | |
160 // i is the ordinal of the day within the year | |
161 | |
162 bool leap = isLeapYear(date.year); | |
163 | |
164 if (i > 365 + leap) | |
165 return onlyYear; | |
166 | |
167 if (i <= 31) { | |
168 date.month = 1; | |
169 date.day = i; | |
170 | |
171 } else if (i <= 59 + leap) { | |
172 date.month = 2; | |
173 date.day = i - 31 - leap; | |
174 | |
175 } else if (i <= 90 + leap) { | |
176 date.month = 3; | |
177 date.day = i - 59 - leap; | |
178 | |
179 } else if (i <= 120 + leap) { | |
180 date.month = 4; | |
181 date.day = i - 90 - leap; | |
182 | |
183 } else if (i <= 151 + leap) { | |
184 date.month = 5; | |
185 date.day = i - 120 - leap; | |
186 | |
187 } else if (i <= 181 + leap) { | |
188 date.month = 6; | |
189 date.day = i - 151 - leap; | |
190 | |
191 } else if (i <= 212 + leap) { | |
192 date.month = 7; | |
193 date.day = i - 181 - leap; | |
194 | |
195 } else if (i <= 243 + leap) { | |
196 date.month = 8; | |
197 date.day = i - 212 - leap; | |
198 | |
199 } else if (i <= 273 + leap) { | |
200 date.month = 9; | |
201 date.day = i - 243 - leap; | |
202 | |
203 } else if (i <= 304 + leap) { | |
204 date.month = 10; | |
205 date.day = i - 273 - leap; | |
206 | |
207 } else if (i <= 334 + leap) { | |
208 date.month = 11; | |
209 date.day = i - 304 - leap; | |
210 | |
211 } else { | |
212 if (i > 365 + leap) | |
213 assert (false); | |
214 | |
215 date.month = 12; | |
216 date.day = i - 334 - leap; | |
217 } | |
218 | |
219 default: break; | |
220 } | |
221 | |
222 return eaten(); | |
223 } | |
224 | |
225 /// Returns the number of chars used to compose a valid date: 0 if no date can be composed. | |
226 /// Fields in date will be zero if incorrect: since 00:00:00,000 is a valid time, the return value must be checked to be sure of the result. | |
227 /// time.seconds may be 60 if the hours and minutes are 23 and 59, as leap seconds are occasionally added to UTC time. | |
228 /// time.hours may be 0 or 24: the latter marks the end of a day, the former the beginning. | |
229 | |
230 size_t iso8601Time(T)(T[] src, ref Date date, ref TimeOfDay time) { | |
231 bool dummy = void; | |
232 T* p = src.ptr; | |
233 return doIso8601Time(p, src, date, time, WHATEVER, dummy); | |
234 } | |
235 | |
236 private enum : ubyte { NO = 0, YES = 1, WHATEVER } | |
237 | |
238 // bothValid is used only to get iso8601() to catch errors correctly | |
239 private size_t doIso8601Time(T)(ref T* p, T[] src, ref Date date, ref TimeOfDay time, ubyte separators, out bool bothValid) | |
240 out { | |
241 // yes, I could just write >= 0, but this emphasizes the difference between == 0 and != 0 | |
242 assert (!time.hours || (time.hours > 0 && time.hours <= 24)); | |
243 assert (!time.minutes || (time.minutes > 0 && time.minutes <= 59)); | |
244 assert (!time.seconds || (time.seconds > 0 && time.seconds <= 60)); | |
245 assert (!time.millis || (time.millis > 0 && time.millis <= 999)); | |
246 } body { | |
247 size_t eaten() { return p - src.ptr; } | |
248 bool done(T[] s) { return .done(eaten(), src.length, *p, s); } | |
249 | |
250 bool checkColon() { | |
251 if (separators == WHATEVER) | |
252 accept(p, ':'); | |
253 | |
254 else if (accept(p, ':') != separators) | |
255 return false; | |
256 | |
257 return true; | |
258 } | |
259 | |
260 byte getTimeZone() { return .getTimeZone(p, date, time, separators, &done); } | |
261 | |
262 // TODO/BUG: need to convert from local time if got T | |
263 // however, Tango provides nothing like Phobos's std.date.getLocalTZA | |
264 // (which doesn't look like it should work on Windows, it should use tzi.bias only, and GetTimeZoneInformationForYear) | |
265 // (and which uses too complicated code for Posix, tzset should be enough) | |
266 // and I'm not interested in delving into system-specific code right now | |
267 // remember also that -1 BC is the year zero in ISO 8601... -2 BC is -1, etc | |
268 if (separators == WHATEVER) | |
269 accept(p, 'T'); | |
270 | |
271 if (parseInt(p, 2u, time.hours) != 2 || time.hours > 24) | |
272 return (time.hours = 0); | |
273 | |
274 auto onlyHour = eaten(); | |
275 | |
276 // hh | |
277 if (done("+,-.012345:")) | |
278 return onlyHour; | |
279 | |
280 switch (getDecimal(p, time, HOUR)) { | |
281 case NOTFOUND: break; | |
282 case FOUND: | |
283 auto onlyDecimal = eaten(); | |
284 if (getTimeZone() == BAD) | |
285 return onlyDecimal; | |
286 | |
287 // /hh,h+/ | |
288 return eaten(); | |
289 | |
290 case BAD: return onlyHour; | |
291 default: assert (false); | |
292 } | |
293 | |
294 switch (getTimeZone()) { | |
295 case NOTFOUND: break; | |
296 case FOUND: return eaten(); | |
297 case BAD: return onlyHour; | |
298 default: assert (false); | |
299 } | |
300 | |
301 if ( | |
302 !checkColon() || | |
303 | |
304 parseInt(p, 2u, time.minutes) != 2 || time.minutes > 59 || | |
305 | |
306 // hour 24 is only for 24:00:00 | |
307 (time.hours == 24 && time.minutes != 0) | |
308 ) { | |
309 time.minutes = 0; | |
310 return onlyHour; | |
311 } | |
312 | |
313 auto onlyMinute = eaten(); | |
314 | |
315 // hh:mm | |
316 if (done("+,-.0123456:")) { | |
317 bothValid = true; | |
318 return onlyMinute; | |
319 } | |
320 | |
321 switch (getDecimal(p, time, MINUTE)) { | |
322 case NOTFOUND: break; | |
323 case FOUND: | |
324 auto onlyDecimal = eaten(); | |
325 if (getTimeZone() == BAD) | |
326 return onlyDecimal; | |
327 | |
328 // /hh:mm,m+/ | |
329 bothValid = true; | |
330 return eaten(); | |
331 | |
332 case BAD: return onlyMinute; | |
333 default: assert (false); | |
334 } | |
335 | |
336 switch (getTimeZone()) { | |
337 case NOTFOUND: break; | |
338 case FOUND: bothValid = true; return eaten(); | |
339 case BAD: return onlyMinute; | |
340 default: assert (false); | |
341 } | |
342 | |
343 if ( | |
344 !checkColon() || | |
345 parseInt(p, 2u, time.seconds) != 2 || time.seconds > 60 || | |
346 (time.hours == 24 && time.seconds != 0) || | |
347 (time.seconds == 60 && time.hours != 23 && time.minutes != 59) | |
348 ) { | |
349 time.seconds = 0; | |
350 return onlyMinute; | |
351 } | |
352 | |
353 auto onlySecond = eaten(); | |
354 | |
355 // hh:mm:ss | |
356 if (done("+,-.Z")) { | |
357 bothValid = true; | |
358 return onlySecond; | |
359 } | |
360 | |
361 switch (getDecimal(p, time, SECOND)) { | |
362 case NOTFOUND: break; | |
363 case FOUND: | |
364 auto onlyDecimal = eaten(); | |
365 if (getTimeZone() == BAD) | |
366 return onlyDecimal; | |
367 | |
368 // /hh:mm:ss,s+/ | |
369 bothValid = true; | |
370 return eaten(); | |
371 | |
372 case BAD: return onlySecond; | |
373 default: assert (false); | |
374 } | |
375 | |
376 if (getTimeZone() == BAD) | |
377 return onlySecond; | |
378 else { | |
379 bothValid = true; | |
380 return eaten(); // hh:mm:ss with timezone | |
381 } | |
382 } | |
383 | |
384 // combination of date and time | |
385 // stricter than just date followed by time: | |
386 // can't have an expanded or reduced date | |
387 // either use separators everywhere or not at all | |
388 | |
389 /// This function is very strict: either a complete date and time can be extracted, or nothing can. | |
390 /// If this function returns zero, the fields of date are undefined. | |
391 | |
392 size_t iso8601(T)(T[] src, ref Date date, ref TimeOfDay time) { | |
393 T* p = src.ptr; | |
394 ubyte sep; | |
395 bool bothValid = false; | |
396 | |
397 if ( | |
398 doIso8601Date(p, src, date, 0u, sep) && | |
399 date.year && date.month && date.day && | |
400 | |
401 // by mutual agreement this T may be omitted | |
402 // but this is just a convenience method for date+time anyway | |
403 demand(p, 'T') && | |
404 | |
405 doIso8601Time(p, src, date, time, sep, bothValid) && | |
406 bothValid | |
407 ) | |
408 return p - src.ptr; | |
409 else | |
410 return 0; | |
411 } | |
412 | |
413 /+ +++++++++++++++++++++++++++++++++++++++ +\ | |
414 | |
415 Privates used by date | |
416 | |
417 \+ +++++++++++++++++++++++++++++++++++++++ +/ | |
418 | |
419 // /([+-]Y{expanded})?(YYYY|YY)/ | |
420 private bool parseYear(T)(ref T* p, size_t expanded, out uint year) { | |
421 | |
422 bool doParse() { | |
423 T* p2 = p; | |
424 | |
425 if (!parseInt(p, expanded + 4u, year)) | |
426 return false; | |
427 | |
428 // it's Y{expanded}YY, Y{expanded}YYYY, or unacceptable | |
429 if (p - p2 - expanded == 2u) | |
430 year *= 100; | |
431 else if (p - p2 - expanded != 4u) | |
432 return false; | |
433 | |
434 return true; | |
435 } | |
436 | |
437 if (accept(p, '-')) { | |
438 if (!doParse()) | |
439 return false; | |
440 year = -year; | |
441 } else { | |
442 accept(p, '+'); | |
443 if (!doParse()) | |
444 return false; | |
445 } | |
446 | |
447 return true; | |
448 } | |
449 | |
450 // find the month and day based on the calendar week | |
451 // uses date.year for leap year calculations | |
452 // returns false if week and date.year are incompatible | |
453 // based on the VBA function at http://www.probabilityof.com/ISO8601.shtml | |
454 private bool getMonthAndDayFromWeek(ref Date date, int week, int day = 1) { | |
455 if (week < 1 || week > 53 || day < 1 || day > 7) | |
456 return false; | |
457 | |
458 bool leap = isLeapYear(date.year); | |
459 | |
460 // only years starting with Thursday and | |
461 // leap years starting with Wednesday have 53 weeks | |
462 | |
463 if (week == 53) { | |
464 int startingDay = dayOfWeek(date.year, 1, 1, leap); | |
465 | |
466 if (!(startingDay == 4 || (leap && startingDay == 3))) | |
467 return false; | |
468 } | |
469 | |
470 // days since year-01-04 | |
471 int delta = 7*(week - 1) - dayOfWeek(date.year, 1, 4, leap) + day; | |
472 | |
473 if (delta <= -4) { | |
474 if (delta < -7) | |
475 assert (false); | |
476 | |
477 --date.year; | |
478 date.month = 12; | |
479 date.day = delta + 4 + 31; | |
480 | |
481 } else if (delta <= 27) { | |
482 date.month = 1; | |
483 date.day = delta + 4; | |
484 | |
485 } else if (delta <= 56 + leap) { | |
486 date.month = 2; | |
487 date.day = delta - 27; | |
488 | |
489 } else if (delta <= 87 + leap) { | |
490 date.month = 3; | |
491 date.day = delta - 55 - leap; | |
492 | |
493 } else if (delta <= 117 + leap) { | |
494 date.month = 4; | |
495 date.day = delta - 86 - leap; | |
496 | |
497 } else if (delta <= 148 + leap) { | |
498 date.month = 5; | |
499 date.day = delta - 116 - leap; | |
500 | |
501 } else if (delta <= 178 + leap) { | |
502 date.month = 6; | |
503 date.day = delta - 147 - leap; | |
504 | |
505 } else if (delta <= 209 + leap) { | |
506 date.month = 7; | |
507 date.day = delta - 177 - leap; | |
508 | |
509 } else if (delta <= 240 + leap) { | |
510 date.month = 8; | |
511 date.day = delta - 208 - leap; | |
512 | |
513 } else if (delta <= 270 + leap) { | |
514 date.month = 9; | |
515 date.day = delta - 239 - leap; | |
516 | |
517 } else if (delta <= 301 + leap) { | |
518 date.month = 10; | |
519 date.day = delta - 269 - leap; | |
520 | |
521 } else if (delta <= 331 + leap) { | |
522 date.month = 11; | |
523 date.day = delta - 300 - leap; | |
524 | |
525 } else if (delta <= 361 + leap) { | |
526 date.month = 12; | |
527 date.day = delta - 330 - leap; | |
528 | |
529 } else { | |
530 if (delta > 365 + leap) | |
531 assert (false); | |
532 | |
533 ++date.year; | |
534 date.month = 1; | |
535 date.day = delta - 365 - leap + 4; | |
536 } | |
537 | |
538 return true; | |
539 } | |
540 | |
541 private bool isLeapYear(int year) { | |
542 return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); | |
543 } | |
544 | |
545 // Babwani's Congruence | |
546 private int dayOfWeek(int year, int month, int day, bool leap) | |
547 in { | |
548 assert (month >= 1 && month <= 12); | |
549 assert (day >= 1 && day <= 31); | |
550 | |
551 // BUG: only works for 1900-3-1 to 2100-2-28 | |
552 assert (year >= 1901 && year <= 2099, "iso8601 :: Can't calculate day of week outside the years 1900-2099"); | |
553 | |
554 } out(result) { | |
555 assert (result >= 1 && result <= 7); | |
556 | |
557 } body { | |
558 int f() { | |
559 if (leap && month <= 2) | |
560 return [6,2][month-1]; | |
561 | |
562 return [0,3,3,6,1,4,6,2,5,0,3,5][month-1]; | |
563 } | |
564 | |
565 int d = ((5*(year % 100) / 4) - 2*((year / 100) % 4) + f() + day) % 7; | |
566 | |
567 // defaults to Saturday=0, Friday=6: convert to Monday=1, Sunday=7 | |
568 return (d <= 1 ? d+6 : d-1); | |
569 } | |
570 | |
571 /+ +++++++++++++++++++++++++++++++++++++++ +\ | |
572 | |
573 Privates used by time | |
574 | |
575 \+ +++++++++++++++++++++++++++++++++++++++ +/ | |
576 | |
577 private enum : ubyte { HOUR, MINUTE, SECOND } | |
578 private enum : byte { BAD, FOUND, NOTFOUND } | |
579 | |
580 private byte getDecimal(T)(ref T* p, ref TimeOfDay time, ubyte which) { | |
581 if (accept(p, ',') || accept(p, '.')) { | |
582 | |
583 T* p2 = p; | |
584 | |
585 int i; | |
586 size_t iLen = parseInt(p, i); | |
587 | |
588 if ( | |
589 iLen == 0 || | |
590 | |
591 // if i is 0, must have at least 3 digits | |
592 // ... or at least that's what I think the standard means | |
593 // when it says "[i]f the magnitude of the number is less | |
594 // than unity, the decimal sign shall be preceded by two | |
595 // zeros"... | |
596 // surely that should read "followed" and not "preceded" | |
597 | |
598 (i == 0 && iLen < 3) | |
599 ) | |
600 return BAD; | |
601 | |
602 // 10 to the power of (iLen - 1) | |
603 int pow = 1; | |
604 while (--iLen) | |
605 pow *= 10; | |
606 | |
607 switch (which) { | |
608 case HOUR: | |
609 time.minutes = 6 * i / pow; | |
610 time.seconds = 6 * i % pow; | |
611 break; | |
612 case MINUTE: | |
613 time.seconds = 6 * i / pow; | |
614 time.millis = 6000 * i / pow % 1000; | |
615 break; | |
616 case SECOND: | |
617 time.millis = 100 * i / pow; | |
618 break; | |
619 | |
620 default: assert (false); | |
621 } | |
622 | |
623 return FOUND; | |
624 } | |
625 | |
626 return NOTFOUND; | |
627 } | |
628 | |
629 // the Date is always UTC, so this just adds the offset to the date fields | |
630 // another option would be to add time zone fields to Date and have this fill them | |
631 | |
632 private byte getTimeZone(T)(ref T* p, ref Date date, ref TimeOfDay time, ubyte separators, bool delegate(T[]) done) { | |
633 if (accept(p, 'Z')) | |
634 return FOUND; | |
635 | |
636 int factor = -1; | |
637 | |
638 if (accept(p, '-')) | |
639 factor = 1; | |
640 | |
641 else if (!accept(p, '+')) | |
642 return NOTFOUND; | |
643 | |
644 int hour, realhour = time.hours, realminute = time.minutes; | |
645 scope(exit) time.hours = cast(uint)realhour; | |
646 scope(exit) time.minutes = cast(uint)realminute; | |
647 if (parseInt(p, 2u, hour) != 2 || hour > 12 || (hour == 0 && factor == 1)) | |
648 return BAD; | |
649 | |
650 realhour += factor * hour; | |
651 | |
652 void hourCheck() { | |
653 if (realhour > 24 || (realhour == 24 && (realminute || time.seconds))) { | |
654 realhour -= 24; | |
655 | |
656 // BUG? what should be done? | |
657 // if we get a time like 20:00-05:00 | |
658 // which needs to be converted to UTC by adding 05:00 to 20:00 | |
659 // we just set the time to 01:00 and the day to 1 | |
660 // even though this is time, which really has nothing to do with the day, which is part of the date | |
661 // if this isn't a bug, it needs to be documented: it's not necessarily obvious | |
662 if (date.day++ && date.day > daysPerMonth(date.month, date.year)) { | |
663 date.day = 1; | |
664 if (++date.month > 12) { | |
665 date.month = 1; | |
666 ++date.year; | |
667 } | |
668 } | |
669 } else if (realhour < 0) { | |
670 realhour += 24; | |
671 | |
672 // ditto above BUG? | |
673 if (date.day-- && date.day < 1) { | |
674 if (--date.month < 1) { | |
675 date.month = 12; | |
676 --date.year; | |
677 } | |
678 | |
679 date.day = daysPerMonth(date.month, date.year); | |
680 } | |
681 } | |
682 } | |
683 | |
684 if (done("012345:")) { | |
685 hourCheck(); | |
686 return FOUND; | |
687 } | |
688 | |
689 if (separators == WHATEVER) | |
690 accept(p, ':'); | |
691 | |
692 else if (accept(p, ':') != separators) | |
693 return BAD; | |
694 | |
695 int minute; | |
696 if (parseInt(p, 2u, minute) != 2) | |
697 return BAD; | |
698 | |
699 assert (minute <= 59); | |
700 | |
701 realminute += factor * minute; | |
702 | |
703 if (realminute > 59) { | |
704 realminute -= 60; | |
705 ++realhour; | |
706 | |
707 } else if (realminute < 0) { | |
708 realminute += 60; | |
709 --realhour; | |
710 } | |
711 | |
712 hourCheck(); | |
713 return FOUND; | |
714 } | |
715 | |
716 /+ +++++++++++++++++++++++++++++++++++++++ +\ | |
717 | |
718 Privates used by both date and time | |
719 | |
720 \+ +++++++++++++++++++++++++++++++++++++++ +/ | |
721 | |
722 private bool accept(T)(ref T* p, char c) { | |
723 if (*p == c) { | |
724 ++p; | |
725 return true; | |
726 } | |
727 return false; | |
728 } | |
729 | |
730 private bool demand(T)(ref T* p, char c) { | |
731 return (*p++ == c); | |
732 } | |
733 | |
734 private bool done(T)(size_t eaten, size_t srcLen, T p, T[] s) { | |
735 if (eaten == srcLen) | |
736 return true; | |
737 | |
738 // s is the array of characters which may come next | |
739 // (i.e. which p may be) | |
740 // sorted in ascending order | |
741 foreach (c; s) { | |
742 if (p < c) | |
743 return true; | |
744 else if (p == c) | |
745 break; | |
746 } | |
747 | |
748 return false; | |
749 } | |
750 | |
751 private int daysPerMonth(int month, int year) { | |
752 if (month == 2 && isLeapYear(year)) | |
753 return 29; | |
754 else | |
755 return [31,28,31,30,31,30,31,31,30,31,30,31][month-1]; | |
756 } | |
757 | |
758 /****************************************************************************** | |
759 | |
760 Extract an integer from the input | |
761 | |
762 ******************************************************************************/ | |
763 | |
764 // note: ISO 8601 code relies on these values always being positive, failing if *p == '-' | |
765 | |
766 private uint parseIntMax(T) (ref T* p) { | |
767 uint value = 0; | |
768 while (*p >= '0' && *p <= '9') | |
769 value = value * 10 + *p++ - '0'; | |
770 return value; | |
771 } | |
772 | |
773 // ... but accept no more than max digits | |
774 | |
775 private uint parseIntMax(T)(ref T* p, uint max) { | |
776 size_t i = 0; | |
777 uint value = 0; | |
778 while (p[i] >= '0' && p[i] <= '9' && i < max) | |
779 value = value * 10 + p[i++] - '0'; | |
780 p += i; | |
781 return value; | |
782 } | |
783 | |
784 // ... and return the amount of digits processed | |
785 | |
786 private size_t parseInt(T, U)(ref T* p, out U i) { | |
787 T* p2 = p; | |
788 i = cast(U)parseIntMax(p); | |
789 return p - p2; | |
790 } | |
791 | |
792 private size_t parseInt(T, U)(ref T* p, uint max, out U i) { | |
793 T* p2 = p; | |
794 i = cast(U)parseIntMax(p, max); | |
795 return p - p2; | |
796 } | |
797 | |
798 //////////////////// | |
799 | |
800 debug (UnitTest) { | |
801 import tango.io.Stdout; | |
802 | |
803 debug(ISO8601) | |
804 { | |
805 void main() { } | |
806 } | |
807 | |
808 unittest { | |
809 Date date; | |
810 TimeOfDay time; | |
811 | |
812 // date | |
813 | |
814 size_t d(char[] s, size_t e = 0) { | |
815 date = date.init; | |
816 return iso8601Date(s, date, e); | |
817 } | |
818 | |
819 assert (d("20abc") == 2); | |
820 assert (date.year == 2000); | |
821 | |
822 assert (d("2004") == 4); | |
823 assert (date.year == 2004); | |
824 | |
825 assert (d("+0019", 2) == 5); | |
826 assert (date.year == 1900); | |
827 | |
828 assert (d("+111985", 2) == 7); | |
829 assert (date.year == 111985); | |
830 | |
831 assert (d("+111985", 1) == 6); | |
832 assert (date.year == 11198); | |
833 | |
834 assert (d("+111985", 3) == 0); | |
835 assert (!date.year); | |
836 | |
837 assert (d("+111985", 4) == 7); | |
838 assert (date.year == 11198500); | |
839 | |
840 assert (d("-111985", 5) == 0); | |
841 assert (!date.year); | |
842 | |
843 assert (d("abc") == 0); | |
844 assert (!date.year); | |
845 | |
846 assert (d("abc123") == 0); | |
847 assert (!date.year); | |
848 | |
849 assert (d("2007-08") == 7); | |
850 assert (date.year == 2007); | |
851 assert (date.month == 8); | |
852 | |
853 assert (d("+001985-04", 2) == 10); | |
854 assert (date.year == 1985); | |
855 assert (date.month == 4); | |
856 | |
857 assert (d("2007-08-07") == 10); | |
858 assert (date.year == 2007); | |
859 assert (date.month == 8); | |
860 assert (date.day == 7); | |
861 | |
862 assert (d("2008-20-30") == 4); | |
863 assert (date.year == 2008); | |
864 assert (!date.month); | |
865 | |
866 assert (d("2007-02-30") == 7); | |
867 assert (date.year == 2007); | |
868 assert (date.month == 2); | |
869 | |
870 assert (d("20060708") == 8); | |
871 assert (date.year == 2006); | |
872 assert (date.month == 7); | |
873 assert (date.day == 8); | |
874 | |
875 assert (d("19953080") == 4); | |
876 assert (date.year == 1995); | |
877 assert (!date.month); | |
878 | |
879 assert (d("+001985-04-12", 2) == 13); | |
880 assert (date.year == 1985); | |
881 assert (date.month == 4); | |
882 assert (date.day == 12); | |
883 | |
884 assert (d("-0123450607", 2) == 11); | |
885 assert (date.year == -12345); | |
886 assert (date.month == 6); | |
887 assert (date.day == 7); | |
888 | |
889 assert (d("1985W15") == 7); | |
890 assert (date.year == 1985); | |
891 assert (date.month == 4); | |
892 assert (date.day == 8); | |
893 | |
894 assert (d("2008-W01") == 8); | |
895 assert (date.year == 2007); | |
896 assert (date.month == 12); | |
897 assert (date.day == 31); | |
898 | |
899 assert (d("2008-W01-2") == 10); | |
900 assert (date.year == 2008); | |
901 assert (date.month == 1); | |
902 assert (date.day == 1); | |
903 | |
904 assert (d("2009-W53-4") == 10); | |
905 assert (date.year == 2009); | |
906 assert (date.month == 12); | |
907 assert (date.day == 31); | |
908 | |
909 assert (d("2009-W01-1") == 10); | |
910 assert (date.year == 2008); | |
911 assert (date.month == 12); | |
912 assert (date.day == 29); | |
913 | |
914 assert (d("2009W537") == 8); | |
915 assert (date.year == 2010); | |
916 assert (date.month == 1); | |
917 assert (date.day == 3); | |
918 | |
919 assert (d("2010W537") == 4); | |
920 assert (date.year == 2010); | |
921 assert (!date.month); | |
922 | |
923 assert (d("2009-W01-3") == 10); | |
924 assert (date.year == 2008); | |
925 assert (date.month == 12); | |
926 assert (date.day == 31); | |
927 | |
928 assert (d("2009-W01-4") == 10); | |
929 assert (date.year == 2009); | |
930 assert (date.month == 1); | |
931 assert (date.day == 1); | |
932 | |
933 /+ BUG: these don't work due to dayOfWeek being crap | |
934 | |
935 assert (d("1000-W07-7") == 10); | |
936 assert (date.year == 1000); | |
937 assert (date.month == 2); | |
938 assert (date.day == 16); | |
939 | |
940 assert (d("1500-W11-1") == 10); | |
941 assert (date.year == 1500); | |
942 assert (date.month == 3); | |
943 assert (date.day == 12); | |
944 | |
945 assert (d("1700-W14-2") == 10); | |
946 assert (date.year == 1700); | |
947 assert (date.month == 4); | |
948 assert (date.day == 6); | |
949 | |
950 assert (d("1800-W19-3") == 10); | |
951 assert (date.year == 1800); | |
952 assert (date.month == 5); | |
953 assert (date.day == 7); | |
954 | |
955 assert (d("1900-W25-4") == 10); | |
956 assert (date.year == 1900); | |
957 assert (date.month == 6); | |
958 assert (date.day == 21); | |
959 | |
960 assert (d("0900-W27-5") == 10); | |
961 assert (date.year == 900); | |
962 assert (date.month == 7); | |
963 assert (date.day == 9); | |
964 | |
965 assert (d("0800-W33-6") == 10); | |
966 assert (date.year == 800); | |
967 assert (date.month == 8); | |
968 assert (date.day == 19); | |
969 | |
970 assert (d("0700-W37-7") == 10); | |
971 assert (date.year == 700); | |
972 assert (date.month == 9); | |
973 assert (date.day == 16); | |
974 | |
975 assert (d("0600-W41-4") == 10); | |
976 assert (date.year == 600); | |
977 assert (date.month == 10); | |
978 assert (date.day == 9); | |
979 | |
980 assert (d("0500-W45-7") == 10); | |
981 assert (date.year == 500); | |
982 assert (date.month == 11); | |
983 assert (date.day == 14);+/ | |
984 | |
985 assert (d("2000-W55") == 4); | |
986 assert (date.year == 2000); | |
987 | |
988 assert (d("1980-002") == 8); | |
989 assert (date.year == 1980); | |
990 assert (date.month == 1); | |
991 assert (date.day == 2); | |
992 | |
993 assert (d("1981-034") == 8); | |
994 assert (date.year == 1981); | |
995 assert (date.month == 2); | |
996 assert (date.day == 3); | |
997 | |
998 assert (d("1982-063") == 8); | |
999 assert (date.year == 1982); | |
1000 assert (date.month == 3); | |
1001 assert (date.day == 4); | |
1002 | |
1003 assert (d("1983-095") == 8); | |
1004 assert (date.year == 1983); | |
1005 assert (date.month == 4); | |
1006 assert (date.day == 5); | |
1007 | |
1008 assert (d("1984-127") == 8); | |
1009 assert (date.year == 1984); | |
1010 assert (date.month == 5); | |
1011 assert (date.day == 6); | |
1012 | |
1013 assert (d("1985-158") == 8); | |
1014 assert (date.year == 1985); | |
1015 assert (date.month == 6); | |
1016 assert (date.day == 7); | |
1017 | |
1018 assert (d("1986-189") == 8); | |
1019 assert (date.year == 1986); | |
1020 assert (date.month == 7); | |
1021 assert (date.day == 8); | |
1022 | |
1023 assert (d("1987-221") == 8); | |
1024 assert (date.year == 1987); | |
1025 assert (date.month == 8); | |
1026 assert (date.day == 9); | |
1027 | |
1028 assert (d("1988-254") == 8); | |
1029 assert (date.year == 1988); | |
1030 assert (date.month == 9); | |
1031 assert (date.day == 10); | |
1032 | |
1033 assert (d("1989-284") == 8); | |
1034 assert (date.year == 1989); | |
1035 assert (date.month == 10); | |
1036 assert (date.day == 11); | |
1037 | |
1038 assert (d("1990316") == 7); | |
1039 assert (date.year == 1990); | |
1040 assert (date.month == 11); | |
1041 assert (date.day == 12); | |
1042 | |
1043 assert (d("1991-347") == 8); | |
1044 assert (date.year == 1991); | |
1045 assert (date.month == 12); | |
1046 assert (date.day == 13); | |
1047 | |
1048 assert (d("1992-000") == 4); | |
1049 assert (date.year == 1992); | |
1050 | |
1051 assert (d("1993-370") == 4); | |
1052 assert (date.year == 1993); | |
1053 | |
1054 // time | |
1055 | |
1056 size_t t(char[] s) { | |
1057 time = time.init; | |
1058 date = date.init; | |
1059 | |
1060 return iso8601Time(s, date, time); | |
1061 } | |
1062 | |
1063 assert (t("20") == 2); | |
1064 assert (time.hours == 20); | |
1065 assert (time.minutes == 0); | |
1066 assert (time.seconds == 0); | |
1067 | |
1068 assert (t("30") == 0); | |
1069 | |
1070 assert (t("2004") == 4); | |
1071 assert (time.hours == 20); | |
1072 assert (time.minutes == 4); | |
1073 assert (time.seconds == 0); | |
1074 | |
1075 assert (t("200406") == 6); | |
1076 assert (time.hours == 20); | |
1077 assert (time.minutes == 4); | |
1078 assert (time.seconds == 6); | |
1079 | |
1080 assert (t("24:00") == 5); | |
1081 assert (time.hours == 24); // should compare equal with 0... can't just set to 0, loss of information | |
1082 assert (time.minutes == 0); | |
1083 assert (time.seconds == 0); | |
1084 | |
1085 assert (t("00:00") == 5); | |
1086 assert (time.hours == 0); | |
1087 assert (time.minutes == 0); | |
1088 assert (time.seconds == 0); | |
1089 | |
1090 assert (t("23:59:60") == 8); | |
1091 assert (time.hours == 23); | |
1092 assert (time.minutes == 59); | |
1093 assert (time.seconds == 60); // leap second | |
1094 | |
1095 assert (t("16:49:30,001") == 12); | |
1096 assert (time.hours == 16); | |
1097 assert (time.minutes == 49); | |
1098 assert (time.seconds == 30); | |
1099 assert (time.millis == 1); | |
1100 | |
1101 assert (t("15:48:29,1") == 10); | |
1102 assert (time.hours == 15); | |
1103 assert (time.minutes == 48); | |
1104 assert (time.seconds == 29); | |
1105 assert (time.millis == 100); | |
1106 | |
1107 assert (t("02:10:34,a") == 8); | |
1108 assert (time.hours == 2); | |
1109 assert (time.minutes == 10); | |
1110 assert (time.seconds == 34); | |
1111 | |
1112 assert (t("14:50,5") == 7); | |
1113 assert (time.hours == 14); | |
1114 assert (time.minutes == 50); | |
1115 assert (time.seconds == 30); | |
1116 | |
1117 assert (t("1540,4") == 6); | |
1118 assert (time.hours == 15); | |
1119 assert (time.minutes == 40); | |
1120 assert (time.seconds == 24); | |
1121 | |
1122 assert (t("1250,") == 4); | |
1123 assert (time.hours == 12); | |
1124 assert (time.minutes == 50); | |
1125 | |
1126 assert (t("14,5") == 4); | |
1127 assert (time.hours == 14); | |
1128 assert (time.minutes == 30); | |
1129 | |
1130 assert (t("12,") == 2); | |
1131 assert (time.hours == 12); | |
1132 assert (time.minutes == 0); | |
1133 | |
1134 assert (t("24:00:01") == 5); | |
1135 assert (time.hours == 24); | |
1136 assert (time.minutes == 0); | |
1137 assert (time.seconds == 0); | |
1138 | |
1139 assert (t("12:34+:56") == 5); | |
1140 assert (time.hours == 12); | |
1141 assert (time.minutes == 34); | |
1142 assert (time.seconds == 0); | |
1143 | |
1144 // just convert to UTC time for time zones? | |
1145 | |
1146 assert (t("14:45:15Z") == 9); | |
1147 assert (time.hours == 14); | |
1148 assert (time.minutes == 45); | |
1149 assert (time.seconds == 15); | |
1150 | |
1151 assert (t("23Z") == 3); | |
1152 assert (time.hours == 23); | |
1153 assert (time.minutes == 0); | |
1154 assert (time.seconds == 0); | |
1155 | |
1156 assert (t("21:32:43-12:34") == 14); | |
1157 assert (time.hours == 10); | |
1158 assert (time.minutes == 6); | |
1159 assert (time.seconds == 43); | |
1160 | |
1161 assert (t("12:34,5+0000") == 12); | |
1162 assert (time.hours == 12); | |
1163 assert (time.minutes == 34); | |
1164 assert (time.seconds == 30); | |
1165 | |
1166 assert (t("03:04+07") == 8); | |
1167 assert (time.hours == 20); | |
1168 assert (time.minutes == 4); | |
1169 assert (time.seconds == 0); | |
1170 | |
1171 assert (t("11,5+") == 4); | |
1172 assert (time.hours == 11); | |
1173 assert (time.minutes == 30); | |
1174 | |
1175 assert (t("07-") == 2); | |
1176 assert (time.hours == 7); | |
1177 | |
1178 assert (t("06:12,7-") == 7); | |
1179 assert (time.hours == 6); | |
1180 assert (time.minutes == 12); | |
1181 assert (time.seconds == 42); | |
1182 | |
1183 assert (t("050403,2+") == 8); | |
1184 assert (time.hours == 5); | |
1185 assert (time.minutes == 4); | |
1186 assert (time.seconds == 3); | |
1187 assert (time.millis == 200); | |
1188 | |
1189 assert (t("061656-") == 6); | |
1190 assert (time.hours == 6); | |
1191 assert (time.minutes == 16); | |
1192 assert (time.seconds == 56); | |
1193 | |
1194 // date and time together | |
1195 | |
1196 size_t b(char[] s) { | |
1197 date = date.init; | |
1198 time = time.init; | |
1199 return iso8601(s, date, time); | |
1200 } | |
1201 | |
1202 assert (b("2007-08-09T12:34:56") == 19); | |
1203 assert (date.year == 2007); | |
1204 assert (date.month == 8); | |
1205 assert (date.day == 9); | |
1206 assert (time.hours == 12); | |
1207 assert (time.minutes == 34); | |
1208 assert (time.seconds == 56); | |
1209 | |
1210 assert (b("1985W155T235030,768") == 19); | |
1211 assert (date.year == 1985); | |
1212 assert (date.month == 4); | |
1213 assert (date.day == 12); | |
1214 assert (time.hours == 23); | |
1215 assert (time.minutes == 50); | |
1216 assert (time.seconds == 30); | |
1217 assert (time.millis == 768); | |
1218 | |
1219 // just convert to UTC time for time zones? | |
1220 | |
1221 assert (b("2009-08-07T01:02:03Z") == 20); | |
1222 assert (date.year == 2009); | |
1223 assert (date.month == 8); | |
1224 assert (date.day == 7); | |
1225 assert (time.hours == 1); | |
1226 assert (time.minutes == 2); | |
1227 assert (time.seconds == 3); | |
1228 | |
1229 assert (b("2007-08-09T03:02,5+04:56") == 24); | |
1230 assert (date.year == 2007); | |
1231 assert (date.month == 8); | |
1232 assert (date.day == 8); | |
1233 assert (time.hours == 22); | |
1234 assert (time.minutes == 6); | |
1235 assert (time.seconds == 30); | |
1236 | |
1237 assert (b("20000228T2330-01") == 16); | |
1238 assert (date.year == 2000); | |
1239 assert (date.month == 2); | |
1240 assert (date.day == 29); | |
1241 assert (time.hours == 0); | |
1242 assert (time.minutes == 30); | |
1243 assert (time.seconds == 0); | |
1244 | |
1245 assert (b("2007-01-01T00:00+01") == 19); | |
1246 assert (date.year == 2006); | |
1247 assert (date.month == 12); | |
1248 assert (date.day == 31); | |
1249 assert (time.hours == 23); | |
1250 assert (time.minutes == 0); | |
1251 assert (time.seconds == 0); | |
1252 | |
1253 assert (b("2007-12-31T23:00-01") == 19); | |
1254 assert (date.year == 2007); | |
1255 assert (date.month == 12); | |
1256 assert (date.day == 31); | |
1257 assert (time.hours == 24); | |
1258 assert (time.minutes == 0); | |
1259 assert (time.seconds == 0); | |
1260 | |
1261 assert (b("2007-12-31T23:01-01") == 19); | |
1262 assert (date.year == 2008); | |
1263 assert (date.month == 1); | |
1264 assert (date.day == 1); | |
1265 assert (time.hours == 0); | |
1266 assert (time.minutes == 1); | |
1267 assert (time.seconds == 0); | |
1268 | |
1269 assert (b("1902-03-04T1a") == 0); | |
1270 assert (b("1902-03-04T10:aa") == 0); | |
1271 assert (b("1902-03-04T10:1aa") == 0); | |
1272 assert (b("1985-04-12T10:15:30+0400") == 0); | |
1273 assert (b("1985-04-12T10:15:30-05:4") == 0); | |
1274 assert (b("1985-04-12T10:15:30-06:4b") == 0); | |
1275 assert (b("19020304T05:06:07") == 0); | |
1276 assert (b("1902-03-04T050607") == 0); | |
1277 assert (b("19020304T05:06:07abcd") == 0); | |
1278 assert (b("1902-03-04T050607abcd") == 0); | |
1279 | |
1280 // unimplemented: intervals, durations, recurring intervals | |
1281 } | |
1282 } |