Mercurial > projects > ldc
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tango/tango/time/ISO8601.d Fri Jan 11 17:57:40 2008 +0100 @@ -0,0 +1,1282 @@ +/******************************************************************************* + + copyright: Copyright (c) 2007 Deewiant. All rights reserved + + license: BSD style: $(LICENSE) + + version: Initial release: Aug 2007 + + author: Deewiant + + Based on the ISO 8601:2004 standard (described in a PDF Wikipedia + http://isotc.iso.org/livelink/livelink/4021199/ISO_8601_2004_E.zip? + func=doc.Fetch&nodeid=4021199), which has functions for parsing almost + every date/time format specified. + + The ones they don't parse are intervals, durations, and recurring + intervals, because I got too lazy to implement them. The functions + (iso8601Time, iso8601Date, and iso8601) update a Date passed instead + of a Time, as does the current iso8601, because that's too limited + a format. One can always convert to a Time if necessary, keeping + in mind that information loss might occur if the Date is outside the + interval Time can represent. + + In addition, because its dayOfWeek function only works for 1900-3-1 to + 2100-2-28, it would fail by a day or two on ISO week dates outside that + interval. (Currently it asserts outside 1901-2099.) If somebody knows a + good algorithm which would fix that, by all means submit it. + + Another thing it doesn't do is conversions from local time to UTC if the + time parsed starts with a 'T'. A comment in doIso8601Time() explains why. + + Because the Date struct has no support for time zones, the module just + converts times with specified time zones into UTC. This leads to + behaviour which may or may not be a bug, as explained in a comment in + getTimeZone(). + +*******************************************************************************/ + +module tango.time.ISO8601; + +public import tango.time.Time; + +public import tango.time.chrono.Calendar; + +private import tango.time.chrono.Gregorian; + +/// Returns the number of chars used to compose a valid date: 0 if no date can be composed. +/// Fields in date will either be correct (e.g. months will be >= 1 and <= 12) or zero. + +size_t iso8601Date(T)(T[] src, ref Date date, size_t expanded = 0) { + ubyte dummy = void; + T* p = src.ptr; + return doIso8601Date(p, src, date, expanded, dummy); +} + +private size_t doIso8601Date(T)(ref T* p, T[] src, ref Date date, size_t expanded, out ubyte separators) +out { + assert (!date.month || (date.month >= 1 && date.month <= 12)); + assert (!date.day || (date.month && date.day >= 1 && date.day <= daysPerMonth(date.month, date.year))); +} body { + + // always set era to AD + date.era = Gregorian.AD_ERA; + + size_t eaten() { return p - src.ptr; } + bool done(T[] s) { return .done(eaten(), src.length, *p, s); } + + if (!parseYear(p, expanded, date.year)) + return (date.year = 0); + + auto onlyYear = eaten(); + + // /([+-]Y{expanded})?(YYYY|YY)/ + if (done("-0123W")) + return onlyYear; + + if (accept(p, '-')) + separators = true; + + if (accept(p, 'W')) { + // (year)-Www-D + + T* p2 = p; + + int i = parseIntMax(p, 3u); + + if (i) if (p - p2 == 2) { + + // (year)-Www + if (done("-")) { + if (getMonthAndDayFromWeek(date, i)) + return eaten(); + + // (year)-Www-D + } else if (demand(p, '-')) + if (getMonthAndDayFromWeek(date, i, *p++ - '0')) + return eaten(); + + } else if (p - p2 == 3) + // (year)WwwD + if (getMonthAndDayFromWeek(date, i / 10, i % 10)) + return eaten(); + + return onlyYear; + } + + // next up, MM or MM[-]DD or DDD + + T* p2 = p; + + int i = parseIntMax(p); + if (!i) + return onlyYear; + + switch (p - p2) { + case 2: + date.month = i; + + if (!(date.month >= 1 && date.month <= 12)) { + date.month = 0; + return onlyYear; + } + + auto onlyMonth = eaten(); + + // (year)-MM + if (done("-")) + return onlyMonth; + + // (year)-MM-DD + if (!( + demand(p, '-') && + (date.day = parseIntMax(p, 2u)) != 0 && date.day <= daysPerMonth(date.month, date.year) + )) { + date.day = 0; + return onlyMonth; + } + + break; + + case 4: + // e.g. 20010203, i = 203 now + + date.month = i / 100; + date.day = i % 100; + + // (year)MMDD + if (!( + date.month >= 1 && date.month <= 12 && + date.day >= 0 && date.day <= daysPerMonth(date.month, date.year) + )) { + date.month = date.day = 0; + return onlyYear; + } + + break; + + case 3: + // (year)-DDD + // i is the ordinal of the day within the year + + bool leap = isLeapYear(date.year); + + if (i > 365 + leap) + return onlyYear; + + if (i <= 31) { + date.month = 1; + date.day = i; + + } else if (i <= 59 + leap) { + date.month = 2; + date.day = i - 31 - leap; + + } else if (i <= 90 + leap) { + date.month = 3; + date.day = i - 59 - leap; + + } else if (i <= 120 + leap) { + date.month = 4; + date.day = i - 90 - leap; + + } else if (i <= 151 + leap) { + date.month = 5; + date.day = i - 120 - leap; + + } else if (i <= 181 + leap) { + date.month = 6; + date.day = i - 151 - leap; + + } else if (i <= 212 + leap) { + date.month = 7; + date.day = i - 181 - leap; + + } else if (i <= 243 + leap) { + date.month = 8; + date.day = i - 212 - leap; + + } else if (i <= 273 + leap) { + date.month = 9; + date.day = i - 243 - leap; + + } else if (i <= 304 + leap) { + date.month = 10; + date.day = i - 273 - leap; + + } else if (i <= 334 + leap) { + date.month = 11; + date.day = i - 304 - leap; + + } else { + if (i > 365 + leap) + assert (false); + + date.month = 12; + date.day = i - 334 - leap; + } + + default: break; + } + + return eaten(); +} + +/// Returns the number of chars used to compose a valid date: 0 if no date can be composed. +/// 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. +/// time.seconds may be 60 if the hours and minutes are 23 and 59, as leap seconds are occasionally added to UTC time. +/// time.hours may be 0 or 24: the latter marks the end of a day, the former the beginning. + +size_t iso8601Time(T)(T[] src, ref Date date, ref TimeOfDay time) { + bool dummy = void; + T* p = src.ptr; + return doIso8601Time(p, src, date, time, WHATEVER, dummy); +} + +private enum : ubyte { NO = 0, YES = 1, WHATEVER } + +// bothValid is used only to get iso8601() to catch errors correctly +private size_t doIso8601Time(T)(ref T* p, T[] src, ref Date date, ref TimeOfDay time, ubyte separators, out bool bothValid) +out { + // yes, I could just write >= 0, but this emphasizes the difference between == 0 and != 0 + assert (!time.hours || (time.hours > 0 && time.hours <= 24)); + assert (!time.minutes || (time.minutes > 0 && time.minutes <= 59)); + assert (!time.seconds || (time.seconds > 0 && time.seconds <= 60)); + assert (!time.millis || (time.millis > 0 && time.millis <= 999)); +} body { + size_t eaten() { return p - src.ptr; } + bool done(T[] s) { return .done(eaten(), src.length, *p, s); } + + bool checkColon() { + if (separators == WHATEVER) + accept(p, ':'); + + else if (accept(p, ':') != separators) + return false; + + return true; + } + + byte getTimeZone() { return .getTimeZone(p, date, time, separators, &done); } + + // TODO/BUG: need to convert from local time if got T + // however, Tango provides nothing like Phobos's std.date.getLocalTZA + // (which doesn't look like it should work on Windows, it should use tzi.bias only, and GetTimeZoneInformationForYear) + // (and which uses too complicated code for Posix, tzset should be enough) + // and I'm not interested in delving into system-specific code right now + // remember also that -1 BC is the year zero in ISO 8601... -2 BC is -1, etc + if (separators == WHATEVER) + accept(p, 'T'); + + if (parseInt(p, 2u, time.hours) != 2 || time.hours > 24) + return (time.hours = 0); + + auto onlyHour = eaten(); + + // hh + if (done("+,-.012345:")) + return onlyHour; + + switch (getDecimal(p, time, HOUR)) { + case NOTFOUND: break; + case FOUND: + auto onlyDecimal = eaten(); + if (getTimeZone() == BAD) + return onlyDecimal; + + // /hh,h+/ + return eaten(); + + case BAD: return onlyHour; + default: assert (false); + } + + switch (getTimeZone()) { + case NOTFOUND: break; + case FOUND: return eaten(); + case BAD: return onlyHour; + default: assert (false); + } + + if ( + !checkColon() || + + parseInt(p, 2u, time.minutes) != 2 || time.minutes > 59 || + + // hour 24 is only for 24:00:00 + (time.hours == 24 && time.minutes != 0) + ) { + time.minutes = 0; + return onlyHour; + } + + auto onlyMinute = eaten(); + + // hh:mm + if (done("+,-.0123456:")) { + bothValid = true; + return onlyMinute; + } + + switch (getDecimal(p, time, MINUTE)) { + case NOTFOUND: break; + case FOUND: + auto onlyDecimal = eaten(); + if (getTimeZone() == BAD) + return onlyDecimal; + + // /hh:mm,m+/ + bothValid = true; + return eaten(); + + case BAD: return onlyMinute; + default: assert (false); + } + + switch (getTimeZone()) { + case NOTFOUND: break; + case FOUND: bothValid = true; return eaten(); + case BAD: return onlyMinute; + default: assert (false); + } + + if ( + !checkColon() || + parseInt(p, 2u, time.seconds) != 2 || time.seconds > 60 || + (time.hours == 24 && time.seconds != 0) || + (time.seconds == 60 && time.hours != 23 && time.minutes != 59) + ) { + time.seconds = 0; + return onlyMinute; + } + + auto onlySecond = eaten(); + + // hh:mm:ss + if (done("+,-.Z")) { + bothValid = true; + return onlySecond; + } + + switch (getDecimal(p, time, SECOND)) { + case NOTFOUND: break; + case FOUND: + auto onlyDecimal = eaten(); + if (getTimeZone() == BAD) + return onlyDecimal; + + // /hh:mm:ss,s+/ + bothValid = true; + return eaten(); + + case BAD: return onlySecond; + default: assert (false); + } + + if (getTimeZone() == BAD) + return onlySecond; + else { + bothValid = true; + return eaten(); // hh:mm:ss with timezone + } +} + +// combination of date and time +// stricter than just date followed by time: +// can't have an expanded or reduced date +// either use separators everywhere or not at all + +/// This function is very strict: either a complete date and time can be extracted, or nothing can. +/// If this function returns zero, the fields of date are undefined. + +size_t iso8601(T)(T[] src, ref Date date, ref TimeOfDay time) { + T* p = src.ptr; + ubyte sep; + bool bothValid = false; + + if ( + doIso8601Date(p, src, date, 0u, sep) && + date.year && date.month && date.day && + + // by mutual agreement this T may be omitted + // but this is just a convenience method for date+time anyway + demand(p, 'T') && + + doIso8601Time(p, src, date, time, sep, bothValid) && + bothValid + ) + return p - src.ptr; + else + return 0; +} + +/+ +++++++++++++++++++++++++++++++++++++++ +\ + + Privates used by date + +\+ +++++++++++++++++++++++++++++++++++++++ +/ + +// /([+-]Y{expanded})?(YYYY|YY)/ +private bool parseYear(T)(ref T* p, size_t expanded, out uint year) { + + bool doParse() { + T* p2 = p; + + if (!parseInt(p, expanded + 4u, year)) + return false; + + // it's Y{expanded}YY, Y{expanded}YYYY, or unacceptable + if (p - p2 - expanded == 2u) + year *= 100; + else if (p - p2 - expanded != 4u) + return false; + + return true; + } + + if (accept(p, '-')) { + if (!doParse()) + return false; + year = -year; + } else { + accept(p, '+'); + if (!doParse()) + return false; + } + + return true; +} + +// find the month and day based on the calendar week +// uses date.year for leap year calculations +// returns false if week and date.year are incompatible +// based on the VBA function at http://www.probabilityof.com/ISO8601.shtml +private bool getMonthAndDayFromWeek(ref Date date, int week, int day = 1) { + if (week < 1 || week > 53 || day < 1 || day > 7) + return false; + + bool leap = isLeapYear(date.year); + + // only years starting with Thursday and + // leap years starting with Wednesday have 53 weeks + + if (week == 53) { + int startingDay = dayOfWeek(date.year, 1, 1, leap); + + if (!(startingDay == 4 || (leap && startingDay == 3))) + return false; + } + + // days since year-01-04 + int delta = 7*(week - 1) - dayOfWeek(date.year, 1, 4, leap) + day; + + if (delta <= -4) { + if (delta < -7) + assert (false); + + --date.year; + date.month = 12; + date.day = delta + 4 + 31; + + } else if (delta <= 27) { + date.month = 1; + date.day = delta + 4; + + } else if (delta <= 56 + leap) { + date.month = 2; + date.day = delta - 27; + + } else if (delta <= 87 + leap) { + date.month = 3; + date.day = delta - 55 - leap; + + } else if (delta <= 117 + leap) { + date.month = 4; + date.day = delta - 86 - leap; + + } else if (delta <= 148 + leap) { + date.month = 5; + date.day = delta - 116 - leap; + + } else if (delta <= 178 + leap) { + date.month = 6; + date.day = delta - 147 - leap; + + } else if (delta <= 209 + leap) { + date.month = 7; + date.day = delta - 177 - leap; + + } else if (delta <= 240 + leap) { + date.month = 8; + date.day = delta - 208 - leap; + + } else if (delta <= 270 + leap) { + date.month = 9; + date.day = delta - 239 - leap; + + } else if (delta <= 301 + leap) { + date.month = 10; + date.day = delta - 269 - leap; + + } else if (delta <= 331 + leap) { + date.month = 11; + date.day = delta - 300 - leap; + + } else if (delta <= 361 + leap) { + date.month = 12; + date.day = delta - 330 - leap; + + } else { + if (delta > 365 + leap) + assert (false); + + ++date.year; + date.month = 1; + date.day = delta - 365 - leap + 4; + } + + return true; +} + +private bool isLeapYear(int year) { + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +// Babwani's Congruence +private int dayOfWeek(int year, int month, int day, bool leap) +in { + assert (month >= 1 && month <= 12); + assert (day >= 1 && day <= 31); + + // BUG: only works for 1900-3-1 to 2100-2-28 + assert (year >= 1901 && year <= 2099, "iso8601 :: Can't calculate day of week outside the years 1900-2099"); + +} out(result) { + assert (result >= 1 && result <= 7); + +} body { + int f() { + if (leap && month <= 2) + return [6,2][month-1]; + + return [0,3,3,6,1,4,6,2,5,0,3,5][month-1]; + } + + int d = ((5*(year % 100) / 4) - 2*((year / 100) % 4) + f() + day) % 7; + + // defaults to Saturday=0, Friday=6: convert to Monday=1, Sunday=7 + return (d <= 1 ? d+6 : d-1); +} + +/+ +++++++++++++++++++++++++++++++++++++++ +\ + + Privates used by time + +\+ +++++++++++++++++++++++++++++++++++++++ +/ + +private enum : ubyte { HOUR, MINUTE, SECOND } +private enum : byte { BAD, FOUND, NOTFOUND } + +private byte getDecimal(T)(ref T* p, ref TimeOfDay time, ubyte which) { + if (accept(p, ',') || accept(p, '.')) { + + T* p2 = p; + + int i; + size_t iLen = parseInt(p, i); + + if ( + iLen == 0 || + + // if i is 0, must have at least 3 digits + // ... or at least that's what I think the standard means + // when it says "[i]f the magnitude of the number is less + // than unity, the decimal sign shall be preceded by two + // zeros"... + // surely that should read "followed" and not "preceded" + + (i == 0 && iLen < 3) + ) + return BAD; + + // 10 to the power of (iLen - 1) + int pow = 1; + while (--iLen) + pow *= 10; + + switch (which) { + case HOUR: + time.minutes = 6 * i / pow; + time.seconds = 6 * i % pow; + break; + case MINUTE: + time.seconds = 6 * i / pow; + time.millis = 6000 * i / pow % 1000; + break; + case SECOND: + time.millis = 100 * i / pow; + break; + + default: assert (false); + } + + return FOUND; + } + + return NOTFOUND; +} + +// the Date is always UTC, so this just adds the offset to the date fields +// another option would be to add time zone fields to Date and have this fill them + +private byte getTimeZone(T)(ref T* p, ref Date date, ref TimeOfDay time, ubyte separators, bool delegate(T[]) done) { + if (accept(p, 'Z')) + return FOUND; + + int factor = -1; + + if (accept(p, '-')) + factor = 1; + + else if (!accept(p, '+')) + return NOTFOUND; + + int hour, realhour = time.hours, realminute = time.minutes; + scope(exit) time.hours = cast(uint)realhour; + scope(exit) time.minutes = cast(uint)realminute; + if (parseInt(p, 2u, hour) != 2 || hour > 12 || (hour == 0 && factor == 1)) + return BAD; + + realhour += factor * hour; + + void hourCheck() { + if (realhour > 24 || (realhour == 24 && (realminute || time.seconds))) { + realhour -= 24; + + // BUG? what should be done? + // if we get a time like 20:00-05:00 + // which needs to be converted to UTC by adding 05:00 to 20:00 + // we just set the time to 01:00 and the day to 1 + // even though this is time, which really has nothing to do with the day, which is part of the date + // if this isn't a bug, it needs to be documented: it's not necessarily obvious + if (date.day++ && date.day > daysPerMonth(date.month, date.year)) { + date.day = 1; + if (++date.month > 12) { + date.month = 1; + ++date.year; + } + } + } else if (realhour < 0) { + realhour += 24; + + // ditto above BUG? + if (date.day-- && date.day < 1) { + if (--date.month < 1) { + date.month = 12; + --date.year; + } + + date.day = daysPerMonth(date.month, date.year); + } + } + } + + if (done("012345:")) { + hourCheck(); + return FOUND; + } + + if (separators == WHATEVER) + accept(p, ':'); + + else if (accept(p, ':') != separators) + return BAD; + + int minute; + if (parseInt(p, 2u, minute) != 2) + return BAD; + + assert (minute <= 59); + + realminute += factor * minute; + + if (realminute > 59) { + realminute -= 60; + ++realhour; + + } else if (realminute < 0) { + realminute += 60; + --realhour; + } + + hourCheck(); + return FOUND; +} + +/+ +++++++++++++++++++++++++++++++++++++++ +\ + + Privates used by both date and time + +\+ +++++++++++++++++++++++++++++++++++++++ +/ + +private bool accept(T)(ref T* p, char c) { + if (*p == c) { + ++p; + return true; + } + return false; +} + +private bool demand(T)(ref T* p, char c) { + return (*p++ == c); +} + +private bool done(T)(size_t eaten, size_t srcLen, T p, T[] s) { + if (eaten == srcLen) + return true; + + // s is the array of characters which may come next + // (i.e. which p may be) + // sorted in ascending order + foreach (c; s) { + if (p < c) + return true; + else if (p == c) + break; + } + + return false; +} + +private int daysPerMonth(int month, int year) { + if (month == 2 && isLeapYear(year)) + return 29; + else + return [31,28,31,30,31,30,31,31,30,31,30,31][month-1]; +} + +/****************************************************************************** + + Extract an integer from the input + +******************************************************************************/ + +// note: ISO 8601 code relies on these values always being positive, failing if *p == '-' + +private uint parseIntMax(T) (ref T* p) { + uint value = 0; + while (*p >= '0' && *p <= '9') + value = value * 10 + *p++ - '0'; + return value; +} + +// ... but accept no more than max digits + +private uint parseIntMax(T)(ref T* p, uint max) { + size_t i = 0; + uint value = 0; + while (p[i] >= '0' && p[i] <= '9' && i < max) + value = value * 10 + p[i++] - '0'; + p += i; + return value; +} + +// ... and return the amount of digits processed + +private size_t parseInt(T, U)(ref T* p, out U i) { + T* p2 = p; + i = cast(U)parseIntMax(p); + return p - p2; +} + +private size_t parseInt(T, U)(ref T* p, uint max, out U i) { + T* p2 = p; + i = cast(U)parseIntMax(p, max); + return p - p2; +} + +//////////////////// + +debug (UnitTest) { + import tango.io.Stdout; + + debug(ISO8601) + { + void main() { } + } + + unittest { + Date date; + TimeOfDay time; + + // date + + size_t d(char[] s, size_t e = 0) { + date = date.init; + return iso8601Date(s, date, e); + } + + assert (d("20abc") == 2); + assert (date.year == 2000); + + assert (d("2004") == 4); + assert (date.year == 2004); + + assert (d("+0019", 2) == 5); + assert (date.year == 1900); + + assert (d("+111985", 2) == 7); + assert (date.year == 111985); + + assert (d("+111985", 1) == 6); + assert (date.year == 11198); + + assert (d("+111985", 3) == 0); + assert (!date.year); + + assert (d("+111985", 4) == 7); + assert (date.year == 11198500); + + assert (d("-111985", 5) == 0); + assert (!date.year); + + assert (d("abc") == 0); + assert (!date.year); + + assert (d("abc123") == 0); + assert (!date.year); + + assert (d("2007-08") == 7); + assert (date.year == 2007); + assert (date.month == 8); + + assert (d("+001985-04", 2) == 10); + assert (date.year == 1985); + assert (date.month == 4); + + assert (d("2007-08-07") == 10); + assert (date.year == 2007); + assert (date.month == 8); + assert (date.day == 7); + + assert (d("2008-20-30") == 4); + assert (date.year == 2008); + assert (!date.month); + + assert (d("2007-02-30") == 7); + assert (date.year == 2007); + assert (date.month == 2); + + assert (d("20060708") == 8); + assert (date.year == 2006); + assert (date.month == 7); + assert (date.day == 8); + + assert (d("19953080") == 4); + assert (date.year == 1995); + assert (!date.month); + + assert (d("+001985-04-12", 2) == 13); + assert (date.year == 1985); + assert (date.month == 4); + assert (date.day == 12); + + assert (d("-0123450607", 2) == 11); + assert (date.year == -12345); + assert (date.month == 6); + assert (date.day == 7); + + assert (d("1985W15") == 7); + assert (date.year == 1985); + assert (date.month == 4); + assert (date.day == 8); + + assert (d("2008-W01") == 8); + assert (date.year == 2007); + assert (date.month == 12); + assert (date.day == 31); + + assert (d("2008-W01-2") == 10); + assert (date.year == 2008); + assert (date.month == 1); + assert (date.day == 1); + + assert (d("2009-W53-4") == 10); + assert (date.year == 2009); + assert (date.month == 12); + assert (date.day == 31); + + assert (d("2009-W01-1") == 10); + assert (date.year == 2008); + assert (date.month == 12); + assert (date.day == 29); + + assert (d("2009W537") == 8); + assert (date.year == 2010); + assert (date.month == 1); + assert (date.day == 3); + + assert (d("2010W537") == 4); + assert (date.year == 2010); + assert (!date.month); + + assert (d("2009-W01-3") == 10); + assert (date.year == 2008); + assert (date.month == 12); + assert (date.day == 31); + + assert (d("2009-W01-4") == 10); + assert (date.year == 2009); + assert (date.month == 1); + assert (date.day == 1); + + /+ BUG: these don't work due to dayOfWeek being crap + + assert (d("1000-W07-7") == 10); + assert (date.year == 1000); + assert (date.month == 2); + assert (date.day == 16); + + assert (d("1500-W11-1") == 10); + assert (date.year == 1500); + assert (date.month == 3); + assert (date.day == 12); + + assert (d("1700-W14-2") == 10); + assert (date.year == 1700); + assert (date.month == 4); + assert (date.day == 6); + + assert (d("1800-W19-3") == 10); + assert (date.year == 1800); + assert (date.month == 5); + assert (date.day == 7); + + assert (d("1900-W25-4") == 10); + assert (date.year == 1900); + assert (date.month == 6); + assert (date.day == 21); + + assert (d("0900-W27-5") == 10); + assert (date.year == 900); + assert (date.month == 7); + assert (date.day == 9); + + assert (d("0800-W33-6") == 10); + assert (date.year == 800); + assert (date.month == 8); + assert (date.day == 19); + + assert (d("0700-W37-7") == 10); + assert (date.year == 700); + assert (date.month == 9); + assert (date.day == 16); + + assert (d("0600-W41-4") == 10); + assert (date.year == 600); + assert (date.month == 10); + assert (date.day == 9); + + assert (d("0500-W45-7") == 10); + assert (date.year == 500); + assert (date.month == 11); + assert (date.day == 14);+/ + + assert (d("2000-W55") == 4); + assert (date.year == 2000); + + assert (d("1980-002") == 8); + assert (date.year == 1980); + assert (date.month == 1); + assert (date.day == 2); + + assert (d("1981-034") == 8); + assert (date.year == 1981); + assert (date.month == 2); + assert (date.day == 3); + + assert (d("1982-063") == 8); + assert (date.year == 1982); + assert (date.month == 3); + assert (date.day == 4); + + assert (d("1983-095") == 8); + assert (date.year == 1983); + assert (date.month == 4); + assert (date.day == 5); + + assert (d("1984-127") == 8); + assert (date.year == 1984); + assert (date.month == 5); + assert (date.day == 6); + + assert (d("1985-158") == 8); + assert (date.year == 1985); + assert (date.month == 6); + assert (date.day == 7); + + assert (d("1986-189") == 8); + assert (date.year == 1986); + assert (date.month == 7); + assert (date.day == 8); + + assert (d("1987-221") == 8); + assert (date.year == 1987); + assert (date.month == 8); + assert (date.day == 9); + + assert (d("1988-254") == 8); + assert (date.year == 1988); + assert (date.month == 9); + assert (date.day == 10); + + assert (d("1989-284") == 8); + assert (date.year == 1989); + assert (date.month == 10); + assert (date.day == 11); + + assert (d("1990316") == 7); + assert (date.year == 1990); + assert (date.month == 11); + assert (date.day == 12); + + assert (d("1991-347") == 8); + assert (date.year == 1991); + assert (date.month == 12); + assert (date.day == 13); + + assert (d("1992-000") == 4); + assert (date.year == 1992); + + assert (d("1993-370") == 4); + assert (date.year == 1993); + + // time + + size_t t(char[] s) { + time = time.init; + date = date.init; + + return iso8601Time(s, date, time); + } + + assert (t("20") == 2); + assert (time.hours == 20); + assert (time.minutes == 0); + assert (time.seconds == 0); + + assert (t("30") == 0); + + assert (t("2004") == 4); + assert (time.hours == 20); + assert (time.minutes == 4); + assert (time.seconds == 0); + + assert (t("200406") == 6); + assert (time.hours == 20); + assert (time.minutes == 4); + assert (time.seconds == 6); + + assert (t("24:00") == 5); + assert (time.hours == 24); // should compare equal with 0... can't just set to 0, loss of information + assert (time.minutes == 0); + assert (time.seconds == 0); + + assert (t("00:00") == 5); + assert (time.hours == 0); + assert (time.minutes == 0); + assert (time.seconds == 0); + + assert (t("23:59:60") == 8); + assert (time.hours == 23); + assert (time.minutes == 59); + assert (time.seconds == 60); // leap second + + assert (t("16:49:30,001") == 12); + assert (time.hours == 16); + assert (time.minutes == 49); + assert (time.seconds == 30); + assert (time.millis == 1); + + assert (t("15:48:29,1") == 10); + assert (time.hours == 15); + assert (time.minutes == 48); + assert (time.seconds == 29); + assert (time.millis == 100); + + assert (t("02:10:34,a") == 8); + assert (time.hours == 2); + assert (time.minutes == 10); + assert (time.seconds == 34); + + assert (t("14:50,5") == 7); + assert (time.hours == 14); + assert (time.minutes == 50); + assert (time.seconds == 30); + + assert (t("1540,4") == 6); + assert (time.hours == 15); + assert (time.minutes == 40); + assert (time.seconds == 24); + + assert (t("1250,") == 4); + assert (time.hours == 12); + assert (time.minutes == 50); + + assert (t("14,5") == 4); + assert (time.hours == 14); + assert (time.minutes == 30); + + assert (t("12,") == 2); + assert (time.hours == 12); + assert (time.minutes == 0); + + assert (t("24:00:01") == 5); + assert (time.hours == 24); + assert (time.minutes == 0); + assert (time.seconds == 0); + + assert (t("12:34+:56") == 5); + assert (time.hours == 12); + assert (time.minutes == 34); + assert (time.seconds == 0); + + // just convert to UTC time for time zones? + + assert (t("14:45:15Z") == 9); + assert (time.hours == 14); + assert (time.minutes == 45); + assert (time.seconds == 15); + + assert (t("23Z") == 3); + assert (time.hours == 23); + assert (time.minutes == 0); + assert (time.seconds == 0); + + assert (t("21:32:43-12:34") == 14); + assert (time.hours == 10); + assert (time.minutes == 6); + assert (time.seconds == 43); + + assert (t("12:34,5+0000") == 12); + assert (time.hours == 12); + assert (time.minutes == 34); + assert (time.seconds == 30); + + assert (t("03:04+07") == 8); + assert (time.hours == 20); + assert (time.minutes == 4); + assert (time.seconds == 0); + + assert (t("11,5+") == 4); + assert (time.hours == 11); + assert (time.minutes == 30); + + assert (t("07-") == 2); + assert (time.hours == 7); + + assert (t("06:12,7-") == 7); + assert (time.hours == 6); + assert (time.minutes == 12); + assert (time.seconds == 42); + + assert (t("050403,2+") == 8); + assert (time.hours == 5); + assert (time.minutes == 4); + assert (time.seconds == 3); + assert (time.millis == 200); + + assert (t("061656-") == 6); + assert (time.hours == 6); + assert (time.minutes == 16); + assert (time.seconds == 56); + + // date and time together + + size_t b(char[] s) { + date = date.init; + time = time.init; + return iso8601(s, date, time); + } + + assert (b("2007-08-09T12:34:56") == 19); + assert (date.year == 2007); + assert (date.month == 8); + assert (date.day == 9); + assert (time.hours == 12); + assert (time.minutes == 34); + assert (time.seconds == 56); + + assert (b("1985W155T235030,768") == 19); + assert (date.year == 1985); + assert (date.month == 4); + assert (date.day == 12); + assert (time.hours == 23); + assert (time.minutes == 50); + assert (time.seconds == 30); + assert (time.millis == 768); + + // just convert to UTC time for time zones? + + assert (b("2009-08-07T01:02:03Z") == 20); + assert (date.year == 2009); + assert (date.month == 8); + assert (date.day == 7); + assert (time.hours == 1); + assert (time.minutes == 2); + assert (time.seconds == 3); + + assert (b("2007-08-09T03:02,5+04:56") == 24); + assert (date.year == 2007); + assert (date.month == 8); + assert (date.day == 8); + assert (time.hours == 22); + assert (time.minutes == 6); + assert (time.seconds == 30); + + assert (b("20000228T2330-01") == 16); + assert (date.year == 2000); + assert (date.month == 2); + assert (date.day == 29); + assert (time.hours == 0); + assert (time.minutes == 30); + assert (time.seconds == 0); + + assert (b("2007-01-01T00:00+01") == 19); + assert (date.year == 2006); + assert (date.month == 12); + assert (date.day == 31); + assert (time.hours == 23); + assert (time.minutes == 0); + assert (time.seconds == 0); + + assert (b("2007-12-31T23:00-01") == 19); + assert (date.year == 2007); + assert (date.month == 12); + assert (date.day == 31); + assert (time.hours == 24); + assert (time.minutes == 0); + assert (time.seconds == 0); + + assert (b("2007-12-31T23:01-01") == 19); + assert (date.year == 2008); + assert (date.month == 1); + assert (date.day == 1); + assert (time.hours == 0); + assert (time.minutes == 1); + assert (time.seconds == 0); + + assert (b("1902-03-04T1a") == 0); + assert (b("1902-03-04T10:aa") == 0); + assert (b("1902-03-04T10:1aa") == 0); + assert (b("1985-04-12T10:15:30+0400") == 0); + assert (b("1985-04-12T10:15:30-05:4") == 0); + assert (b("1985-04-12T10:15:30-06:4b") == 0); + assert (b("19020304T05:06:07") == 0); + assert (b("1902-03-04T050607") == 0); + assert (b("19020304T05:06:07abcd") == 0); + assert (b("1902-03-04T050607abcd") == 0); + + // unimplemented: intervals, durations, recurring intervals + } +}