From 9c0eb37adb14209e066bd25c6a3e27b0f698decc Mon Sep 17 00:00:00 2001 From: Ryan Lortie Date: Mon, 27 Sep 2010 09:06:24 -0400 Subject: [PATCH] Bug 630077 - GDateTime week number support 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 | 1 + glib/gdatetime.c | 96 ++++++++++++- glib/gdatetime.h | 1 + glib/glib.symbols | 1 + glib/tests/gdatetime.c | 188 ++++++++++++++++++++------ 5 files changed, 239 insertions(+), 48 deletions(-) diff --git a/docs/reference/glib/glib-sections.txt b/docs/reference/glib/glib-sections.txt index 98cb530b1..031550bcc 100644 --- a/docs/reference/glib/glib-sections.txt +++ b/docs/reference/glib/glib-sections.txt @@ -1483,6 +1483,7 @@ g_date_time_get_month g_date_time_get_day_of_month +g_date_time_get_week_numbering_year g_date_time_get_week_of_year g_date_time_get_day_of_week diff --git a/glib/gdatetime.c b/glib/gdatetime.c index bcfb6e7fd..3509cec74 100644 --- a/glib/gdatetime.c +++ b/glib/gdatetime.c @@ -1582,13 +1582,103 @@ 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). * - * Return value: the week of the year + * 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. + * + * 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 diff --git a/glib/gdatetime.h b/glib/gdatetime.h index f668fde87..b76df89c7 100644 --- a/glib/gdatetime.h +++ b/glib/gdatetime.h @@ -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); diff --git a/glib/glib.symbols b/glib/glib.symbols index 39000bd0e..970fc0803 100644 --- a/glib/glib.symbols +++ b/glib/glib.symbols @@ -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 diff --git a/glib/tests/gdatetime.c b/glib/tests/gdatetime.c index e6b1c4d13..20f7cda28 100644 --- a/glib/tests/gdatetime.c +++ b/glib/tests/gdatetime.c @@ -230,50 +230,6 @@ test_GDateTime_get_day_of_month (void) g_date_time_unref (dt); } -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) { @@ -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 (); }