[glib/wip/rancell/iso8601: 507/507] GDateTime: Support parsing ISO 8601 strings.



commit d693b46d9ce62faddcd4bf49bfe68a2c9ce1325c
Author: Robert Ancell <robert ancell canonical com>
Date:   Thu Aug 25 11:53:54 2016 +1200

    GDateTime: Support parsing ISO 8601 strings.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=753459

 glib/gdatetime.c       |  450 ++++++++++++++++++++++++++++++++++++++++++++++++
 glib/gdatetime.h       |   45 +++++
 glib/tests/gdatetime.c |  329 +++++++++++++++++++++++++++++++++++
 3 files changed, 824 insertions(+), 0 deletions(-)
---
diff --git a/glib/gdatetime.c b/glib/gdatetime.c
index 745a32a..50ebaff 100644
--- a/glib/gdatetime.c
+++ b/glib/gdatetime.c
@@ -22,6 +22,7 @@
  *          Thiago Santos <thiago sousa santos collabora co uk>
  *          Emmanuele Bassi <ebassi linux intel com>
  *          Ryan Lortie <desrt desrt ca>
+ *          Robert Ancell <robert ancell canonical com>
  */
 
 /* Algorithms within this file are based on the Calendar FAQ by
@@ -902,6 +903,455 @@ g_date_time_new_from_timeval_utc (const GTimeVal *tv)
   return datetime;
 }
 
+static gboolean
+get_iso8601_int (const gchar *text, gint length, gint *value)
+{
+    gint i, v = 0;
+
+    for (i = 0; i < length; i++)
+      {
+        gchar c = text[i];
+        if (c < '0' || c > '9')
+          return FALSE;
+        v = v * 10 + (c - '0');
+      }
+
+    *value = v;
+    return TRUE;
+}
+
+static gboolean
+get_iso8601_seconds (const gchar *text, gint length, gdouble *value, GDateTimeParseFlags *flags)
+{
+    gint i;
+    gdouble multiplier = 0.1, v = 0;
+
+    for (i = 0; i < length; i++)
+      {
+        gchar c = text[i];
+        if (c == '.' || c == ',')
+          {
+            i++;
+            *flags |= G_DATE_TIME_PARSE_SUBSECOND;
+            break;
+          }
+        if (c < '0' || c > '9')
+          return FALSE;
+        v = v * 10 + (c - '0');
+      }
+
+    for (; i < length; i++)
+      {
+        gchar c = text[i];
+        if (c < '0' || c > '9')
+          return FALSE;
+        v += (c - '0') * multiplier;
+        multiplier *= 0.1;
+      }
+
+    *value = v;
+    return TRUE;
+}
+
+static gboolean
+convert_from_iso8601_ordinal (gint year, gint ordinal_day, gint *month, gint *day)
+{
+    gint m;
+
+    if (ordinal_day < 1)
+      return FALSE;
+
+    for (m = 1; m <= 12; m++)
+      {
+        if (ordinal_day <= days_in_year[GREGORIAN_LEAP (year)][m])
+          {
+            *month = m;
+            *day = ordinal_day - days_in_year[GREGORIAN_LEAP (year)][m - 1];
+            return TRUE;
+          }
+      }
+
+    return FALSE;
+}
+
+static gboolean
+convert_from_iso8601_week (gint year, gint week, gint week_day, gint *offset)
+{
+    gint days, week_offset;
+
+    if (week < 1 || week > 52 || week_day < 1 || week_day > 7)
+      return FALSE;
+
+    /* Work out the day week one starts on */
+    days = ymd_to_days (year, 1, 1);
+    week_offset = -(days % 7);
+    if (week_offset < -3)
+      week_offset += 7;
+
+    *offset = week_offset + ((week - 1) * 7) + week_day;
+    return TRUE;
+}
+
+static gboolean
+parse_iso8601_date (const gchar *text, gint length,
+                    gint *year, gint *month, gint *day, gint *offset,
+                    GDateTimeParseFlags *flags)
+{
+    /* YYYY-MM-DD */
+    if (length == 10 && text[4] == '-' && text[7] == '-')
+      {
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 5, 2, month) &&
+               get_iso8601_int (text + 8, 2, day);
+      }
+    /* YYYY-Www */
+    else if (length == 8 && text[4] == '-' && text[5] == 'W')
+      {
+        gint week;
+        *month = 1;
+        *day = 1;
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 6, 2, &week) &&
+               convert_from_iso8601_week (*year, week, 1, offset);
+      }
+    /* YYYY-DDD */
+    else if (length == 8 && text[4] == '-')
+      {
+        gint ordinal_day;
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_ORDINAL_DAY;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 5, 3, &ordinal_day) &&
+               convert_from_iso8601_ordinal (*year, ordinal_day, month, day);
+      }
+    /* YYYY-Www-D */
+    else if (length == 10 && text[4] == '-' && text[5] == 'W' && text[8] == '-')
+      {
+        gint week, week_day;
+        *month = 1;
+        *day = 1;
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK | G_DATE_TIME_PARSE_WEEK_DAY;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 6, 2, &week) &&
+               get_iso8601_int (text + 9, 1, &week_day) &&
+               convert_from_iso8601_week (*year, week, week_day, offset);
+      }
+    /* YYYYWwwD */
+    else if (length == 8 && text[4] == 'W')
+      {
+        gint week, week_day;
+        *month = 1;
+        *day = 1;
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK | G_DATE_TIME_PARSE_WEEK_DAY;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 5, 2, &week) &&
+               get_iso8601_int (text + 7, 1, &week_day) &&
+               convert_from_iso8601_week (*year, week, week_day, offset);
+      }
+    /* YYYYMMDD */
+    else if (length == 8)
+      {
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 4, 2, month) &&
+               get_iso8601_int (text + 6, 2, day);
+      }
+    /* --MM-DD */
+    else if (length == 7 && text[0] == '-' && text[1] == '-' && text[4] == '-')
+      {
+        *flags |= G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY;
+        return get_iso8601_int (text + 2, 2, month) &&
+               get_iso8601_int (text + 5, 2, day);
+      }
+    /* YYYY-MM */
+    else if (length == 7 && text[4] == '-')
+      {
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 5, 2, month);
+      }
+    /* YYYYWww */
+    else if (length == 7 && text[4] == 'W')
+      {
+        gint week;
+        *month = 1;
+        *day = 1;
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 5, 2, &week) &&
+               convert_from_iso8601_week (*year, week, 1, offset);
+      }
+    /* YYYYDDD */
+    else if (length == 7)
+      {
+        gint ordinal_day;
+        *flags |= G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_ORDINAL_DAY;
+        return get_iso8601_int (text, 4, year) &&
+               get_iso8601_int (text + 4, 3, &ordinal_day) &&
+               convert_from_iso8601_ordinal (*year, ordinal_day, month, day);
+      }
+    /* --MMDD */
+    else if (length == 6 && text[0] == '-' && text[1] == '-')
+      {
+        *flags |= G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY;
+        return get_iso8601_int (text + 2, 2, month) &&
+               get_iso8601_int (text + 4, 2, day);
+      }
+    else
+      return FALSE;
+}
+
+/* Check if text has Unicode minus symbol prefix (−) */
+static gboolean
+has_minus_prefix (const gchar *text)
+{
+    return (guint8) text[0] == 0xe2 && (guint8) text[1] == 0x88 && (guint8) text[2] == 0x92;
+}
+
+static gboolean
+parse_iso8601_timezone (const gchar *text, gint length, GTimeZone **tz)
+{
+  gint offset_hours, offset_minutes;
+
+  /* Z */
+  if (length == 1 && text[0] == 'Z')
+    {
+      offset_hours = 0;
+      offset_minutes = 0;
+    }
+  /* +hh:mm or -hh:mm */
+  else if (length == 6 && (text[0] == '+' || text[0] == '-') && text[3] == ':')
+    {
+      if (!get_iso8601_int (text + 1, 2, &offset_hours) ||
+          !get_iso8601_int (text + 4, 2, &offset_minutes))
+          return FALSE;
+    }
+  /* −hh:mm */
+  else if (length == 8 && has_minus_prefix (text) && text[5] == ':')
+    {
+      if (!get_iso8601_int (text + 3, 2, &offset_hours) ||
+          !get_iso8601_int (text + 6, 2, &offset_minutes))
+          return FALSE;
+    }
+  /* +hhmm or -hhmm */
+  else if (length == 5 && (text[0] == '+' || text[0] == '-'))
+    {
+      if (!get_iso8601_int (text + 1, 2, &offset_hours) ||
+          !get_iso8601_int (text + 3, 2, &offset_minutes))
+          return FALSE;
+    }
+  /* −hhmm */
+  else if (length == 7 && has_minus_prefix (text))
+    {
+      if (!get_iso8601_int (text + 3, 2, &offset_hours) ||
+          !get_iso8601_int (text + 5, 2, &offset_minutes))
+          return FALSE;
+    }
+  /* +hh or -hh */
+  else if (length == 3 && (text[0] == '+' || text[0] == '-'))
+    {
+      if (!get_iso8601_int (text + 1, 2, &offset_hours))
+          return FALSE;
+      offset_minutes = 0;
+    }
+  /* −hh */
+  else if (length == 5 && has_minus_prefix (text))
+    {
+      if (!get_iso8601_int (text + 3, 2, &offset_hours))
+          return FALSE;
+      offset_minutes = 0;
+    }
+  else
+    return FALSE;
+
+  *tz = g_time_zone_new (text);
+
+  return TRUE;
+}
+
+static gboolean
+parse_iso8601_time (const gchar *text, gint length,
+                    gint *hour, gint *minute, gdouble *seconds, GTimeZone **tz,
+                    GDateTimeParseFlags *flags)
+{
+    gint i;
+
+    /* Check for timezone suffix */
+    for (i = 0; i < length; i++)
+      {
+        if (parse_iso8601_timezone (text + i, length - i, tz))
+          {
+            *flags |= G_DATE_TIME_PARSE_TIMEZONE;
+            length = i;
+            break;
+          }
+      }
+
+    /* hh:mm:ss(.sss) */
+    if (length >= 8 && text[2] == ':' && text[5] == ':')
+      {
+        *flags |= G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND;
+        return get_iso8601_int (text, 2, hour) &&
+               get_iso8601_int (text + 3, 2, minute) &&
+               get_iso8601_seconds (text + 6, length - 6, seconds, flags);
+      }
+    /* hhmmss(.sss) */
+    else if (length >= 6)
+      {
+        *flags |= G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND;
+        return get_iso8601_int (text, 2, hour) &&
+               get_iso8601_int (text + 2, 2, minute) &&
+               get_iso8601_seconds (text + 4, length - 4, seconds, flags);
+      }
+    /* hh:mm */
+    else if (length == 5 && text[2] == ':')
+      {
+        *flags |= G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE;
+        return get_iso8601_int (text, 2, hour) &&
+               get_iso8601_int (text + 3, 2, minute);
+      }
+    /* hhmm */
+    else if (length == 4)
+      {
+        *flags |= G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE;
+        return get_iso8601_int (text, 2, hour) &&
+               get_iso8601_int (text + 2, 2, minute);
+      }
+    /* hh */
+    else if (length == 2)
+      {
+        *flags |= G_DATE_TIME_PARSE_HOUR;
+        return get_iso8601_int (text, 2, hour);
+      }
+    else
+        return FALSE;
+}
+
+static GDateTime *
+parse_iso8601 (const gchar *text, gint year, gint month, gint day, GDateTimeParseFlags *flags)
+{
+    gint length, date_length = -1;
+    gint y = year, m = month, d = day, offset = 0, hour = 0, minute = 0;
+    gdouble seconds = 0.0;
+    GTimeZone *tz = NULL;
+    GDateTime *datetime = NULL;
+
+    /* <date>T<time> */
+    for (length = 0; text[length] != '\0'; length++)
+      {
+        if (date_length < 0 && text[length] == 'T')
+          date_length = length;
+      }
+
+    if (date_length < 0)
+      {
+        if (!parse_iso8601_date (text, length, &y, &m, &d, &offset, flags))
+          {
+            *flags = 0;
+            if (!parse_iso8601_time (text, length, &hour, &minute, &seconds, &tz, flags))
+              goto out;
+          }
+      }
+    else
+      {
+        if (!parse_iso8601_date (text, date_length, &y, &m, &d, &offset, flags) ||
+            !parse_iso8601_time (text + date_length + 1, length - (date_length + 1),
+                                 &hour, &minute, &seconds, &tz, flags))
+          goto out;
+      }
+
+    if (tz == NULL)
+      tz = g_time_zone_new_local ();
+    datetime = g_date_time_new (tz, y, m, d, hour, minute, seconds);
+    if (datetime != NULL && offset != 0)
+      datetime->days += offset;
+
+out:
+    if (tz != NULL)
+      g_time_zone_unref (tz);
+    return datetime;
+}
+
+/**
+ * g_date_time_new_from_iso8601:
+ * @text: an ISO 8601 formatted time string.
+ * @flags: (allow-none): location to write which fields were parsed to generate
+ * this #GDateTime or %NULL.
+ *
+ * Creates a #GDateTime corresponding to the given ISO 8601 formatted string
+ * @text.
+ *
+ * This call can fail (returning %NULL) if @text is not a valid ISO 8601
+ * formatted string.
+ *
+ * You should release the return value by calling g_date_time_unref()
+ * when you are done with it.
+ *
+ * Returns: a new #GDateTime, or %NULL
+ *
+ * Since: 2.50
+ **/
+GDateTime *
+g_date_time_new_from_iso8601 (const gchar         *text,
+                              GDateTimeParseFlags *flags)
+{
+    GDateTime *datetime;
+    GDateTimeParseFlags f = 0;
+
+    g_return_val_if_fail (text != NULL, NULL);
+
+    datetime = parse_iso8601 (text, 0, 0, 0, &f);
+
+    if (flags != NULL)
+      *flags = f;
+    return datetime;
+}
+
+/**
+ * g_date_time_new_from_iso8601_with_year:
+ * @text: an ISO 8601 formatted time string.
+ * @year: the year to use if @text does not specify one.
+ * @month: the month to use if @text does not specify one.
+ * @day: the month to use if @text does not specify one.
+ * @flags: (allow-none): location to write which fields were parsed to generate
+ * this #GDateTime or %NULL.
+ *
+ * Creates a #GDateTime corresponding to the given ISO 8601 formatted string
+ * @text.
+ *
+ * ISO 8601 strings that do not contain dates are times (e.g. "22:10:42") or
+ * dates with just the month and day, e.g. "--08-24".
+ *
+ * This call can fail (returning %NULL) if @text is not a valid ISO 8601
+ * formatted string.
+ *
+ * You should release the return value by calling g_date_time_unref()
+ * when you are done with it.
+ *
+ * Returns: a new #GDateTime, or %NULL
+ *
+ * Since: 2.50
+ **/
+GDateTime *
+g_date_time_new_from_iso8601_with_date (const gchar         *text,
+                                        gint                 year,
+                                        gint                 month,
+                                        gint                 day,
+                                        GDateTimeParseFlags *flags)
+{
+    GDateTime *datetime;
+    GDateTimeParseFlags f = 0;
+
+    g_return_val_if_fail (text != NULL, NULL);
+
+    datetime = parse_iso8601 (text, year, month, day, &f);
+
+    if (flags != NULL)
+      *flags = f;
+    return datetime;
+}
+
 /* full new functions {{{1 */
 
 /**
diff --git a/glib/gdatetime.h b/glib/gdatetime.h
index 927e29c..3297f50 100644
--- a/glib/gdatetime.h
+++ b/glib/gdatetime.h
@@ -96,6 +96,41 @@ typedef gint64 GTimeSpan;
  */
 typedef struct _GDateTime GDateTime;
 
