123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587
/*******************************************************************************

        Copyright:      Copyright (c) 2007-2008 Matti Niemenmaa.
                        All rights reserved
        License:        BSD style: $(LICENSE)
        Version:        Aug 2007: Initial release
                        Feb 2008: Retooled
        Author:         Matti Niemenmaa

        This module is based on the ISO 8601:2004 standard, and has functions
        for parsing (almost) every date/time format specified therein. (The
        ones not supported are intervals, durations, and recurring intervals.)

        Refer to the standard for a full description of the formats supported.

        The functions (parseTime, parseDate, and parseDateAndTime) are
        overloaded into two different versions of each: one updates a given
        Time, and the other updates a given ExtendedDate struct. The purpose of
        this struct is to support more detailed information which the Time data
        type does not (and, given its simple integer nature, cannot) support.

        Times with specified time zones are simply converted into UTC: this may
        lead to the date changing when only a time was parsed: e.g. "01:00+03"
        is the same as "22:00", except that when the former is parsed, one is
        subtracted from the day.

*******************************************************************************/

module tango.time.ISO8601;

public import tango.time.Time;
public import tango.time.chrono.Gregorian;

import tango.core.Exception : IllegalArgumentException;
import tango.math.Math : min;

private alias Time DT;
private alias ExtendedDate FullDate;

/** An extended date type, wrapping a Time together with some additional
 * information. */
public struct ExtendedDate {
   /** The Time value, containing the information it can. */
   DT val;

   private int year_;

   /** Returns the year part of the date: a value in the range
    * [-1_000_000_000,-1] ∪ [1,999_999_999], where -1 is the year 1 BCE.
    *
    * Do not use val.year directly unless you are absolutely sure that it is in
    * the range a Time can hold (-10000 to 9999).
    */
   @property const int year()
   out(val) {
      assert (  (val >= -1_000_000_000 && val <=          -1)
             || (val >=              1 && val <= 999_999_999));
   } body {
      if (year_)
         return year_;

      auto era = Gregorian.generic.getEra(val);
      if (era == Gregorian.AD_ERA)
         return Gregorian.generic.getYear(val);
      else
         return -Gregorian.generic.getYear(val);
   }

   // y may be zero: if so, it refers to the year 1 BCE
   @property private void year(int y) {
      if (DTyear(y)) {
         year_ = 0;
         // getYear returns uint: be careful with promotion to unsigned
         int toAdd = y - Gregorian.generic.getYear(val);
         val = Gregorian.generic.addYears(val, toAdd);
      } else
         year_ = y < 0 ? y-1 : y;
   }

   private byte mask; // leap second and endofday

   /** Returns the seconds part of the date: may be 60 if a leap second
    * occurred. In such a case, val's seconds part is 59.
    */
   @property const uint seconds() { return val.time.seconds + ((mask >>> 0) & 1); }
   alias seconds secs, second, sec;

   /** Whether the ISO 8601 representation of this hour is 24 or 00: whether
    * this instant of midnight is to be considered the end of the previous day
    * or the start of the next.
    *
    * If the time of val is not exactly 00:00:00.000, this value is undefined.
    */
   @property const bool endOfDay() { return 1 ==              ((mask >>> 1) & 1); }

   private void setLeap    () { mask |= 1 << 0; }
   private void setEndOfDay() { mask |= 1 << 1; }

   debug (Tango_ISO8601) private char[] toStr() {
      return Stdout.layout.convert(
         "{:d} and {:d}-{:d2}-{:d2} :: {:d2}:{:d2}:{:d2}.{:d3} and {:d2}, {}",
         year_, years(*this), months(*this), days(*this),
         hours(*this), mins(*this), .secs(*this), ms(*this),
         this.seconds, this.endOfDay);
   }
}

/** Parses a date in a format specified in ISO 8601:2004.
 *
 * Returns the number of characters used to compose a valid date: 0 if no date
 * can be composed.
 *
 * Fields in dt will either be correct (e.g. months will be >= 1 and <= 12) or
 * the default, which is 1 for year, month, and day, and 0 for all other
 * fields. Unless one is absolutely sure that 0001-01-01 can never be
 * encountered, one should check the return value to be sure that the parsing
 * succeeded as expected.
 *
 * A third parameter is available for the ExtendedDate version: this allows for
 * parsing expanded year representations. The parameter is the number of extra
 * year digits beyond four, and defaults to zero. It must be within the range
 * [0,5]: this allows for a maximum year of 999 999 999, which should be enough
 * for now.
 *
 * When using expanded year representations, be careful to use
 * ExtendedDate.year instead of the Time's year value.
 *
 * Examples:
 * ---
 * Time t;
 * ExtendedDate ed;
 *
 * parseDate("19",             t);    // January 1st, 1900
 * parseDate("1970",           t);    // January 1st, 1970
 * parseDate("1970-02",        t);    // February 1st, 1970
 * parseDate("19700203",       t);    // February 3rd, 1970
 * parseDate("+19700203",     ed, 2); // March 1st, 197002
 * parseDate("-197002-04-01", ed, 2); // April 1st, -197003 (197003 BCE)
 * parseDate("00000101",       t);    // January 1st, -1 (1 BCE)
 * parseDate("1700-W14-2",     t);    // April 6th, 1700
 * parseDate("2008W01",        t);    // December 31st, 2007
 * parseDate("1987-221",       t);    // August 9th, 1987
 * parseDate("1234abcd",       t);    // January 1st, 1234; return value is 4
 * parseDate("12abcdef",       t);    // January 1st, 1200; return value is 2
 * parseDate("abcdefgh",       t);    // January 1st, 0001; return value is 0
 * ---
 */
