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.
This commit is contained in:
Ryan Lortie 2010-09-27 09:06:24 -04:00
parent fe1186a842
commit fff6814973
5 changed files with 239 additions and 48 deletions

View 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

View File

@ -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

View 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);

View 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

View File

@ -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 ();
}