From 4bac6613cfca4b0676a51e1df0848fa2e4d35120 Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Wed, 15 Sep 2010 13:55:36 +0100 Subject: [PATCH] datetime: Update modifiers for DST changes If a DateTime gets modified to cross the DST state from its previous state then we want to update the DateTime to compensate for the new offset. In other words, if we have a DateTime defined as: DateTime({ y: 2009, m: 8, d: 15, hh: 3, mm: 0, tz: 'Europe/London' }); and we add six months to it, the hour must be changed to 60 minutes behind, as the DST comes into effect. https://bugzilla.gnome.org/show_bug.cgi?id=50076 --- glib/gdatetime.c | 270 +++++++++++++++++++++++++---------------- glib/tests/gdatetime.c | 67 ++++++++-- 2 files changed, 220 insertions(+), 117 deletions(-) diff --git a/glib/gdatetime.c b/glib/gdatetime.c index 634f73d15..e5b55c6be 100644 --- a/glib/gdatetime.c +++ b/glib/gdatetime.c @@ -313,111 +313,6 @@ get_weekday_name_abbr (gint day) return NULL; } -static inline gint -date_to_proleptic_gregorian (gint year, - gint month, - gint day) -{ - gint64 days; - - days = (year - 1) * 365 + ((year - 1) / 4) - ((year - 1) / 100) - + ((year - 1) / 400); - - days += days_in_year[0][month - 1]; - if (GREGORIAN_LEAP (year) && month > 2) - day++; - - days += day; - - return days; -} - -static inline void -g_date_time_add_days_internal (GDateTime *datetime, - gint64 days) -{ - datetime->days += days; -} - -static inline void -g_date_time_add_usec (GDateTime *datetime, - gint64 usecs) -{ - gint64 u = datetime->usec + usecs; - gint d = u / USEC_PER_DAY; - - if (u < 0) - d -= 1; - - if (d != 0) - g_date_time_add_days_internal (datetime, d); - - if (u < 0) - datetime->usec = USEC_PER_DAY + (u % USEC_PER_DAY); - else - datetime->usec = u % USEC_PER_DAY; -} - -/*< internal > - * g_date_time_add_ymd: - * @datetime: a #GDateTime - * @years: years to add, in the Gregorian calendar - * @months: months to add, in the Gregorian calendar - * @days: days to add, in the Gregorian calendar - * - * Updates @datetime by adding @years, @months and @days to it - * - * This function modifies the passed #GDateTime so public accessors - * should make always pass a copy - */ -static inline void -g_date_time_add_ymd (GDateTime *datetime, - gint years, - gint months, - gint days) -{ - gint y = g_date_time_get_year (datetime); - gint m = g_date_time_get_month (datetime); - gint d = g_date_time_get_day_of_month (datetime); - gint step, i; - const guint16 *max_days; - - y += years; - - /* subtract one day for leap years */ - if (GREGORIAN_LEAP (y) && m == 2) - { - if (d == 29) - d -= 1; - } - - /* add months */ - step = months > 0 ? 1 : -1; - for (i = 0; i < ABS (months); i++) - { - m += step; - - if (m < 1) - { - y -= 1; - m = 12; - } - else if (m > 12) - { - y += 1; - m = 1; - } - } - - /* clamp the days */ - max_days = days_in_months[GREGORIAN_LEAP (y) ? 1 : 0]; - if (max_days[m] < d) - d = max_days[m]; - - datetime->days = date_to_proleptic_gregorian (y, m, d); - g_date_time_add_days_internal (datetime, days); -} - #define ZONEINFO_DIR "zoneinfo" #define TZ_MAGIC "TZif" #define TZ_MAGIC_LEN (strlen (TZ_MAGIC)) @@ -950,6 +845,171 @@ g_time_zone_sink (GTimeZone *time_zone, } } +static inline gint +date_to_proleptic_gregorian (gint year, + gint month, + gint day) +{ + gint64 days; + + days = (year - 1) * 365 + ((year - 1) / 4) - ((year - 1) / 100) + + ((year - 1) / 400); + + days += days_in_year[0][month - 1]; + if (GREGORIAN_LEAP (year) && month > 2) + day++; + + days += day; + + return days; +} + +static inline void g_date_time_add_usec (GDateTime *datetime, + gint64 usecs); + +static inline void +g_date_time_add_days_internal (GDateTime *datetime, + gint64 days) +{ + gboolean was_dst = FALSE; + gint64 old_offset = 0; + + if (datetime->tz != NULL && datetime->tz->tz_file != NULL) + { + was_dst = g_time_zone_get_is_dst (datetime->tz); + old_offset = g_time_zone_get_offset (datetime->tz); + + datetime->tz->is_floating = TRUE; + } + + datetime->days += days; + + if (datetime->tz != NULL && datetime->tz->tz_file != NULL) + { + gint64 offset; + + g_time_zone_sink (datetime->tz, datetime); + + if (was_dst == g_time_zone_get_is_dst (datetime->tz)) + return; + + offset = old_offset - g_time_zone_get_offset (datetime->tz); + g_date_time_add_usec (datetime, offset * USEC_PER_SECOND * -1); + } +} + +static inline void +g_date_time_add_usec (GDateTime *datetime, + gint64 usecs) +{ + gint64 u = datetime->usec + usecs; + gint d = u / USEC_PER_DAY; + gboolean was_dst = FALSE; + gint64 old_offset = 0; + + /* if we are using a time zone from a zoneinfo we want to + * check for changes in the DST and update the DateTime + * accordingly in case we change for standard time to DST + * and vice versa + */ + if (datetime->tz != NULL && datetime->tz->tz_file != NULL) + { + was_dst = g_time_zone_get_is_dst (datetime->tz); + old_offset = g_time_zone_get_offset (datetime->tz); + + /* force the floating state */ + datetime->tz->is_floating = TRUE; + } + + if (u < 0) + d -= 1; + + if (d != 0) + g_date_time_add_days_internal (datetime, d); + + if (u < 0) + datetime->usec = USEC_PER_DAY + (u % USEC_PER_DAY); + else + datetime->usec = u % USEC_PER_DAY; + + if (datetime->tz != NULL && datetime->tz->tz_file != NULL) + { + gint64 offset; + + /* sink the timezone; if there were no changes in the + * DST state then bail out; otherwise, apply the change + * in the offset to the DateTime + */ + g_time_zone_sink (datetime->tz, datetime); + + if (was_dst == g_time_zone_get_is_dst (datetime->tz)) + return; + + offset = old_offset - g_time_zone_get_offset (datetime->tz); + g_date_time_add_usec (datetime, offset * USEC_PER_SECOND * -1); + } +} + +/*< internal > + * g_date_time_add_ymd: + * @datetime: a #GDateTime + * @years: years to add, in the Gregorian calendar + * @months: months to add, in the Gregorian calendar + * @days: days to add, in the Gregorian calendar + * + * Updates @datetime by adding @years, @months and @days to it + * + * This function modifies the passed #GDateTime so public accessors + * should make always pass a copy + */ +static inline void +g_date_time_add_ymd (GDateTime *datetime, + gint years, + gint months, + gint days) +{ + gint y = g_date_time_get_year (datetime); + gint m = g_date_time_get_month (datetime); + gint d = g_date_time_get_day_of_month (datetime); + gint step, i; + const guint16 *max_days; + + y += years; + + /* subtract one day for leap years */ + if (GREGORIAN_LEAP (y) && m == 2) + { + if (d == 29) + d -= 1; + } + + /* add months */ + step = months > 0 ? 1 : -1; + for (i = 0; i < ABS (months); i++) + { + m += step; + + if (m < 1) + { + y -= 1; + m = 12; + } + else if (m > 12) + { + y += 1; + m = 1; + } + } + + /* clamp the days */ + max_days = days_in_months[GREGORIAN_LEAP (y) ? 1 : 0]; + if (max_days[m] < d) + d = max_days[m]; + + datetime->days = date_to_proleptic_gregorian (y, m, d); + g_date_time_add_days_internal (datetime, days); +} + static GDateTime * g_date_time_new (void) { diff --git a/glib/tests/gdatetime.c b/glib/tests/gdatetime.c index fe6808e3f..fdd62cf7d 100644 --- a/glib/tests/gdatetime.c +++ b/glib/tests/gdatetime.c @@ -484,26 +484,30 @@ test_GDateTime_add_years (void) static void test_GDateTime_add_months (void) { + GTimeZone *utc_tz = g_time_zone_new_utc (); + #define TEST_ADD_MONTHS(y,m,d,a,ny,nm,nd) G_STMT_START { \ GDateTime *dt, *dt2; \ - dt = g_date_time_new_from_date (y, m, d); \ + dt = g_date_time_new_full (y, m, d, 0, 0, 0, utc_tz); \ dt2 = g_date_time_add_months (dt, a); \ ASSERT_DATE (dt2, ny, nm, nd); \ g_date_time_unref (dt); \ g_date_time_unref (dt2); \ } G_STMT_END - TEST_ADD_MONTHS (2009, 12, 31, 1, 2010, 1, 31); - TEST_ADD_MONTHS (2009, 12, 31, 1, 2010, 1, 31); - TEST_ADD_MONTHS (2009, 6, 15, 1, 2009, 7, 15); - TEST_ADD_MONTHS (1400, 3, 1, 1, 1400, 4, 1); - TEST_ADD_MONTHS (1400, 1, 31, 1, 1400, 2, 28); - TEST_ADD_MONTHS (1400, 1, 31, 7200, 2000, 1, 31); - TEST_ADD_MONTHS (2008, 2, 29, 12, 2009, 2, 28); - TEST_ADD_MONTHS (2000, 8, 16, -5, 2000, 3, 16); - TEST_ADD_MONTHS (2000, 8, 16, -12, 1999, 8, 16); - TEST_ADD_MONTHS (2011, 2, 1, -13, 2010, 1, 1); - TEST_ADD_MONTHS (1776, 7, 4, 1200, 1876, 7, 4); + TEST_ADD_MONTHS (2009, 12, 31, 1, 2010, 1, 31); + TEST_ADD_MONTHS (2009, 12, 31, 1, 2010, 1, 31); + TEST_ADD_MONTHS (2009, 6, 15, 1, 2009, 7, 15); + TEST_ADD_MONTHS (1400, 3, 1, 1, 1400, 4, 1); + TEST_ADD_MONTHS (1400, 1, 31, 1, 1400, 2, 28); + TEST_ADD_MONTHS (1400, 1, 31, 7200, 2000, 1, 31); + TEST_ADD_MONTHS (2008, 2, 29, 12, 2009, 2, 28); + TEST_ADD_MONTHS (2000, 8, 16, -5, 2000, 3, 16); + TEST_ADD_MONTHS (2000, 8, 16, -12, 1999, 8, 16); + TEST_ADD_MONTHS (2011, 2, 1, -13, 2010, 1, 1); + TEST_ADD_MONTHS (1776, 7, 4, 1200, 1876, 7, 4); + + g_time_zone_free (utc_tz); } static void @@ -981,6 +985,44 @@ GDateTime *__dt = g_date_time_new_from_date (2009, 10, 24); \ TEST_PRINTF ("%Z", dst); } +static void +test_GDateTime_dst (void) +{ + GDateTime *dt1, *dt2; + GTimeZone *tz; + + tz = g_time_zone_new_for_name ("Europe/London"); + + /* this date has the DST state set for Europe/London */ + dt1 = g_date_time_new_full (2009, 8, 15, 3, 0, 1, tz); + g_assert (g_date_time_is_daylight_savings (dt1)); + g_assert_cmpint (g_date_time_get_utc_offset (dt1) / G_USEC_PER_SEC, ==, 3600); + g_assert_cmpint (g_date_time_get_hour (dt1), ==, 3); + + /* add 6 months to clear the DST flag and go back one hour */ + dt2 = g_date_time_add_months (dt1, 6); + g_assert (!g_date_time_is_daylight_savings (dt2)); + g_assert_cmpint (g_date_time_get_utc_offset (dt2) / G_USEC_PER_SEC, ==, 0); + g_assert_cmpint (g_date_time_get_hour (dt2), ==, 2); + + g_date_time_unref (dt2); + g_date_time_unref (dt1); + + /* now do the reverse: start with a non-DST state and move to DST */ + dt1 = g_date_time_new_full (2009, 2, 15, 2, 0, 1, tz); + g_assert (!g_date_time_is_daylight_savings (dt1)); + g_assert_cmpint (g_date_time_get_hour (dt1), ==, 2); + + dt2 = g_date_time_add_months (dt1, 6); + g_assert (g_date_time_is_daylight_savings (dt2)); + g_assert_cmpint (g_date_time_get_hour (dt2), ==, 3); + + g_date_time_unref (dt2); + g_date_time_unref (dt1); + + g_time_zone_free (tz); +} + gint main (gint argc, gchar *argv[]) @@ -1029,6 +1071,7 @@ main (gint argc, g_test_add_func ("/GDateTime/to_utc", test_GDateTime_to_utc); g_test_add_func ("/GDateTime/today", test_GDateTime_today); g_test_add_func ("/GDateTime/utc_now", test_GDateTime_utc_now); + g_test_add_func ("/GDateTime/dst", test_GDateTime_dst); return g_test_run (); }