Bug 630077 - GDateTime week number support
authorRyan Lortie <desrt@desrt.ca>
Mon, 27 Sep 2010 13:06:24 +0000 (09:06 -0400)
committerRyan Lortie <desrt@desrt.ca>
Fri, 1 Oct 2010 15:20:55 +0000 (11:20 -0400)
Fully implement support for ISO 8601 week dates in GDateTime and
document that this is the case.

Add an exhaustive test case to ensure correctness.

docs/reference/glib/glib-sections.txt
glib/gdatetime.c
glib/gdatetime.h
glib/glib.symbols
glib/tests/gdatetime.c

index 344e2d4..0a818b8 100644 (file)
@@ -1491,6 +1491,7 @@ g_date_time_get_month
 g_date_time_get_day_of_month
 
 <SUBSECTION>
+g_date_time_get_week_numbering_year
 g_date_time_get_week_of_year
 g_date_time_get_day_of_week
 
index 1115495..4ad3807 100644 (file)
@@ -1583,12 +1583,102 @@ g_date_time_get_day_of_month (GDateTime *datetime)
 
 /* Week of year / day of week getters {{{1 */
 /**
+ * g_date_time_get_week_numbering_year:
+ * @date: a #GDateTime
+ *
+ * Returns the ISO 8601 week-numbering year in which the week containing
+ * @datetime falls.
+ *
+ * This function, taken together with g_date_time_get_week_of_year() and
+ * g_date_time_get_day_of_week() can be used to determine the full ISO
+ * week date on which @datetime falls.
+ *
+ * This is usually equal to the normal Gregorian year (as returned by
+ * g_date_time_get_year()), except as detailed below:
+ *
+ * For Thursday, the week-numbering year is always equal to the usual
+ * calendar year.  For other days, the number is such that every day
+ * within a complete week (Monday to Sunday) is contained within the
+ * same week-numbering year.
+ *
+ * For Monday, Tuesday and Wednesday occuring near the end of the year,
+ * this may mean that the week-numbering year is one greater than the
+ * calendar year (so that these days have the same week-numbering year
+ * as the Thursday occuring early in the next year).
+ *
+ * For Friday, Saturaday and Sunday occuring near the start of the year,
+ * this may mean that the week-numbering year is one less than the
+ * calendar year (so that these days have the same week-numbering year
+ * as the Thursday occuring late in the previous year).
+ *
+ * An equivalent description is that the week-numbering year is equal to
+ * the calendar year containing the majority of the days in the current
+ * week (Monday to Sunday).
+ *
+ * Note that January 1 0001 in the proleptic Gregorian calendar is a
+ * Monday, so this function never returns 0.
+ *
+ * Returns: the ISO 8601 week-numbering year for @datetime
+ *
+ * Since: 2.26
+ **/
+gint
+g_date_time_get_week_numbering_year (GDateTime *datetime)
+{
+  gint year, month, day, weekday;
+
+  g_date_time_get_ymd (datetime, &year, &month, &day);
+  weekday = g_date_time_get_day_of_week (datetime);
+
+  /* January 1, 2, 3 might be in the previous year if they occur after
+   * Thursday.
+   *
+   *   Jan 1:  Friday, Saturday, Sunday    =>  day 1:  weekday 5, 6, 7
+   *   Jan 2:  Saturday, Sunday            =>  day 2:  weekday 6, 7
+   *   Jan 3:  Sunday                      =>  day 3:  weekday 7
+   *
+   * So we have a special case if (day - weekday) <= -4
+   */
+  if (month == 1 && (day - weekday) <= -4)
+    return year - 1;
+
+  /* December 29, 30, 31 might be in the next year if they occur before
+   * Thursday.
+   *
+   *   Dec 31: Monday, Tuesday, Wednesday  =>  day 31: weekday 1, 2, 3
+   *   Dec 30: Monday, Tuesday             =>  day 30: weekday 1, 2
+   *   Dec 29: Monday                      =>  day 29: weekday 1
+   *
+   * So we have a special case if (day - weekday) >= 28
+   */
+  else if (month == 12 && (day - weekday) >= 28)
+    return year + 1;
+
+  else
+    return year;
+}
+
+/**
  * g_date_time_get_week_of_year:
  * @datetime: a #GDateTime
  *
- * Returns the numeric week of the respective year.
+ * Returns the ISO 8601 week number for the week containing @datetime.
+ * The ISO 8601 week number is the same for every day of the week (from
+ * Moday through Sunday).  That can produce some unusual results
+ * (described below).
+ *
+ * The first week of the year is week 1.  This is the week that contains
+ * the first Thursday of the year.  Equivalently, this is the first week
+ * that has more than 4 of its days falling within the calendar year.
+ *
+ * The value 0 is never returned by this function.  Days contained
+ * within a year but occuring before the first ISO 8601 week of that
+ * year are considered as being contained in the last week of the
+ * previous year.  Similarly, the final days of a calendar year may be
+ * considered as being part of the first ISO 8601 week of the next year
+ * if 4 or more days of that week are contained within the new year.
  *
- * Return value: the week of the year
+ * Returns: the ISO 8601 week number for @datetime.
  *
  * Since: 2.26
  */