+/**
+ * GDateTimeParseFlags:
+ * @G_DATE_TIME_PARSE_YEAR: the parsed date includes the year.
+ * @G_DATE_TIME_PARSE_MONTH: the parsed date includes the month.
+ * @G_DATE_TIME_PARSE_DAY: the parsed date includes the day.
+ * @G_DATE_TIME_PARSE_WEEK: the parsed date is calculated from the week number.
+ * @G_DATE_TIME_PARSE_WEEK_DAY: the parsed date is calculated from the week.
+ *     number and day of the week. (Implies G_DATE_TIME_PARSE_WEEK).
+ * @G_DATE_TIME_PARSE_ORDINAL_DAY: the parsed date is calculated from the
+       ordinal day (i.e. 1-365/366).
+ * @G_DATE_TIME_PARSE_HOUR: the parsed date includes the hour.
+ * @G_DATE_TIME_PARSE_MINUTE: the parsed date includes the minute.
+ * @G_DATE_TIME_PARSE_SECOND: the parsed date includes the second.
+ * @G_DATE_TIME_PARSE_SUBSECOND: the parsed date includes the subsecond.
+ * @G_DATE_TIME_PARSE_TIMEZONE: the parsed date includes a timezone.
+ *
+ * Flags used in g_date_time_new_from_iso8601 () and
+ * g_date_time_new_from_iso8601_with_date () to indicate what fields were
+ * parsed from the ISO 8601 string.
+ */
+typedef enum
+{
+    G_DATE_TIME_PARSE_YEAR        = 1 << 0,
+    G_DATE_TIME_PARSE_MONTH       = 1 << 1,
+    G_DATE_TIME_PARSE_DAY         = 1 << 2,
+    G_DATE_TIME_PARSE_WEEK        = 1 << 3,
+    G_DATE_TIME_PARSE_WEEK_DAY    = 1 << 4,
+    G_DATE_TIME_PARSE_ORDINAL_DAY = 1 << 5,
+    G_DATE_TIME_PARSE_HOUR        = 1 << 6,
+    G_DATE_TIME_PARSE_MINUTE      = 1 << 7,
+    G_DATE_TIME_PARSE_SECOND      = 1 << 8,
+    G_DATE_TIME_PARSE_SUBSECOND   = 1 << 9,
+    G_DATE_TIME_PARSE_TIMEZONE    = 1 << 10
+} GDateTimeParseFlags;
+
 GLIB_AVAILABLE_IN_ALL
 void                    g_date_time_unref                               (GDateTime      *datetime);
 GLIB_AVAILABLE_IN_ALL