public size_t parseDate(T)(T[] src, ref DT dt) {
   auto fd = FullDate(dt);

   auto ret = parseDate(src, fd);
   dt = fd.val;
   return ret;
}
/** ditto */
public size_t parseDate(T)(T[] src, ref FullDate fd, ubyte expanded = 0) {
   ubyte dummy = void;
   T* p = src.ptr;
   return doIso8601Date(p, src, fd, expanded, dummy);
}

private size_t doIso8601Date(T)(

   ref T* p, T[] src,
   ref FullDate fd,
   ubyte expanded,
   out ubyte separators

) {
   if (expanded > 5)
      throw new IllegalArgumentException(
         "ISO8601 :: year expanded by more than 5 digits does not fit in int");

   size_t eaten() { return p - src.ptr; }
   size_t remaining() { return src.length - eaten(); }
   bool done(const(T[]) s) { return .done(eaten(), src.length, p, s); }

   if (!parseYear(p, src.length, expanded, fd))
      return 0;

   auto onlyYear = eaten();

   // /([+-]Y{expanded})?(YYYY|YY)/
   if (done("-0123W"))
      return onlyYear;

   if (accept(p, '-')) {
      separators = YES;

      if (remaining() == 0)
         return eaten() - 1;
   }

   if (accept(p, 'W')) {
      T* p2 = p;

      int i = parseInt(p, min(cast(size_t)3, remaining()));

      if (i) 
      {
          if (p - p2 == 2) {

               // (year)-Www
               if (done("-")) {
                      if (getMonthAndDayFromWeek(fd, i))
                           return eaten();

               // (year)-Www-D
               } else if (demand(p, '-')) {
                      if (remaining() == 0)
                           return eaten() - 1;

                      if (separators == NO) {
                           // (year)Www after all
                           if (getMonthAndDayFromWeek(fd, i))
                                  return eaten() - 1;

                      } else if (getMonthAndDayFromWeek(fd, i, *p++ - '0'))
                           return eaten();
               }

          } else if (p - p2 == 3) {
               // (year)WwwD, i == wwD

               if (separators == YES) {
                      // (year)-Www after all
                      if (getMonthAndDayFromWeek(fd, i / 10))
                           return eaten() - 1;

               } else if (getMonthAndDayFromWeek(fd, i / 10, i % 10))
                      return eaten();
          }
      }
      return onlyYear;
   }

   // next up, MM or MM[-]DD or DDD

   T* p2 = p;

   int i = parseInt(p, remaining());
   if (!i)
      return onlyYear;

   switch (p - p2) {
      case 2:
         // MM or MM-DD

         if (i >= 1 && i <= 12)
            addMonths(fd, i);
         else
            return onlyYear;

         auto onlyMonth = eaten();

         // (year)-MM
         if (done("-") || !demand(p, '-') || separators == NO)
            return onlyMonth;

         int day = parseInt(p, min(cast(size_t)2, remaining()));

         // (year)-MM-DD
         if (day && day <= daysPerMonth(months(fd), fd.year))
            addDays(fd, day);
         else
            return onlyMonth;

         break;

      case 4:
         // e.g. 20010203, i = 203 now

         int month = i / 100;
         int day   = i % 100;

         if (separators == YES) {
            // Don't accept the day: behave as though we only got the month
            p -= 2;
            i = month;
            goto case 2;
         }

         // (year)MMDD
         if (
            month >= 1 && month <= 12 &&
            day   >= 1 && day   <= daysPerMonth(month, fd.year)
         ) {
            addMonths(fd, month);
            addDays  (fd, day);
         } else
            return onlyYear;

         break;

      case 3:
         // (year)-DDD
         // i is the ordinal of the day within the year

         if (i > 365 + isLeapYear(fd.year))
            return onlyYear;

         addDays(fd, i);
         break;
      default: break;
   }
   return eaten();
}

/** Parses a time of day in a format specified in ISO 8601:2004.
 *
 * Returns the number of characters used to compose a valid time: 0 if no time
 * can be composed.
 *
 * Fields in dt will either be correct or the default, which is 0 for all
 * time-related fields. fields. Unless one is absolutely sure that midnight
 * can never be encountered, one should check the return value to be sure that
 * the parsing succeeded as expected.
 *
 * Extra fields in ExtendedDate:
 *
 * Seconds may be 60 if the hours and minutes are 23 and 59, as leap seconds
 * are occasionally added to UTC time. A Time's seconds will be 59 in this
 * case.
 *
 * Hours may be 0 or 24: the latter marks the end of a day and the former the
 * beginning, although they both refer to the same instant in time. A Time
 * will be precisely 00:00 in either case.
 *
 * Examples:
 * ---
 * Time t;
 * ExtendedDate ed;
 *
 * // ",000" omitted for clarity
 * parseTime("20",             t); // 20:00:00
 * parseTime("2004",           t); // 20:04:00
 * parseTime("20:04:06",       t); // 20:04:06
 * parseTime("16:49:30,001",   t); // 16:49:30,001
 * parseTime("16:49:30,1",     t); // 16:49:30,100
 * parseTime("16:49,4",        t); // 16:49:24
 * parseTime("23:59:60",      ed); // 23:59:60
 * parseTime("24:00:01",       t); // 00:00:00; return value is 5
 * parseTime("24:00:01",      ed); // 00:00:00; return value is 5; endOfDay
 * parseTime("30",             t); // 00:00:00; return value is 0
 * parseTime("21:32:43-12:34", t); // 10:06:43; day increased by one
 * ---
 */
