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



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

    GDateTime: Support parsing ISO 8601 strings.
    
    This supports a subset of ISO 8601 since that is a commonly used standard for
    storing date and time information. We support only ISO 8601 strings that contain
    full date and time information as this would otherwise not map to GDateTime.
    This subset includes all of RFC 3339 which is commonly used on the Internet and
    the week and ordinal day formats as these are supported in the GDateTime APIs.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=753459

 docs/reference/glib/glib-sections.txt |    1 +
 glib/gdatetime.c                      |  337 +++++++++++++++++++++++++++++++++
 glib/gdatetime.h                      |    4 +
 glib/tests/gdatetime.c                |  311 ++++++++++++++++++++++++++++++-
 4 files changed, 652 insertions(+), 1 deletions(-)
---
diff --git a/docs/reference/glib/glib-sections.txt b/docs/reference/glib/glib-sections.txt
index e09d4d3..e8a7fc8 100644
--- a/docs/reference/glib/glib-sections.txt
+++ b/docs/reference/glib/glib-sections.txt
@@ -1667,6 +1667,7 @@ g_date_time_new_from_unix_utc
 <SUBSECTION>
 g_date_time_new_from_timeval_local
 g_date_time_new_from_timeval_utc
+g_date_time_new_from_iso8601
 
 <SUBSECTION>
 g_date_time_new
diff --git a/glib/gdatetime.c b/glib/gdatetime.c
index c78bc4c..06f7bfb 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,342 @@ g_date_time_new_from_timeval_utc (const GTimeVal *tv)
   return datetime;
 }
 
