From 6ca07285303485a4ecac8064beb329a21d4a0512 Mon Sep 17 00:00:00 2001 From: Timur Safin <tsafin@tarantool.org> Date: Wed, 21 Sep 2022 17:08:10 +0300 Subject: [PATCH] datetime: fix interval arithmetic for DST We did not take into consideration the fact that as result of date/time arithmetic we could get in a different timezone, if DST boundary has been crossed during operation. ``` tarantool> datetime.new{year=2008, month=1, day=1, tz='Europe/Moscow'} + datetime.interval.new{month=6} --- - 2008-07-01T01:00:00 Europe/Moscow ... ``` Now we resolve tzoffset at the end of operation if tzindex is not 0. Fixes #7700 NO_DOC=bugfix --- .../gh-7700-interval-arithmetic-across-dst.md | 19 +++++++++ src/lib/core/datetime.c | 26 ++++++++---- test/app-tap/datetime.test.lua | 41 ++++++++++++++++++- 3 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/gh-7700-interval-arithmetic-across-dst.md diff --git a/changelogs/unreleased/gh-7700-interval-arithmetic-across-dst.md b/changelogs/unreleased/gh-7700-interval-arithmetic-across-dst.md new file mode 100644 index 0000000000..05b1a0d0d0 --- /dev/null +++ b/changelogs/unreleased/gh-7700-interval-arithmetic-across-dst.md @@ -0,0 +1,19 @@ +## bugfix/datetime + +* Fixed interval arithmetic for boundaries crossing DST (gh-7700). + + We did not take into consideration the fact that + as result of date/time arithmetic we could get + in a different timezone, if DST boundary has been + crossed during operation. + + ``` + tarantool> datetime.new{year=2008, month=1, day=1, + tz='Europe/Moscow'} + + datetime.interval.new{month=6} + --- + - 2008-07-01T01:00:00 Europe/Moscow + ... + ``` + Now we resolve tzoffset at the end of operation if + tzindex is not 0. diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c index ab4f5ce6e5..e1e85457a9 100644 --- a/src/lib/core/datetime.c +++ b/src/lib/core/datetime.c @@ -49,18 +49,19 @@ local_secs(const struct datetime *date) /** * Resolve tzindex encoded timezone from @sa date using Olson facilities. - * @param[in] date decode input datetime value. + * @param[in] epoch decode input epoch time (in seconds). + * @param[in] tzindex use timezone index for decode. * @param[out] gmtoff return resolved timezone offset (in seconds). * @param[out] isdst return resolved daylight saving time status for the zone. */ static inline bool -datetime_timezone_lookup(const struct datetime *date, long *gmtoff, int *isdst) +epoch_timezone_lookup(int64_t epoch, int16_t tzindex, long *gmtoff, int *isdst) { - if (date->tzindex == 0) + if (tzindex == 0) return false; - struct tnt_tm tm = {.tm_epoch = date->epoch}; - if (!timezone_tzindex_lookup(date->tzindex, &tm)) + struct tnt_tm tm = {.tm_epoch = epoch}; + if (!timezone_tzindex_lookup(tzindex, &tm)) return false; *gmtoff = tm.tm_gmtoff; @@ -75,7 +76,8 @@ datetime_isdst(const struct datetime *date) int isdst = 0; long gmtoff = 0; - return datetime_timezone_lookup(date, &gmtoff, &isdst) && (isdst != 0); + epoch_timezone_lookup(date->epoch, date->tzindex, &gmtoff, &isdst); + return isdst != 0; } long @@ -84,8 +86,7 @@ datetime_gmtoff(const struct datetime *date) int isdst = 0; long gmtoff = date->tzoffset * 60; - datetime_timezone_lookup(date, &gmtoff, &isdst); - + epoch_timezone_lookup(date->epoch, date->tzindex, &gmtoff, &isdst); return gmtoff; } @@ -704,6 +705,7 @@ datetime_increment_by(struct datetime *self, int direction, int64_t dt = local_dt(secs); int nsec = self->nsec; int offset = self->tzoffset; + int tzindex = self->tzindex; bool is_ymd_updated = false; int64_t years = ival->year; @@ -789,9 +791,15 @@ datetime_increment_by(struct datetime *self, int direction, if (rc != 0) return rc; + if (tzindex != 0) { + int isdst = 0; + long gmtoff = offset * 60; + epoch_timezone_lookup(secs, tzindex, &gmtoff, &isdst); + offset = gmtoff / 60; + } self->epoch = utc_secs(secs, offset); self->nsec = nsec; - self->tzoffset = datetime_gmtoff(self) / 60; + self->tzoffset = offset; return 0; } diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index a7089fc9e7..b08b0dd26a 100755 --- a/test/app-tap/datetime.test.lua +++ b/test/app-tap/datetime.test.lua @@ -1395,11 +1395,12 @@ Matrix of addition operands eligibility and their result type | table | | | | ]] test:test("Matrix of allowed time and interval additions", function(test) - test:plan(67) + test:plan(79) -- check arithmetic with leap dates local T1970 = date.new{year = 1970, month = 1, day = 1} - local T2000 = date.new{year = 2000, month = 1, day = 1} + local T2000 = date.new{year = 2000, month = 1, day = 1, + tz = 'Europe/Moscow'} local I1 = date.interval.new{day = 1} local M2 = date.interval.new{month = 2} @@ -1462,6 +1463,42 @@ test:test("Matrix of allowed time and interval additions", function(test) test:is(tostring(Y1 + T1970), "1971-01-01T00:00:00Z", "value: Y + T") test:is(tostring(Y5 + Y1), "+6 years", "Y + Y") + -- Check winter/DST sensitive operations with intervals. + -- We use 2000 year here, because then Moscow still were + -- switching between winter and summer time. + local msk_offset = 180 -- expected winter time offset + local msd_offset = 240 -- expected daylight saving time offset + + local res = T2000 + I1 + test:is(tostring(res), "2000-01-02T00:00:00 Europe/Moscow", + "value: 2000 + I") + test:is(res.tzoffset, msk_offset, "2000-01-02T00:00:00 - winter") + + res = T2000 + i1 + test:is(tostring(res), "2000-01-02T00:00:00 Europe/Moscow", + "value: 2000 + i") + test:is(res.tzoffset, msk_offset, "2000-01-02T00:00:00 - winter") + + res = T2000 + M2 + test:is(tostring(res), "2000-03-01T00:00:00 Europe/Moscow", + "value: 2000 + 2M") + test:is(res.tzoffset, msk_offset, "2000-03-01T00:00:00 - winter") + + res = T2000 + M2 + M2 + M2 + test:is(tostring(res), "2000-07-01T00:00:00 Europe/Moscow", + "value: 2000 + 6M") + test:is(res.tzoffset, msd_offset, "2000-07-01T00:00:00 - summer") + + res = T2000 + m2 + test:is(tostring(res), "2000-03-01T00:00:00 Europe/Moscow", + "value: 2000 + 2m") + test:is(res.tzoffset, msk_offset, "2000-03-01T00:00:00 - winter") + + res = T2000 + m2 + M2 + m2 + test:is(tostring(res), "2000-07-01T00:00:00 Europe/Moscow", + "value: 2000 + 6m") + test:is(res.tzoffset, msd_offset, "2000-07-01T00:00:00 - summer") + assert_raises_like(test, expected_interval_but, function() return T1970 + 123 end) assert_raises_like(test, expected_interval_but, -- GitLab