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
This commit is contained in:
Emmanuele Bassi 2010-09-15 13:55:36 +01:00 committed by Ryan Lortie
parent 0746f74036
commit 4bac6613cf
2 changed files with 220 additions and 117 deletions

View File

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

View File

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