@@ -118,6 +153,16 @@ GDateTime *             g_date_time_new_from_timeval_local              (const G
 GLIB_AVAILABLE_IN_ALL
 GDateTime *             g_date_time_new_from_timeval_utc                (const GTimeVal *tv);
 
+GLIB_AVAILABLE_IN_2_50
+GDateTime *             g_date_time_new_from_iso8601                    (const gchar         *text,
+                                                                         GDateTimeParseFlags *flags);
+GLIB_AVAILABLE_IN_2_50
+GDateTime *             g_date_time_new_from_iso8601_with_date          (const gchar         *text,
+                                                                         gint                 year,
+                                                                         gint                 month,
+                                                                         gint                 day,
+                                                                         GDateTimeParseFlags *flags);
+
 GLIB_AVAILABLE_IN_ALL
 GDateTime *             g_date_time_new                                 (GTimeZone      *tz,
                                                                          gint            year,
diff --git a/glib/tests/gdatetime.c b/glib/tests/gdatetime.c
index 1a9e3fb..ef471b5 100644
--- a/glib/tests/gdatetime.c
+++ b/glib/tests/gdatetime.c
@@ -482,6 +482,334 @@ test_GDateTime_new_from_timeval_utc (void)
 }
 
 static void
+test_GDateTime_new_from_iso8601 (void)
+{
+  GDateTime *dt;
+  GDateTimeParseFlags flags;
+
+  /* Need non-empty string */
+  dt = g_date_time_new_from_iso8601 ("", NULL);
+  g_assert (dt == NULL);
+
+  /* Needs to be correctly formatted */
+  dt = g_date_time_new_from_iso8601 ("not a date", NULL);
+  g_assert (dt == NULL);
+
+  /* Can't have whitespace */
+  dt = g_date_time_new_from_iso8601 ("2016 08 24", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24 ", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 (" 2016-08-24", &flags);
+  g_assert (dt == NULL);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("20160824", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY);
+  g_date_time_unref (dt);
+
+  /* Months are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-1-01", NULL);
+  g_assert (dt == NULL);
+
+  /* Days are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-01-1", NULL);
+  g_assert (dt == NULL);
+
+  /* Need consistent usage of separators */
+  dt = g_date_time_new_from_iso8601 ("2016-0824", NULL);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("201608-24", NULL);
+  g_assert (dt == NULL);
+
+  /* Check month within valid range */
+  dt = g_date_time_new_from_iso8601 ("2016-00-13", NULL);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("2016-13-13", NULL);
+  g_assert (dt == NULL);
+
+  /* Check day within valid range */
+  dt = g_date_time_new_from_iso8601 ("2016-01-00", NULL);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("2016-01-32", NULL);
+  g_assert (dt == NULL);
+
+  dt = g_date_time_new_from_iso8601 ("2016-237", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_ORDINAL_DAY);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016237", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_ORDINAL_DAY);
+  g_date_time_unref (dt);
+
+  /* Days start at 1 */
+  dt = g_date_time_new_from_iso8601 ("2016-000", NULL);
+  g_assert (dt == NULL);
+
+  /* Limited to number of days in the year (2016 is a leap year) */
+  dt = g_date_time_new_from_iso8601 ("2016-367", NULL);
+  g_assert (dt == NULL);
+
+  /* Days are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-1", NULL);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("2016-12", NULL);
+  g_assert (dt == NULL);
+
+  dt = g_date_time_new_from_iso8601 ("2016-W34", &flags);
+  ASSERT_DATE (dt, 2016, 8, 22);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016W34", &flags);
+  ASSERT_DATE (dt, 2016, 8, 22);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK);
+  g_date_time_unref (dt);
+
+  /* Weeks are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-W3", NULL);
+  g_assert (dt == NULL);
+
+  /* Weeks start at 1 */
+  dt = g_date_time_new_from_iso8601 ("2016-W00", NULL);
+  g_assert (dt == NULL);
+
+  /* Limited to number of weeks in the year */
+  dt = g_date_time_new_from_iso8601 ("2016-W53", NULL);
+  g_assert (dt == NULL);
+
+  /* Check week day changes depending on year */
+  dt = g_date_time_new_from_iso8601 ("2017W34", &flags);
+  ASSERT_DATE (dt, 2017, 8, 21);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK);
+  g_date_time_unref (dt);
+
+  /* Check week day changes depending on leap years */
+  dt = g_date_time_new_from_iso8601 ("1900W01", &flags);
+  ASSERT_DATE (dt, 1900, 1, 1);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-W34-3", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK | G_DATE_TIME_PARSE_WEEK_DAY);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016W343", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_WEEK | G_DATE_TIME_PARSE_WEEK_DAY);
+  g_date_time_unref (dt);
+
+  /* Limited to number of days in the week */
+  dt = g_date_time_new_from_iso8601 ("2016-W34-0", NULL);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("2016-W34-8", NULL);
+  g_assert (dt == NULL);
+
+  /* Days are one digit */
+  dt = g_date_time_new_from_iso8601 ("2016-W34-99", NULL);
+  g_assert (dt == NULL);
+
+  /* YYYY-MM not allowed */
+  dt = g_date_time_new_from_iso8601 ("2016-08", NULL);
+  g_assert (dt == NULL);
+
+  dt = g_date_time_new_from_iso8601_with_date ("--08-24", 2016, 0, 0, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("--0824", 2016, 0, 0, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 0, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("22", 2016, 8, 24, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_HOUR);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("22:10", 2016, 8, 24, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("22:10:42", 2016, 8, 24, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("22:10:42.123456", 2016, 8, 24, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42.123456);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND | 
G_DATE_TIME_PARSE_SUBSECOND);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("2210", 2016, 8, 24, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("221042", 2016, 8, 24, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601_with_date ("221042.123456", 2016, 8, 24, &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42.123456);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND | 
G_DATE_TIME_PARSE_SUBSECOND);
+  g_date_time_unref (dt);
+
+  /* Can't parse time without date */
+  dt = g_date_time_new_from_iso8601 ("22", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("22:10", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("22:10:42", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("22:10:42.123456", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("2210", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("221042", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("221042.123456", &flags);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42.123456", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42.123456);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND | 
G_DATE_TIME_PARSE_SUBSECOND);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T2210", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T221042", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T221042.123456", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42.123456);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_MINUTE | G_DATE_TIME_PARSE_SECOND | 
G_DATE_TIME_PARSE_SUBSECOND);
+  g_date_time_unref (dt);
+
+  /* UTC time uses 'Z' */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22Z", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, 0);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_TIMEZONE);
+  g_date_time_unref (dt);
+
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22+12:00", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, 12 * G_TIME_SPAN_HOUR);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_TIMEZONE);
+  g_date_time_unref (dt);
+
+  /* Check negative values */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22-02", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, -2 * G_TIME_SPAN_HOUR);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_TIMEZONE);
+  g_date_time_unref (dt);
+
+  /* Check negative values using Unicode character − */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22−02:00", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, -2 * G_TIME_SPAN_HOUR);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_TIMEZONE);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22−0200", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, -2 * G_TIME_SPAN_HOUR);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_TIMEZONE);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22−02", &flags);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 0, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, -2 * G_TIME_SPAN_HOUR);
+  g_assert_cmpint (flags, ==, G_DATE_TIME_PARSE_YEAR | G_DATE_TIME_PARSE_MONTH | G_DATE_TIME_PARSE_DAY |
+                   G_DATE_TIME_PARSE_HOUR | G_DATE_TIME_PARSE_TIMEZONE);
+  g_date_time_unref (dt);
+
+  /* Timezone seconds not allowed */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22-12:00:00", &flags);
+  g_assert (dt == NULL);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22-12:00:00.000", &flags);
+  g_assert (dt == NULL);
+
+  /* Timezone hours two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22-2", &flags);
+  g_assert (dt == NULL);
+}
+
+static void
 test_GDateTime_to_unix (void)
 {
   GDateTime *dt;
@@ -1759,6 +2087,7 @@ main (gint   argc,
   g_test_add_func ("/GDateTime/new_from_timeval", test_GDateTime_new_from_timeval);
   g_test_add_func ("/GDateTime/new_from_timeval_utc", test_GDateTime_new_from_timeval_utc);
   g_test_add_func ("/GDateTime/new_from_timeval/overflow", test_GDateTime_new_from_timeval_overflow);
+  g_test_add_func ("/GDateTime/new_from_iso8601", test_GDateTime_new_from_iso8601);
   g_test_add_func ("/GDateTime/new_full", test_GDateTime_new_full);
   g_test_add_func ("/GDateTime/now", test_GDateTime_now);
   g_test_add_func ("/GDateTime/printf", test_GDateTime_printf);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]