public size_t parseTime(T)(T[] src, ref DT dt) {
   auto fd = FullDate(dt);

   auto ret = parseTime(src, fd);
   dt = fd.val;
   return ret;
}
/** ditto */
public size_t parseTime(T)(T[] src, ref FullDate fd) {
   bool dummy = void;
   T* p = src.ptr;
   return doIso8601Time(p, src, fd, WHATEVER, dummy);
}

// separators
private enum : ubyte { NO = 0, YES = 1, WHATEVER }

// bothValid is used only to get parseDateAndTime() to catch errors correctly
private size_t doIso8601Time(T)(

   ref T* p, T[] src,
   ref FullDate fd,
   ubyte separators,
   out bool bothValid

) {
   size_t eaten() { return p - src.ptr; }
   size_t remaining() { return src.length - eaten(); }
   bool done(const(T[]) s) { return .done(eaten(), src.length, p, s); }
   bool checkColon() { return .checkColon(p, separators); }

   byte getTimeZone() { return .getTimeZone(p, remaining(), fd, separators, &done); }

   if (separators == WHATEVER)
      accept(p, 'T');

   int hour = void;
   if (parseInt(p, min(cast(size_t)2, remaining()), hour) != 2 || hour > 24)
      return 0;

   if (hour == 24)
      fd.setEndOfDay();

   // Add the hours even if endOfDay: the day should be the next day, not the
   // previous
   addHours(fd, hour);

   auto onlyHour = eaten();

   // hh
   if (done("+,-.012345:"))
      return onlyHour;

   switch (getDecimal(p, remaining(), fd, 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())
      return onlyHour;

   int mins = void;
   if (
      parseInt(p, min(cast(size_t)2, remaining()), mins) != 2 ||
      mins > 59 ||
      // end of day is only for 24:00:00
      (fd.endOfDay && mins != 0)
   )
      return onlyHour;

   addMins(fd, mins);

   auto onlyMinute = eaten();

   // hh:mm
   if (done("+,-.0123456:")) {
      bothValid = true;
      return onlyMinute;
   }

   switch (getDecimal(p, remaining(), fd, 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())
      return onlyMinute;

   int sec = void;
   if (
      parseInt(p, min(cast(size_t)2, remaining()), sec) != 2 ||
      sec > 60 ||
      (fd.endOfDay && sec != 0)
   )
      return onlyMinute;

   if (sec == 60) {
      if (hours(fd) != 23 && .mins(fd) != 59)
         return onlyMinute;

      fd.setLeap();
      --sec;
   }
   addSecs(fd, sec);

   auto onlySecond = eaten();

   // hh:mm:ss
   if (done("+,-.Z")) {
      bothValid = true;
      return onlySecond;
   }

   switch (getDecimal(p, remaining(), fd, 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
   }
}

/** Parses a combined date and time in a format specified in ISO 8601:2004.
 *
 * Returns the number of characters used to compose a valid date and time.
 * Zero is returned if a complete date and time cannot be extracted. In that
 * case, the value of the resulting Time or ExtendedDate is undefined.
 *
 * This function is stricter than just calling parseDate followed by
 * parseTime: there are no allowances for expanded years or reduced dates
 * (two-digit years), and separator usage must be consistent.
 *
 * Although the standard allows for omitting the T between the date and the
 * time, this function requires it.
 *
 * Examples:
 * ---
 * Time t;
 *
 * // January 1st, 2008 00:01:00
 * parseDateAndTime("2007-12-31T23:01-01", t);
 *
 * // April 12th, 1985 23:50:30,042
 * parseDateAndTime("1985W155T235030,042", t);
 *
 * // Invalid time: returns zero
 * parseDateAndTime("1902-03-04T10:1a", t);
 *
 * // Separating T omitted: returns zero
 * parseDateAndTime("1985-04-1210:15:30+04:00", t);
 *
 * // Inconsistent separators: all return zero
 * parseDateAndTime("200512-01T10:02",          t);
 * parseDateAndTime("1985-04-12T10:15:30+0400", t);
 * parseDateAndTime("1902-03-04T050607",        t);
 * ---
 */
public size_t parseDateAndTime(T)(T[] src, ref DT dt) {
   FullDate fd;
   auto ret = parseDateAndTime(src, fd);
   dt = fd.val;
   return ret;
}
/** ditto */
public size_t parseDateAndTime(T)(T[] src, ref FullDate fd) {
   T* p = src.ptr;
   ubyte sep;
   bool bothValid = false;

   if (
      doIso8601Date(p, src, fd, cast(ubyte)0, sep) &&

      // by mutual agreement this T may be omitted
      // but this is just a convenience method for date+time anyway
      src.length - (p - src.ptr) >= 1 &&
      demand(p, 'T') &&

      doIso8601Time(p, src, fd, sep, bothValid) &&
      bothValid
   )
      return p - src.ptr;
   else
      return 0;
}

/+ +++++++++++++++++++++++++++++++++++++++ +\

   Privates used by date

\+ +++++++++++++++++++++++++++++++++++++++ +/

private:

// /([+-]Y{expanded})?(YYYY|YY)/
bool parseYear(T)(ref T* p, size_t len, ubyte expanded, ref FullDate fd) {

   int year = void;

   bool doParse() {
      T* p2 = p;

      if (!parseInt(p, min(cast(size_t)(expanded + 4), len), year))
         return false;

      // it's Y{expanded}YY, Y{expanded}YYYY, or unacceptable

      if (p - p2 - expanded == 2)
         year *= 100;
      else if (p - p2 - expanded != 4)
         return false;

      return true;
   }

   if (accept(p, '-')) {
      if (!doParse() || year < 0)
         return false;
      year = -year;
   } else {
      accept(p, '+');
      if (!doParse() || year < 0)
         return false;
   }

   fd.year = year;

   return true;
}

// find the month and day given a calendar week and the day of the week
// uses fd.year for leap year calculations
// returns false if week and fd.year are incompatible
bool getMonthAndDayFromWeek(ref FullDate fd, int week, int day = 1) {
   if (week < 1 || week > 53 || day < 1 || day > 7)
      return false;

   int year = fd.year;

   // only years starting with Thursday and leap years starting with Wednesday
   // have 53 weeks
   if (week == 53) {
      int startingDay = dayOfWeek(year, 1, 1);

      if (!(startingDay == 4 || (isLeapYear(year) && startingDay == 3)))
         return false;
   }

   // XXX
   // days since year-01-04, plus 4 (?)...
   /* This is a bit scary, actually: I have ***no idea why this works***. I
    * came up with this completely by accident. It seems to work though -
    * unless it fails in some (very) obscure case which isn't represented in
    * the unit tests.
   */
   addDays(fd, 7*(week - 1) + day - dayOfWeek(year, 1, 4) + 4);

   return true;
}

bool isLeapYear(int year) {
   return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}

int dayOfWeek(int year, int month, int day)
in {
   assert (month  >= 1 && month  <= 12);
   assert (day    >= 1 && day    <= 31);
} out(result) {
   assert (result >= 1 && result <= 7);
} body {
   uint era = erafy(year);

   int result =
      Gregorian.generic.getDayOfWeek(
         Gregorian.generic.toTime(year, month, day, 0, 0, 0, 0, era));

   if (result == Gregorian.DayOfWeek.Sunday)
      return 7;
   else
      return result;
}

/+ +++++++++++++++++++++++++++++++++++++++ +\

   Privates used by time

\+ +++++++++++++++++++++++++++++++++++++++ +/

enum : ubyte { HOUR, MINUTE, SECOND }
enum :  byte { BAD, FOUND, NOTFOUND }

bool checkColon(T)(ref T* p, ref ubyte separators) {
   ubyte foundSep = accept(p, ':') ? YES : NO;
   if (foundSep != separators) {
      if (separators == WHATEVER)
         separators = foundSep;
      else
         return false;
   }
   return true;
}

byte getDecimal(T)(ref T* p, size_t len, ref FullDate fd, ubyte which) {
   if (!(accept(p, ',') || accept(p, '.')))
      return NOTFOUND;

   T* p2 = p;

   int i = void;
   auto iLen = parseInt(p, len-1, 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:
         addMins(fd, 6 * i / pow);
         addSecs(fd, 6 * i % pow);
         break;
      case MINUTE:
         addSecs(fd, 6    * i / pow);
         addMs  (fd, 6000 * i / pow % 1000);
         break;
      case SECOND:
         addMs(fd, 100 * i / pow);
         break;

      default: assert (false);
   }

   return FOUND;
}

// the DT is always UTC, so this just adds the offset to the date fields
// another option would be to add time zone fields to DT and have this fill them

byte getTimeZone(T)(ref const(T)* p, size_t len, ref FullDate fd, ubyte separators, bool delegate(const(T[])) done) {
   bool checkColon() { return .checkColon(p, separators); }

   if (len == 0)
      return NOTFOUND;

   auto p0 = p;

   if (accept(p, 'Z'))
      return FOUND;

   int factor = -1;

   if (accept(p, '-'))
      factor = 1;
   else if (!accept(p, '+'))
      return NOTFOUND;

   int hour = void;
   if (parseInt(p, min(cast(size_t)2, len-1), hour) != 2 || hour > 12 || (hour == 0 && factor == 1))
      return BAD;

   addHours(fd, factor * hour);

   // if we go forward in time to midnight, it's 24:00
   if (
      factor > 0 &&
      hours(fd) == 0 && mins(fd) == 0 && secs(fd) == 0 && ms(fd) == 0
   )
      fd.setEndOfDay();

   if (done("012345:"))
      return FOUND;

   auto afterHours = p;

   if (!checkColon())
      return BAD;

   int minute = void;
   if (parseInt(p, min(cast(size_t)2, len - (p-p0)), minute) != 2) {
      // The hours were valid even if the minutes weren't
      p = afterHours;
      return FOUND;
   }

   addMins(fd, factor * minute);

   // as above
   if (
      factor > 0 &&
      hours(fd) == 0 && mins(fd) == 0 && secs(fd) == 0 && ms(fd) == 0
   )
      fd.setEndOfDay();

   return FOUND;
}

/+ +++++++++++++++++++++++++++++++++++++++ +\

   Privates used by both date and time

\+ +++++++++++++++++++++++++++++++++++++++ +/

bool accept(T)(ref T* p, char c) {
   if (*p == c) {
      ++p;
      return true;
   }
   return false;
}

bool demand(T)(ref T* p, char c) {
   return (*p++ == c);
}

bool done(T)(size_t eaten, size_t srcLen, const(T*) p, const(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
   T t = *p;
   foreach (c; s) {
      if (t < c)
         return true;
      else if (t == c)
         break;
   }
   return false;
}

int daysPerMonth(int month, int year) {
   uint era = erafy(year);
   return Gregorian.generic.getDaysInMonth(year, month, era);
}

uint erafy(ref int year) {
   if (year < 0) {
      year *= -1;
      return Gregorian.BC_ERA;
   } else
      return Gregorian.AD_ERA;
}

/+ +++++++++++++++++++++++++++++++++++++++ +\

   Extract an integer from the input, accept no more than max digits

\+ +++++++++++++++++++++++++++++++++++++++ +/

// note: code relies on these always being positive, failing if *p == '-'

int parseInt(T)(ref T* p, size_t max) {
   size_t i = 0;
   int value = 0;
   while (i < max && p[i] >= '0' && p[i] <= '9')
      value = value * 10 + p[i++] - '0';
   p += i;
   return value;
}

// ... and return the amount of digits processed

size_t parseInt(T)(ref T* p, size_t max, out int i) {
   T* p2 = p;
   i = parseInt(p, max);
   return p - p2;
}


/+ +++++++++++++++++++++++++++++++++++++++ +\

   Helpers for DT/FullDate manipulation

\+ +++++++++++++++++++++++++++++++++++++++ +/

// as documented in tango.time.Time
bool DTyear(int year) { return year >= -10000 && year <= 9999; }

void addMonths(ref FullDate d, int n) { d.val = Gregorian.generic.addMonths(d.val, n-1); } // -1 due to initial being 1
void addDays  (ref FullDate d, int n) { d.val += TimeSpan.fromDays   (n-1); } // ditto
void addHours (ref FullDate d, int n) { d.val += TimeSpan.fromHours  (n); }
void addMins  (ref FullDate d, int n) { d.val += TimeSpan.fromMinutes(n); }
void addSecs  (ref FullDate d, int n) { d.val += TimeSpan.fromSeconds(n); }
void addMs    (ref FullDate d, int n) { d.val += TimeSpan.fromMillis (n); }

// years and secs always just get the DT value
int years (const(FullDate) d) { return Gregorian.generic.getYear      (d.val); }
int months(const(FullDate) d) { return Gregorian.generic.getMonth     (d.val); }
int days  (const(FullDate) d) { return Gregorian.generic.getDayOfMonth(d.val); }
int hours (const(FullDate) d) { return d.val.time.hours;   }
int mins  (const(FullDate) d) { return d.val.time.minutes; }
int secs  (const(FullDate) d) { return d.val.time.seconds; }
int ms    (const(FullDate) d) { return d.val.time.millis;  }

////////////////////

// Unit tests

debug (UnitTest) {
   // void main() {}

   debug (Tango_ISO8601_Valgrind) import tango.stdc.stdlib : malloc, free;

   unittest {
      FullDate fd;

      // date

      size_t d(const(char[]) s, ubyte e = 0) {
         fd = fd.init;
         return parseDate(s, fd, e);
      }

      auto
         INIT_YEAR  = years (FullDate.init),
         INIT_MONTH = months(FullDate.init),
         INIT_DAY   = days  (FullDate.init);

      assert (d("20abc") == 2);
      assert (years(fd) == 2000);

      assert (d("2004") == 4);
      assert (years(fd) == 2004);

      assert (d("+0019", 2) == 5);
      assert (years(fd) == 1900);

      assert (d("+111985", 2) == 7);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year   == 111985);

      assert (d("+111985", 1) == 6);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year   == 11198);

      assert (d("+111985", 3) == 0);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year   == INIT_YEAR);

      assert (d("+111985", 4) == 7);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year   == 11198500);

      assert (d("-111985", 5) == 0);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year   == INIT_YEAR);

      assert (d("+999999999", 5) == 10);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year == 999_999_999);

      try {
         d("+10000000000", 6);
         assert (false);
      } catch (IllegalArgumentException) {
         assert (years(fd) == INIT_YEAR);
         assert (fd.year   == INIT_YEAR);
      }

      assert (d("-999999999", 5) == 10);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year == -1_000_000_000);

      assert (d("0001") == 4);
      assert (years(fd) == 1);
      assert (fd.year   == 1);

      assert (d("0000") == 4);
      assert (fd.year   == -1);

      assert (d("-0001") == 5);
      assert (fd.year   == -2);

      assert (d("abc") == 0);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year   == INIT_YEAR);

      assert (d("abc123") == 0);
      assert (years(fd) == INIT_YEAR);
      assert (fd.year   == INIT_YEAR);

      assert (d("2007-08") == 7);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    8);

      assert (d("+001985-04", 2) == 10);
      assert (years(fd)  == 1985);
      assert (months(fd) ==    4);

      assert (d("2007-08-07") == 10);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    8);
      assert (days(fd)   ==    7);

      assert (d("2008-20-30") == 4);
      assert (years(fd)  == 2008);
      assert (months(fd) == INIT_MONTH);

      assert (d("2007-02-30") == 7);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    2);

      assert (d("20060708") == 8);
      assert (years(fd)  == 2006);
      assert (months(fd) ==    7);
      assert (days(fd)   ==    8);

      assert (d("19953080") == 4);
      assert (years(fd)  == 1995);
      assert (months(fd) == INIT_MONTH);

      assert (d("2007-0201") == 7);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    2);

      assert (d("200702-01") == 6);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    2);

      assert (d("+001985-04-12", 2) == 13);
      assert (years(fd)  == 1985);
      assert (fd.year    == 1985);
      assert (months(fd) ==    4);
      assert (days(fd)   ==   12);

      assert (d("-0123450607", 2) == 11);
      assert (years(fd)  == INIT_YEAR);
      assert (fd.year    == -12346);
      assert (months(fd) ==      6);
      assert (days(fd)   ==      7);

      assert (d("1985W15") == 7);
      assert (years(fd)  == 1985);
      assert (months(fd) ==    4);
      assert (days(fd)   ==    8);

      assert (d("2008-W01") == 8);
      assert (years(fd)  == 2007);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   31);

      assert (d("2008-W012") == 8);
      assert (years(fd)  == 2007);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   31);

      assert (d("2008W01-2") == 7);
      assert (years(fd)  == 2007);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   31);

      assert (d("2008-W01-2") == 10);
      assert (years(fd)  == 2008);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    1);

      assert (d("2009-W53-4") == 10);
      assert (years(fd)  == 2009);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   31);

      assert (d("2009-W01-1") == 10);
      assert (years(fd)  == 2008);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   29);

      assert (d("2009W537") == 8);
      assert (years(fd)  == 2010);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    3);

      assert (d("2010W537") == 4);
      assert (years(fd)  == 2010);
      assert (months(fd) == INIT_MONTH);

      assert (d("2009-W01-3") == 10);
      assert (years(fd)  == 2008);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   31);

      assert (d("2009-W01-4") == 10);
      assert (years(fd)  == 2009);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    1);

      assert (d("2004-W53-6") == 10);
      assert (years(fd)  == 2005);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    1);

      assert (d("2004-W53-7") == 10);
      assert (years(fd)  == 2005);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    2);

      assert (d("2005-W52-6") == 10);
      assert (years(fd)  == 2005);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   31);

      assert (d("2007-W01-1") == 10);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    1);

      assert (d("1000-W07-7") == 10);
      assert (years(fd)  == 1000);
      assert (months(fd) ==    2);
      assert (days(fd)   ==   16);

      assert (d("1500-W11-1") == 10);
      assert (years(fd)  == 1500);
      assert (months(fd) ==    3);
      assert (days(fd)   ==   12);

      assert (d("1700-W14-2") == 10);
      assert (years(fd)  == 1700);
      assert (months(fd) ==    4);
      assert (days(fd)   ==    6);

      assert (d("1800-W19-3") == 10);
      assert (years(fd)  == 1800);
      assert (months(fd) ==    5);
      assert (days(fd)   ==    7);

      assert (d("1900-W25-4") == 10);
      assert (years(fd)  == 1900);
      assert (months(fd) ==    6);
      assert (days(fd)   ==   21);

      assert (d("0900-W27-5") == 10);
      assert (years(fd)  ==  900);
      assert (months(fd) ==    7);
      assert (days(fd)   ==    9);

      assert (d("0800-W33-6") == 10);
      assert (years(fd)  ==  800);
      assert (months(fd) ==    8);
      assert (days(fd)   ==   19);

      assert (d("0700-W37-7") == 10);
      assert (years(fd)  ==  700);
      assert (months(fd) ==    9);
      assert (days(fd)   ==   16);

      assert (d("0600-W41-4") == 10);
      assert (years(fd)  ==  600);
      assert (months(fd) ==   10);
      assert (days(fd)   ==    9);

      assert (d("0500-W45-7") == 10);
      assert (years(fd)  ==  500);
      assert (months(fd) ==   11);
      assert (days(fd)   ==   14);

      assert (d("2000-W55") == 4);
      assert (years(fd) == 2000);

      assert (d("1980-002") == 8);
      assert (years(fd)  == 1980);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    2);

      assert (d("1981-034") == 8);
      assert (years(fd)  == 1981);
      assert (months(fd) ==    2);
      assert (days(fd)   ==    3);

      assert (d("1982-063") == 8);
      assert (years(fd)  == 1982);
      assert (months(fd) ==    3);
      assert (days(fd)   ==    4);

      assert (d("1983-095") == 8);
      assert (years(fd)  == 1983);
      assert (months(fd) ==    4);
      assert (days(fd)   ==    5);

      assert (d("1984-127") == 8);
      assert (years(fd)  == 1984);
      assert (months(fd) ==    5);
      assert (days(fd)   ==    6);

      assert (d("1985-158") == 8);
      assert (years(fd)  == 1985);
      assert (months(fd) ==    6);
      assert (days(fd)   ==    7);

      assert (d("1986-189") == 8);
      assert (years(fd)  == 1986);
      assert (months(fd) ==    7);
      assert (days(fd)   ==    8);

      assert (d("1987-221") == 8);
      assert (years(fd)  == 1987);
      assert (months(fd) ==    8);
      assert (days(fd)   ==    9);

      assert (d("1988-254") == 8);
      assert (years(fd)  == 1988);
      assert (months(fd) ==    9);
      assert (days(fd)   ==   10);

      assert (d("1989-284") == 8);
      assert (years(fd)  == 1989);
      assert (months(fd) ==   10);
      assert (days(fd)   ==   11);

      assert (d("1990316") == 7);
      assert (years(fd)  == 1990);
      assert (months(fd) ==   11);
      assert (days(fd)   ==   12);

      assert (d("1991-347") == 8);
      assert (years(fd)  == 1991);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   13);

      assert (d("1992-000") == 4);
      assert (years(fd) == 1992);

      assert (d("1993-370") == 4);
      assert (years(fd) == 1993);

      // time

      size_t t(const(char[]) s) {
         fd = fd.init;
         return parseTime(s, fd);
      }

      assert (t("20") == 2);
      assert (hours(fd) == 20);
      assert (mins(fd)  ==  0);
      assert (secs(fd)  ==  0);

      assert (t("30") == 0);

      assert (t("T15") == 3);
      assert (hours(fd) == 15);
      assert (mins(fd)  ==  0);
      assert (secs(fd)  ==  0);

      assert (t("T1") == 0);
      assert (t("T") == 0);

      assert (t("2004") == 4);
      assert (hours(fd) == 20);
      assert (mins(fd)  ==  4);
      assert (secs(fd)  ==  0);

      assert (t("200406") == 6);
      assert (hours(fd) == 20);
      assert (mins(fd)  ==  4);
      assert (secs(fd)  ==  6);

      assert (t("24:00") == 5);
      assert (fd.endOfDay);
      assert (days(fd)  == INIT_DAY + 1);
      assert (hours(fd) == 0);
      assert (mins(fd)  == 0);
      assert (secs(fd)  == 0);

      assert (t("00:00") == 5);
      assert (hours(fd) == 0);
      assert (mins(fd)  == 0);
      assert (secs(fd)  == 0);

      assert (t("23:59:60") == 8);
      assert (hours(fd)  == 23);
      assert (mins(fd)   == 59);
      assert (secs(fd)   == 59);
      assert (fd.seconds == 60);

      assert (t("12:3456") == 5);
      assert (hours(fd) == 12);
      assert (mins(fd)  == 34);

      assert (t("1234:56") == 4);
      assert (hours(fd) == 12);
      assert (mins(fd)  == 34);

      assert (t("16:49:30,001") == 12);
      assert (hours(fd) == 16);
      assert (mins(fd)  == 49);
      assert (secs(fd)  == 30);
      assert (ms(fd)    ==  1);

      assert (t("15:48:29,1") == 10);
      assert (hours(fd) ==  15);
      assert (mins(fd)  ==  48);
      assert (secs(fd)  ==  29);
      assert (ms(fd)    == 100);

      assert (t("02:10:34,a") ==  8);
      assert (hours(fd) ==  2);
      assert (mins(fd)  == 10);
      assert (secs(fd)  == 34);

      assert (t("14:50,5") == 7);
      assert (hours(fd) == 14);
      assert (mins(fd)  == 50);
      assert (secs(fd)  == 30);

      assert (t("1540,4") == 6);
      assert (hours(fd) == 15);
      assert (mins(fd)  == 40);
      assert (secs(fd)  == 24);

      assert (t("1250,") == 4);
      assert (hours(fd) == 12);
      assert (mins(fd)  == 50);

      assert (t("14,5") == 4);
      assert (hours(fd) == 14);
      assert (mins(fd)  == 30);

      assert (t("12,") == 2);
      assert (hours(fd) == 12);
      assert (mins(fd)  ==  0);

      assert (t("24:00:01") == 5);
      assert (fd.endOfDay);
      assert (hours(fd) == 0);
      assert (mins(fd)  == 0);
      assert (secs(fd)  == 0);

      assert (t("12:34+:56") == 5);
      assert (hours(fd) == 12);
      assert (mins(fd)  == 34);
      assert (secs(fd)  ==  0);

      // time zones

      assert (t("14:45:15Z") == 9);
      assert (hours(fd) == 14);
      assert (mins(fd)  == 45);
      assert (secs(fd)  == 15);

      assert (t("23Z") == 3);
      assert (hours(fd) == 23);
      assert (mins(fd)  ==  0);
      assert (secs(fd)  ==  0);

      assert (t("21:32:43-12:34") == 14);
      assert (days(fd)  == INIT_DAY + 1);
      assert (hours(fd) == 10);
      assert (mins(fd)  ==  6);
      assert (secs(fd)  == 43);

      assert (t("12:34,5+00:00") == 13);
      assert (hours(fd) == 12);
      assert (mins(fd)  == 34);
      assert (secs(fd)  == 30);

      assert (t("03:04+07") == 8);
      assert (hours(fd) == 20);
      assert (mins(fd)  ==  4);
      assert (secs(fd)  ==  0);

      assert (t("11,5+") == 4);
      assert (hours(fd) == 11);
      assert (mins(fd)  == 30);

      assert (t("07-") == 2);
      assert (hours(fd) == 7);

      assert (t("06:12,7-") == 7);
      assert (hours(fd) ==  6);
      assert (mins(fd)  == 12);
      assert (secs(fd)  == 42);

      assert (t("050403,2+") == 8);
      assert (hours(fd) ==   5);
      assert (mins(fd)  ==   4);
      assert (secs(fd)  ==   3);
      assert (ms(fd)    == 200);

      assert (t("061656-") == 6);
      assert (hours(fd) ==  6);
      assert (mins(fd)  == 16);
      assert (secs(fd)  == 56);

      // date and time together

      size_t b(const(char[]) s) {
         fd = fd.init;
         return parseDateAndTime(s, fd);
      }

      assert (b("2007-08-09T12:34:56") == 19);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    8);
      assert (days(fd)   ==    9);
      assert (hours(fd)  ==   12);
      assert (mins(fd)   ==   34);
      assert (secs(fd)   ==   56);

      assert (b("1985W155T235030,768") == 19);
      assert (years(fd)  == 1985);
      assert (months(fd) ==    4);
      assert (days(fd)   ==   12);
      assert (hours(fd)  ==   23);
      assert (mins(fd)   ==   50);
      assert (secs(fd)   ==   30);
      assert (ms(fd)     ==  768);

      // time zones

      assert (b("2009-08-07T01:02:03Z") == 20);
      assert (years(fd)  == 2009);
      assert (months(fd) ==    8);
      assert (days(fd)   ==    7);
      assert (hours(fd)  ==    1);
      assert (mins(fd)   ==    2);
      assert (secs(fd)   ==    3);

      assert (b("2007-08-09T03:02,5+04:56") == 24);
      assert (years(fd)  == 2007);
      assert (months(fd) ==    8);
      assert (days(fd)   ==    8);
      assert (hours(fd)  ==   22);
      assert (mins(fd)   ==    6);
      assert (secs(fd)   ==   30);

      assert (b("20000228T2330-01") == 16);
      assert (years(fd)  == 2000);
      assert (months(fd) ==    2);
      assert (days(fd)   ==   29);
      assert (hours(fd)  ==    0);
      assert (mins(fd)   ==   30);
      assert (secs(fd)   ==    0);

      assert (b("2007-01-01T00:00+01") == 19);
      assert (years(fd)  == 2006);
      assert (months(fd) ==   12);
      assert (days(fd)   ==   31);
      assert (hours(fd)  ==   23);
      assert (mins(fd)   ==    0);
      assert (secs(fd)   ==    0);

      assert (b("2007-12-31T23:00-01") == 19);
      assert (fd.endOfDay);
      assert (years(fd)  == 2008);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    1);
      assert (hours(fd)  ==    0);
      assert (mins(fd)   ==    0);
      assert (secs(fd)   ==    0);

      assert (b("2007-12-31T23:01-01") == 19);
      assert (!fd.endOfDay);
      assert (years(fd)  == 2008);
      assert (months(fd) ==    1);
      assert (days(fd)   ==    1);
      assert (hours(fd)  ==    0);
      assert (mins(fd)   ==    1);
      assert (secs(fd)   ==    0);

      assert (b("1902-03-04T1a") == 0);
      assert (b("1902-03-04T10:aa") == 0);
      assert (b("1902-03-04T10:1aa") == 0);
      assert (b("200512-01T10:02") == 0);
      assert (b("1985-04-1210:15:30+04:00") == 0);
      assert (b("1985-04-12T10:15:30+0400") == 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);

      assert (b("1985-04-12T10:15:30-05:4") == 22);
      assert (years(fd)  == 1985);
      assert (months(fd) ==    4);
      assert (days(fd)   ==   12);
      assert (hours(fd)  ==   15);
      assert (mins(fd)   ==   15);
      assert (secs(fd)   ==   30);
      assert (b("2009-04-13T23:00-01") == 19);
      assert (fd.endOfDay);
      assert (years(fd)  == 2009);
      assert (months(fd) ==    4);
      assert (days(fd)   ==   14);
      assert (hours(fd)  ==    0);
      assert (mins(fd)   ==    0);
      assert (secs(fd)   ==    0);
      assert (b("2009-04-13T24:00Z") == 17);
      assert (fd.endOfDay);
      assert (years(fd)  == 2009);
      assert (months(fd) ==    4);
      assert (days(fd)   ==   14);
      assert (hours(fd)  ==    0);
      assert (mins(fd)   ==    0);
      assert (secs(fd)   ==    0);

      // unimplemented: intervals, durations, recurring intervals

      debug (Tango_ISO8601_Valgrind) {
         size_t valgrind(size_t delegate(const(char[])) f, const(char[]) s) {
            auto p = cast(char*)malloc(s.length);
            auto ps = p[0..s.length];
            ps[] = s[];
            auto result = f(ps);
            free(p);
            return result;
         }
         size_t vd(const(char[]) s) {
            size_t date(const(char[]) ss) { return d(ss); }
            return valgrind(&date, s);
         }
         size_t vt(const(char[]) s) { return valgrind(&t, s); }
         size_t vb(const(char[]) s) { return valgrind(&b, s); }

         assert (vd("1") == 0);
         assert (vd("19") == 2);
         assert (vd("199") == 0);
         assert (vd("1999") == 4);
         assert (vd("1999-") == 4);
         assert (vd("1999-W") == 4);
         assert (vd("1999-W0") == 4);
         assert (vd("1999-W01") == 8);
         assert (vd("1999-W01-") == 8);
         assert (vd("1999-W01-3") == 10);
         assert (vd("1999W") == 4);
         assert (vd("1999W0") == 4);
         assert (vd("1999W01") == 7);
         assert (vd("1999W01-") == 7);
         assert (vd("1999W01-3") == 7);
         assert (vd("1999W013") == 8);
         assert (vd("1999-0") == 4);
         assert (vd("1999-01") == 7);
         assert (vd("1999-01-") == 7);
         assert (vd("1999-01-0") == 7);
         assert (vd("1999-01-01") == 10);
         assert (vd("1999-0101") == 7);
         assert (vd("1999-365") == 8);
         assert (vd("1999365") == 7);

         assert (vt("1") == 0);
         assert (vt("15") == 2);
         assert (vt("15:") == 2);
         assert (vt("15:3") == 2);
         assert (vt("15:30") == 5);
         assert (vt("153") == 2);
         assert (vt("1530") == 4);
         assert (vt("1530:") == 4);
         assert (vt("15304") == 4);
         assert (vt("153045") == 6);
         assert (vt("15:30:") == 5);
         assert (vt("15:30:4") == 5);
         assert (vt("15:30:45") == 8);
         assert (vt("T15") == 3);
         assert (vt("T1") == 0);
         assert (vt("T") == 0);
         assert (vt("15,") == 2);
         assert (vt("15,2") == 4);
         assert (vt("1530,") == 4);
         assert (vt("1530,2") == 6);
         assert (vt("15:30:45,") == 8);
         assert (vt("15:30:45,2") == 10);
         assert (vt("153045,") == 6);
         assert (vt("153045,2") == 8);
         assert (vt("153045,22") == 9);
         assert (vt("153045,222") == 10);
         assert (vt("15Z") == 3);
         assert (vt("15+") == 2);
         assert (vt("15-") == 2);
         assert (vt("15+0") == 2);
         assert (vt("15+00") == 5);
         assert (vt("15+00:") == 5);
         assert (vt("15+00:0") == 5);
         assert (vt("15+00:00") == 8);
         assert (vb("1999-01-01") == 0);
         assert (vb("1999-01-01T") == 0);
         assert (vb("1999-01-01T15:30:45") == 19);
      }
   }
}