+/* Parse integers in the form d (week days), dd (hours etc), ddd (ordinal days) or dddd (years) */
+static gboolean
+get_iso8601_int (const gchar *text, gsize length, gint *value)
+{
+  gint i, v = 0;
+
+  if (length < 1 || length > 4)
+    return FALSE;
+
+  for (i = 0; i < length; i++)
+    {
+      const gchar c = text[i];
+      if (c < '0' || c > '9')
+        return FALSE;
+      v = v * 10 + (c - '0');
+    }
+
+  *value = v;
+  return TRUE;
+}
+
+/* Parse seconds in the form ss or ss.sss (variable length decimal) */
+static gboolean
+get_iso8601_seconds (const gchar *text, gsize length, gdouble *value)
+{
+  gint i;
+  gdouble multiplier = 0.1, v = 0;
+
+  if (length < 2)
+    return FALSE;
+
+  for (i = 0; i < 2; i++)
+    {
+      const gchar c = text[i];
+      if (c < '0' || c > '9')
+        return FALSE;
+      v = v * 10 + (c - '0');
+    }
+
+  if (length > 2 && !(text[i] == '.' || text[i] == ','))
+    return FALSE;
+  i++;
+  if (i == length)
+    return FALSE;
+
+  for (; i < length; i++)
+    {
+      const gchar c = text[i];
+      if (c < '0' || c > '9')
+        return FALSE;
+      v += (c - '0') * multiplier;
+      multiplier *= 0.1;
+    }
+
+  *value = v;
+  return TRUE;
+}
+
+static GDateTime *
+g_date_time_new_ordinal (GTimeZone *tz, gint year, gint ordinal_day, gint hour, gint minute, gdouble seconds)
+{
+  GDateTime *dt;
+
+  if (ordinal_day < 1 || ordinal_day > (GREGORIAN_LEAP (year) ? 366 : 365))
+    return NULL;
+
+  dt = g_date_time_new (tz, year, 1, 1, hour, minute, seconds);
+  dt->days += ordinal_day - 1;
+
+  return dt;
+}
+
+static GDateTime *
+g_date_time_new_week (GTimeZone *tz, gint year, gint week, gint week_day, gint hour, gint minute, gdouble 
seconds)
+{
+  gint64 p;
+  gint max_week, jan4_week_day, ordinal_day;
+  GDateTime *dt;
+
+  p = (year * 365 + (year / 4) - (year / 100) + (year / 400)) % 7;
+  max_week = p == 4 ? 53 : 52;
+
+  if (week < 1 || week > max_week || week_day < 1 || week_day > 7)
+    return NULL;
+
+  dt = g_date_time_new (tz, year, 1, 4, 0, 0, 0);
+  g_date_time_get_week_number (dt, NULL, &jan4_week_day, NULL);
+  ordinal_day = (week * 7) + week_day - (jan4_week_day + 3);
+  if (ordinal_day < 0)
+    {
+      year--;
+      ordinal_day += GREGORIAN_LEAP (year) ? 366 : 365;
+    }
+  else if (ordinal_day > (GREGORIAN_LEAP (year) ? 366 : 365))
+    {
+      ordinal_day -= (GREGORIAN_LEAP (year) ? 366 : 365);
+      year++;
+    }
+
+  return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds);
+}
+
+static GDateTime *
+parse_iso8601_date (const gchar *text, gsize length,
+                    gint hour, gint minute, gdouble seconds, GTimeZone *tz)
+{
+  /* YYYY-MM-DD */
+  if (length == 10 && text[4] == '-' && text[7] == '-')
+    {
+      int year, month, day;
+      if (!get_iso8601_int (text, 4, &year) ||
+          !get_iso8601_int (text + 5, 2, &month) ||
+          !get_iso8601_int (text + 8, 2, &day))
+        return NULL;
+      return g_date_time_new (tz, year, month, day, hour, minute, seconds);
+    }
+  /* YYYY-DDD */
+  else if (length == 8 && text[4] == '-')
+    {
+      gint year, ordinal_day;
+      if (!get_iso8601_int (text, 4, &year) ||
+          !get_iso8601_int (text + 5, 3, &ordinal_day))
+        return NULL;
+      return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds);
+    }
+  /* YYYY-Www-D */
+  else if (length == 10 && text[4] == '-' && text[5] == 'W' && text[8] == '-')
+    {
+      gint year, week, week_day;
+      if (!get_iso8601_int (text, 4, &year) ||
+          !get_iso8601_int (text + 6, 2, &week) ||
+          !get_iso8601_int (text + 9, 1, &week_day))
+        return NULL;
+      return g_date_time_new_week (tz, year, week, week_day, hour, minute, seconds);
+    }
+  /* YYYYWwwD */
+  else if (length == 8 && text[4] == 'W')
+    {
+      gint year, week, week_day;
+      if (!get_iso8601_int (text, 4, &year) ||
+          !get_iso8601_int (text + 5, 2, &week) ||
+          !get_iso8601_int (text + 7, 1, &week_day))
+        return NULL;
+      return g_date_time_new_week (tz, year, week, week_day, hour, minute, seconds);
+    }
+  /* YYYYMMDD */
+  else if (length == 8)
+    {
+      int year, month, day;
+      if (!get_iso8601_int (text, 4, &year) ||
+          !get_iso8601_int (text + 4, 2, &month) ||
+          !get_iso8601_int (text + 6, 2, &day))
+        return NULL;
+      return g_date_time_new (tz, year, month, day, hour, minute, seconds);
+    }
+  /* YYYYDDD */
+  else if (length == 7)
+    {
+      gint year, ordinal_day;
+      if (!get_iso8601_int (text, 4, &year) ||
+          !get_iso8601_int (text + 4, 3, &ordinal_day))
+        return NULL;
+      return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds);
+    }
+  else
+    return FALSE;
+}
+
+static GTimeZone *
+parse_iso8601_timezone (const gchar *text, gsize length, gssize *tz_offset)
+{
+  gint i, tz_length, offset_sign = 1, offset_hours, offset_minutes;
+  GTimeZone *tz;
+
+  /* UTC uses Z suffix  */
+  if (length > 0 && text[length - 1] == 'Z')
+    {
+      *tz_offset = length - 1;
+      return g_time_zone_new_utc ();
+    }
+
+  /* Look for '+' or '-' of offset */
+  for (i = length - 1; i >= 0; i--)
+    if (text[i] == '+' || text[i] == '-')
+      {
+        offset_sign = text[i] == '-' ? -1 : 1;
+        break;
+      }
+  if (i < 0)
+    return NULL;
+  tz_length = length - i;
+
+  /* +hh:mm or -hh:mm */
+  if (tz_length == 6 && text[i+3] == ':')
+    {
+      if (!get_iso8601_int (text + i + 1, 2, &offset_hours) ||
+          !get_iso8601_int (text + i + 4, 2, &offset_minutes))
+        return NULL;
+    }
+  /* +hhmm or -hhmm */
+  else if (tz_length == 5)
+    {
+      if (!get_iso8601_int (text + i + 1, 2, &offset_hours) ||
+          !get_iso8601_int (text + i + 3, 2, &offset_minutes))
+        return NULL;
+    }
+  /* +hh or -hh */
+  else if (tz_length == 3)
+    {
+      if (!get_iso8601_int (text + i + 1, 2, &offset_hours))
+        return NULL;
+      offset_minutes = 0;
+    }
+  else
+    return NULL;
+
+  *tz_offset = i;
+  tz = g_time_zone_new (text + i);
+
+  /* Double-check that the GTimeZone matches our interpretation of the timezone.
+   * Failure would indicate a bug either here of in the GTimeZone code. */
+  g_assert (g_time_zone_get_offset (tz, 0) == offset_sign * (offset_hours * 3600 + offset_minutes * 60));
+
+  return tz;
+}
+
+static gboolean
+parse_iso8601_time (const gchar *text, gsize length,
+                    gint *hour, gint *minute, gdouble *seconds, GTimeZone **tz)
+{
+  gssize tz_offset = -1;
+
+  /* Check for timezone suffix */
+  *tz = parse_iso8601_timezone (text, length, &tz_offset);
+  if (tz_offset >= 0)
+    length = tz_offset;
+
+  /* hh:mm:ss(.sss) */
+  if (length >= 8 && text[2] == ':' && text[5] == ':')
+    {
+      return get_iso8601_int (text, 2, hour) &&
+             get_iso8601_int (text + 3, 2, minute) &&
+             get_iso8601_seconds (text + 6, length - 6, seconds);
+    }
+  /* hhmmss(.sss) */
+  else if (length >= 6)
+    {
+      return get_iso8601_int (text, 2, hour) &&
+             get_iso8601_int (text + 2, 2, minute) &&
+             get_iso8601_seconds (text + 4, length - 4, seconds);
+    }
+  else
+    return FALSE;
+}
+
+/**
+ * g_date_time_new_from_iso8601:
+ * @text: an ISO 8601 formatted time string.
+ * @default_tz: (nullable): a #GTimeZone to use if the text doesn't contain a
+ *                          timezone, or %NULL.
+ *
+ * Creates a #GDateTime corresponding to the given
+ * [ISO 8601 formatted string](https://en.wikipedia.org/wiki/ISO_8601)
+ * @text. ISO 8601 strings of the form <date><sep><time><tz> are supported.
+ *
+ * <sep> is the separator and can be either 'T', 't' or ' '.
+ *
+ * <date> is in the form:
+ *
+ * - `YYYY-MM-DD` - Year/month/day, e.g. 2016-08-24.
+ * - `YYYYMMDD` - Same as above without dividers.
+ * - `YYYY-DDD` - Ordinal day where DDD is from 001 to 366, e.g. 2016-237.
+ * - `YYYYDDD` - Same as above without dividers.
+ * - `YYYY-Www-D` - Week day where ww is from 01 to 52 and D from 1-7,
+ *   e.g. 2016-W34-3.
+ * - `YYYYWwwD` - Same as above without dividers.
+ *
+ * <time> is in the form:
+ *
+ * - `hh:mm:ss(.sss)` - Hours, minutes, seconds (subseconds), e.g. 22:10:42.123.
+ * - `hhmmss(.sss)` - Same as above without dividers.
+ *
+ * <tz> is an optional timezone suffix of the form:
+ *
+ * - `Z` - UTC.
+ * - `+hh:mm` or `-hh:mm` - Offset from UTC in hours and minutes, e.g. +12:00.
+ * - `+hh` or `-hh` - Offset from UTC in hours, e.g. +12.
+ *
+ * If the timezone is not provided in @text it must be provided in @default_tz
+ * (this field is otherwise ignored).
+ *
+ * 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: (transfer full) (nullable): a new #GDateTime, or %NULL
+ *
+ * Since: 2.54
+ */
+GDateTime *
+g_date_time_new_from_iso8601 (const gchar *text, GTimeZone *default_tz)
+{
+  gint length, date_length = -1;
+  gint hour = 0, minute = 0;
+  gdouble seconds = 0.0;
+  GTimeZone *tz = NULL;
+  GDateTime *datetime = NULL;
+
+  g_return_val_if_fail (text != NULL, NULL);
+
+  /* Count length of string and find date / time separator ('T', 't', or ' ') */
+  for (length = 0; text[length] != '\0'; length++)
+    {
+      if (date_length < 0 && (text[length] == 'T' || text[length] == 't' || text[length] == ' '))
+        date_length = length;
+    }
+
+  if (date_length < 0)
+    return NULL;
+
+  if (!parse_iso8601_time (text + date_length + 1, length - (date_length + 1),
+                           &hour, &minute, &seconds, &tz))
+    goto out;
+  if (tz == NULL && default_tz == NULL)
+    return NULL;
+
+  datetime = parse_iso8601_date (text, date_length, hour, minute, seconds, tz ? tz : default_tz);
+
+out:
+    if (tz != NULL)
+      g_time_zone_unref (tz);
+    return datetime;
+}
+
 /* full new functions {{{1 */
 
 /**
diff --git a/glib/gdatetime.h b/glib/gdatetime.h
index 927e29c..02f1fc7 100644
--- a/glib/gdatetime.h
+++ b/glib/gdatetime.h
@@ -118,6 +118,10 @@ 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_54
+GDateTime *             g_date_time_new_from_iso8601                    (const gchar    *text,
+                                                                         GTimeZone      *default_tz);
+
 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 014be0b..fe8651e 100644
--- a/glib/tests/gdatetime.c
+++ b/glib/tests/gdatetime.c
@@ -25,14 +25,17 @@
 #include <locale.h>
 
 #define ASSERT_DATE(dt,y,m,d) G_STMT_START { \
+  g_assert_nonnull ((dt)); \
   g_assert_cmpint ((y), ==, g_date_time_get_year ((dt))); \
   g_assert_cmpint ((m), ==, g_date_time_get_month ((dt))); \
   g_assert_cmpint ((d), ==, g_date_time_get_day_of_month ((dt))); \
 } G_STMT_END
-#define ASSERT_TIME(dt,H,M,S) G_STMT_START { \
+#define ASSERT_TIME(dt,H,M,S,U) G_STMT_START { \
+  g_assert_nonnull ((dt)); \
   g_assert_cmpint ((H), ==, g_date_time_get_hour ((dt))); \
   g_assert_cmpint ((M), ==, g_date_time_get_minute ((dt))); \
   g_assert_cmpint ((S), ==, g_date_time_get_second ((dt))); \
+  g_assert_cmpint ((U), ==, g_date_time_get_microsecond ((dt))); \
 } G_STMT_END
 
 static void
@@ -469,6 +472,311 @@ test_GDateTime_new_from_timeval_utc (void)
 }
 
 static void
+test_GDateTime_new_from_iso8601 (void)
+{
+  GDateTime *dt;
+  GTimeZone *tz;
+
+  /* Need non-empty string */
+  dt = g_date_time_new_from_iso8601 ("", NULL);
+  g_assert_null (dt);
+
+  /* Needs to be correctly formatted */
+  dt = g_date_time_new_from_iso8601 ("not a date", NULL);
+  g_assert_null (dt);
+
+  /* Check common case */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+
+  /* Timezone is required in text or passed as arg */
+  tz = g_time_zone_new_utc ();
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42", tz);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  g_date_time_unref (dt);
+  g_time_zone_unref (tz);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42", NULL);
+  g_assert_null (dt);
+
+  /* Can't have whitespace */
+  dt = g_date_time_new_from_iso8601 ("2016 08 24T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42Z ", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 (" 2016-08-24T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Check lowercase time separator or space allowed */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24t22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24 22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+
+  /* Check dates without separators allowed */
+  dt = g_date_time_new_from_iso8601 ("20160824T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+
+  /* Months are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-1-01T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Days are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-01-1T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Need consistent usage of separators */
+  dt = g_date_time_new_from_iso8601 ("2016-0824T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("201608-24T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Check month within valid range */
+  dt = g_date_time_new_from_iso8601 ("2016-00-13T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-13-13T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Check day within valid range */
+  dt = g_date_time_new_from_iso8601 ("2016-01-00T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-01-31T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 1, 31);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-01-32T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-02-29T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 2, 29);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-02-30T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2017-02-28T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2017, 2, 28);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2017-02-29T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-03-31T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 3, 31);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-03-32T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-04-30T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 4, 30);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-04-31T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-05-31T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 5, 31);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-05-32T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-06-30T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 6, 30);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-06-31T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-07-31T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 7, 31);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-07-32T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-31T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 31);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-32T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-09-30T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 9, 30);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-09-31T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-10-31T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 10, 31);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-10-32T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-11-30T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 11, 30);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-11-31T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-12-31T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 12, 31);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-12-32T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Check ordinal days work */
+  dt = g_date_time_new_from_iso8601 ("2016-237T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016237T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+
+  /* Check ordinal leap days */
+  dt = g_date_time_new_from_iso8601 ("2016-366T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 12, 31);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2017-365T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2017, 12, 31);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2017-366T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Days start at 1 */
+  dt = g_date_time_new_from_iso8601 ("2016-000T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Limited to number of days in the year (2016 is a leap year) */
+  dt = g_date_time_new_from_iso8601 ("2016-367T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Days are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-1T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-12T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Check week days work */
+  dt = g_date_time_new_from_iso8601 ("2016-W34-3T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016W343T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_date_time_unref (dt);
+
+  /* We don't support weeks without weekdays (valid ISO 8601) */
+  dt = g_date_time_new_from_iso8601 ("2016-W34T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016W34T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Weeks are two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-W3-1T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Weeks start at 1 */
+  dt = g_date_time_new_from_iso8601 ("2016-W00-1T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Week one might be in the previous year */
+  dt = g_date_time_new_from_iso8601 ("2015-W01-1T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2014, 12, 29);
+  g_date_time_unref (dt);
+
+  /* Last week might be in next year */
+  dt = g_date_time_new_from_iso8601 ("2015-W53-7T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 1, 3);
+  g_date_time_unref (dt);
+
+  /* Week 53 doesn't always exist */
+  dt = g_date_time_new_from_iso8601 ("2016-W53-1T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Limited to number of days in the week */
+  dt = g_date_time_new_from_iso8601 ("2016-W34-0T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-W34-8T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Days are one digit */
+  dt = g_date_time_new_from_iso8601 ("2016-W34-99T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Check week day changes depending on year */
+  dt = g_date_time_new_from_iso8601 ("2017-W34-1T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2017, 8, 21);
+  g_date_time_unref (dt);
+
+  /* Check week day changes depending on leap years */
+  dt = g_date_time_new_from_iso8601 ("1900-W01-1T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 1900, 1, 1);
+  g_date_time_unref (dt);
+
+  /* YYYY-MM not allowed (NOT valid ISO 8601) */
+  dt = g_date_time_new_from_iso8601 ("2016-08T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* We don't support omitted year (valid ISO 8601) */
+  dt = g_date_time_new_from_iso8601 ("--08-24T22:10:42Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("--0824T22:10:42Z", NULL);
+  g_assert_null (dt);
+
+  /* Check subseconds work */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42.123456Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 123456);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42,123456Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 123456);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42.Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T221042.123456Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 123456);
+  g_date_time_unref (dt);
+
+  /* We don't support times without minutes / seconds (valid ISO 8601) */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10Z", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T2210Z", NULL);
+  g_assert_null (dt);
+
+  /* UTC time uses 'Z' */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42Z", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, 0);
+  g_date_time_unref (dt);
+
+  /* Check timezone works */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42+12:00", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, 12 * G_TIME_SPAN_HOUR);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42+12", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, 12 * G_TIME_SPAN_HOUR);
+  g_date_time_unref (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22:10:42-02", NULL);
+  ASSERT_DATE (dt, 2016, 8, 24);
+  ASSERT_TIME (dt, 22, 10, 42, 0);
+  g_assert_cmpint (g_date_time_get_utc_offset (dt), ==, -2 * G_TIME_SPAN_HOUR);
+  g_date_time_unref (dt);
+
+  /* Timezone seconds not allowed */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22-12:00:00", NULL);
+  g_assert_null (dt);
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22-12:00:00.000", NULL);
+  g_assert_null (dt);
+
+  /* Timezone hours two digits */
+  dt = g_date_time_new_from_iso8601 ("2016-08-24T22-2Z", NULL);
+  g_assert_null (dt);
+}
+
+static void
 test_GDateTime_to_unix (void)
 {
   GDateTime *dt;
@@ -1813,6 +2121,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]