@@ -1608,7 +1698,7 @@ g_date_time_get_week_of_year (GDateTime *datetime)
  * g_date_time_get_day_of_week:
  * @datetime: a #GDateTime
  *
- * Retrieves the ISO 8601 day of the week represented by @datetime (1 is
+ * Retrieves the ISO 8601 day of the week on which @datetime falls (1 is
  * Monday, 2 is Tuesday... 7 is Sunday).
  *
  * Return value: the day of the week
index f668fde..b76df89 100644 (file)
@@ -184,6 +184,7 @@ gint                    g_date_time_get_year                            (GDateTi
 gint                    g_date_time_get_month                           (GDateTime      *datetime);
 gint                    g_date_time_get_day_of_month                    (GDateTime      *datetime);
 
+gint                    g_date_time_get_week_numbering_year             (GDateTime      *datetime);
 gint                    g_date_time_get_week_of_year                    (GDateTime      *datetime);
 gint                    g_date_time_get_day_of_week                     (GDateTime      *datetime);
 
index f4b45a0..52d0519 100644 (file)
@@ -349,6 +349,7 @@ g_date_time_get_second
 g_date_time_get_seconds
 g_date_time_get_timezone_abbreviation
 g_date_time_get_utc_offset
+g_date_time_get_week_numbering_year
 g_date_time_get_week_of_year
 g_date_time_get_year
 g_date_time_get_ymd
index e6b1c4d..20f7cda 100644 (file)
@@ -231,50 +231,6 @@ test_GDateTime_get_day_of_month (void)
 }
 
 static void
-test_GDateTime_get_ymd (void)
-{
-   GDateTime *dt;
-   struct tm tm;
-   time_t t;
-   gint d, m, y;
-   gint d2, m2, y2;
-   gint days[2][13] = {{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
-                       {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
-
-   t = time (NULL);
-   memset (&tm, 0, sizeof (struct tm));
-   get_localtime_tm (t, &tm);
-
-   dt = g_date_time_new_from_unix_local (t);
-   g_date_time_get_ymd(dt, &y, &m, &d);
-   g_assert_cmpint(y, ==, tm.tm_year + 1900);
-   g_assert_cmpint(m, ==, tm.tm_mon + 1);
-   g_assert_cmpint(d, ==, tm.tm_mday);
-
-   /* exaustive test */
-   for (y = 1750; y < 2250; y++)
-     {
-       gint leap = ((y % 4) == 0) && (!(((y % 100) == 0) && ((y % 400) != 0)))
-                 ? 1
-                 : 0;
-
-       for (m = 1; m <= 12; m++)
-         {
-           for (d = 1; d <= days[leap][m]; d++)
-             {
-               GDateTime *dt1 = g_date_time_new_utc (y, m, d, 0, 0, 0);
-
-               g_date_time_get_ymd (dt1, &y2, &m2, &d2);
-               g_assert_cmpint (y, ==, y2);
-               g_assert_cmpint (m, ==, m2);
-               g_assert_cmpint (d, ==, d2);
-               g_date_time_unref (dt1);
-             }
-         }
-     }
-}
-
-static void
 test_GDateTime_get_hour (void)
 {
   GDateTime *dt;
@@ -898,6 +854,148 @@ test_GDateTime_dst (void)
   g_time_zone_unref (tz);
 }
 
+static inline gboolean
+is_leap_year (gint year)
+{
+  g_assert (1 <= year && year <= 9999);
+
+  return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
+}
+
+static inline gint
+days_in_month (gint year, gint month)
+{
+  const gint table[2][13] = {
+    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
+    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+  };
+
+  g_assert (1 <= month && month <= 12);
+
+  return table[is_leap_year (year)][month];
+}
+
+static void
+test_all_dates (void)
+{
+  gint year, month, day;
+  GTimeZone *timezone;
+  gint64 unix_time;
+  gint day_of_year;
+  gint week_year;
+  gint week_num;
+  gint weekday;
+
+  /* save some time by hanging on to this. */
+  timezone = g_time_zone_new_utc ();
+
+  unix_time = G_GINT64_CONSTANT(-62135596800);
+
+  /* 0001-01-01 is 0001-W01-1 */
+  week_year = 1;
+  week_num = 1;
+  weekday = 1;
+
+
+  /* The calendar makes a full cycle every 400 years, so we could
+   * theoretically just test years 1 through 400.  That assumes that our
+   * software has no bugs, so probably we should just test them all. :)
+   */
+  for (year = 1; year <= 9999; year++)
+    {
+      day_of_year = 1;
+
+      for (month = 1; month <= 12; month++)
+        for (day = 1; day <= days_in_month (year, month); day++)
+          {
+            GDateTime *dt;
+
+            dt = g_date_time_new (timezone, year, month, day, 0, 0, 0);
+
+#if 0
+            g_print ("%04d-%02d-%02d = %04d-W%02d-%d = %04d-%03d\n",
+                     year, month, day,
+                     week_year, week_num, weekday,
+                     year, day_of_year);
+#endif
+
+            /* sanity check */
+            if G_UNLIKELY (g_date_time_get_year (dt) != year ||
+                           g_date_time_get_month (dt) != month ||
+                           g_date_time_get_day_of_month (dt) != day)
+              g_error ("%04d-%02d-%02d comes out as %04d-%02d-%02d",
+                       year, month, day,
+                       g_date_time_get_year (dt),
+                       g_date_time_get_month (dt),
+                       g_date_time_get_day_of_month (dt));
+
+            if G_UNLIKELY (g_date_time_get_week_numbering_year (dt) != week_year ||
+                           g_date_time_get_week_of_year (dt) != week_num ||
+                           g_date_time_get_day_of_week (dt) != weekday)
+              g_error ("%04d-%02d-%02d should be %04d-W%02d-%d but "
+                       "comes out as %04d-W%02d-%d", year, month, day,
+                       week_year, week_num, weekday,
+                       g_date_time_get_week_numbering_year (dt),
+                       g_date_time_get_week_of_year (dt),
+                       g_date_time_get_day_of_week (dt));
+
+            if G_UNLIKELY (g_date_time_to_unix (dt) != unix_time)
+              g_error ("%04d-%02d-%02d 00:00:00 UTC should have unix time %"
+                       G_GINT64_FORMAT " but comes out as %"G_GINT64_FORMAT,
+                       year, month, day, unix_time, g_date_time_to_unix (dt));
+
+            if G_UNLIKELY (g_date_time_get_day_of_year (dt) != day_of_year)
+              g_error ("%04d-%02d-%02d should be day of year %d"
+                       " but comes out as %d", year, month, day,
+                       day_of_year, g_date_time_get_day_of_year (dt));
+
+            if G_UNLIKELY (g_date_time_get_hour (dt) != 0 ||
+                           g_date_time_get_minute (dt) != 0 ||
+                           g_date_time_get_seconds (dt) != 0)
+              g_error ("%04d-%02d-%02d 00:00:00 UTC comes out "
+                       "as %02d:%02d:%02.6f", year, month, day,
+                       g_date_time_get_hour (dt),
+                       g_date_time_get_minute (dt),
+                       g_date_time_get_seconds (dt));
+            /* done */
+
+            /* add 24 hours to unix time */
+            unix_time += 24 * 60 * 60;
+
+            /* move day of year forward */
+            day_of_year++;
+
+            /* move the week date forward */
+            if (++weekday == 8)
+              {
+                weekday = 1; /* Sunday -> Monday */
+
+                /* NOTE: year/month/day is the final day of the week we
+                 * just finished.
+                 *
+                 * If we just finished the last week of last year then
+                 * we are definitely starting the first week of this
+                 * year.
+                 *
+                 * Otherwise, if we're still in this year, but Sunday
+                 * fell on or after December 28 then December 29, 30, 31
+                 * could be days within the next year's first year.
+                 */
+                if (year != week_year || (month == 12 && day >= 28))
+                  {
+                    /* first week of the new year */
+                    week_num = 1;
+                    week_year++;
+                  }
+                else
+                  week_num++;
+              }
+          }
+    }
+
+  g_time_zone_unref (timezone);
+}
+
 gint
 main (gint   argc,
       gchar *argv[])
@@ -920,7 +1018,6 @@ main (gint   argc,
   g_test_add_func ("/GDateTime/get_day_of_week", test_GDateTime_get_day_of_week);
   g_test_add_func ("/GDateTime/get_day_of_month", test_GDateTime_get_day_of_month);
   g_test_add_func ("/GDateTime/get_day_of_year", test_GDateTime_get_day_of_year);
-  g_test_add_func ("/GDateTime/get_ymd", test_GDateTime_get_ymd);
   g_test_add_func ("/GDateTime/get_hour", test_GDateTime_get_hour);
   g_test_add_func ("/GDateTime/get_microsecond", test_GDateTime_get_microsecond);
   g_test_add_func ("/GDateTime/get_minute", test_GDateTime_get_minute);
@@ -940,6 +1037,7 @@ main (gint   argc,
   g_test_add_func ("/GDateTime/to_utc", test_GDateTime_to_utc);
   g_test_add_func ("/GDateTime/now_utc", test_GDateTime_now_utc);
   g_test_add_func ("/GDateTime/dst", test_GDateTime_dst);
+  g_test_add_func ("/GDateTime/test-all-dates", test_all_dates);
 
   return g_test_run ();
 }