diff --git a/changelogs/unreleased/gh-10363-datetime-handle-timezone.md b/changelogs/unreleased/gh-10363-datetime-handle-timezone.md new file mode 100644 index 000000000000..254abfc564e5 --- /dev/null +++ b/changelogs/unreleased/gh-10363-datetime-handle-timezone.md @@ -0,0 +1,5 @@ +## bugfix/datetime + +* `datetime` methods now handle timezones properly. Now timezone changes + only affect the way the object is formatted and don't alter the + represented timestamp (gh-10363). diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index 8b59ff325f82..acd87876e023 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -621,6 +621,9 @@ local function datetime_new(obj) s = s - 1 fraction = fraction + 1 end + + s = s + (offset or 0) * 60 + -- if there are separate nsec, usec, or msec provided then -- timestamp should be integer if count_usec == 0 then @@ -939,7 +942,20 @@ local function datetime_parse_from(str, obj) -- Override timezone, if it was not specified in a parsed -- string. if date.tz == '' and date.tzoffset == 0 then - datetime_set(date, { tzoffset = tzoffset, tz = tzname }) + local offset = nil + + if tzoffset ~= nil then + offset = get_timezone(tzoffset, 'tzoffset') + check_range(offset, -720, 840, 'tzoffset') + end + + if tzname ~= nil then + offset, date.tzindex = parse_tzname(date.epoch, tzname) + end + + if offset ~= nil then + time_localize(date, offset) + end end return date, len @@ -1152,13 +1168,23 @@ function datetime_set(self, obj) if tzname ~= nil then offset, self.tzindex = parse_tzname(sec_int, tzname) end - self.epoch = utc_secs(sec_int, offset) + self.epoch = sec_int self.nsec = nsec self.tzoffset = offset return self end + -- Only timezone is changed. + if not hms and not ymd then + if tzname ~= nil then + offset, self.tzindex = parse_tzname(self.epoch, tzname) + end + + self.tzoffset = offset + return self + end + -- normalize time to UTC from current timezone time_delocalize(self) diff --git a/test/app-luatest/datetime_test.lua b/test/app-luatest/datetime_test.lua index ad4f2dcad5de..621dd527c722 100644 --- a/test/app-luatest/datetime_test.lua +++ b/test/app-luatest/datetime_test.lua @@ -2098,3 +2098,56 @@ for supported_by, standard_cases in pairs(UNSUPPORTED_DATETIME_FORMATS) do end end end + +-- Test providing a timezone doesn't affect the timestamp +-- value but instead it affects hours/minutes represented by +-- datetime objects. +-- +-- The scenario has been broken before gh-10363. +pg.test_timestamp_with_tz_in_new_preserves_timestamp = function() + local now = dt.now() + local tzoffset = now.tzoffset + + local now_from_timestamp_with_tzoffset = dt.new({ + timestamp = now.timestamp, + tzoffset = tzoffset, + }) + local now_from_timestamp_different_tzoffset = dt.new({ + timestamp = now.timestamp, + tzoffset = tzoffset + 60, + }) + + -- Provided timestamp values aren't affected by tzoffsets. + t.assert_equals(now.timestamp, now_from_timestamp_with_tzoffset.timestamp) + t.assert_equals(now.timestamp, + now_from_timestamp_different_tzoffset.timestamp) + + -- Hours are updated correspondingly to the provided + -- tzoffsets. + t.assert_equals(now.hour, now_from_timestamp_with_tzoffset.hour) + t.assert_equals(now.hour + 1, now_from_timestamp_different_tzoffset.hour) +end + +-- Test setting a timezone also changes the hour represented +-- by a datetime object and a corresponding timestamp remains +-- the same. +-- +-- The scenario has been broken before gh-10363. +pg.test_tz_updates_not_change_timestamps = function() + local now = dt.now() + local tzoffset = now.tzoffset + + local now_with_tzoffset_plus_60 = dt.new({timestamp = now.timestamp}) + :set({tzoffset = tzoffset + 60}) + local now_with_tzoffset_minus_60 = dt.new({timestamp = now.timestamp}) + :set({tzoffset = tzoffset - 60}) + + -- Timestamp values aren't affected by tzoffset changes. + t.assert_equals(now.timestamp, now_with_tzoffset_plus_60.timestamp) + t.assert_equals(now.timestamp, now_with_tzoffset_minus_60.timestamp) + + -- Hours are updated correspondingly to the tzoffset + -- changes. + t.assert_equals(now.hour + 1, now_with_tzoffset_plus_60.hour) + t.assert_equals(now.hour - 1, now_with_tzoffset_minus_60.hour) +end diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index e77d64fd817c..2a8b2efc55bc 100755 --- a/test/app-tap/datetime.test.lua +++ b/test/app-tap/datetime.test.lua @@ -2819,27 +2819,28 @@ test:test("Time :set{} operations", function(test) 'hour 6') test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T06:12:23+0300', 'min 12, sec 23') - test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-09T06:12:23-0800', + test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-08T19:12:23-0800', 'offset -0800' ) - test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T06:12:23+0800', + test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T11:12:23+0800', 'offset +0800' ) - -- timestamp 1630359071.125 is 2021-08-30T21:31:11.125Z + -- Timestamp 1630359071.125 is 2021-08-30T21:31:11.125+0000 + -- or 2021-08-31T05:31:11.125+0800. test:is(tostring(ts:set{ timestamp = 1630359071.125 }), - '2021-08-30T21:31:11.125+0800', 'timestamp 1630359071.125' ) - test:is(tostring(ts:set{ msec = 123}), '2021-08-30T21:31:11.123+0800', + '2021-08-31T05:31:11.125+0800', 'timestamp 1630359071.125' ) + test:is(tostring(ts:set{ msec = 123}), '2021-08-31T05:31:11.123+0800', 'msec = 123') - test:is(tostring(ts:set{ usec = 123}), '2021-08-30T21:31:11.000123+0800', + test:is(tostring(ts:set{ usec = 123}), '2021-08-31T05:31:11.000123+0800', 'usec = 123') - test:is(tostring(ts:set{ nsec = 123}), '2021-08-30T21:31:11.000000123+0800', + test:is(tostring(ts:set{ nsec = 123}), '2021-08-31T05:31:11.000000123+0800', 'nsec = 123') test:is(tostring(ts:set{timestamp = 1630359071, msec = 123}), - '2021-08-30T21:31:11.123+0800', 'timestamp + msec') + '2021-08-31T05:31:11.123+0800', 'timestamp + msec') test:is(tostring(ts:set{timestamp = 1630359071, usec = 123}), - '2021-08-30T21:31:11.000123+0800', 'timestamp + usec') + '2021-08-31T05:31:11.000123+0800', 'timestamp + usec') test:is(tostring(ts:set{timestamp = 1630359071, nsec = 123}), - '2021-08-30T21:31:11.000000123+0800', 'timestamp + nsec') + '2021-08-31T05:31:11.000000123+0800', 'timestamp + nsec') test:is(tostring(ts:set{timestamp = -0.1}), - '1969-12-31T23:59:59.900+0800', 'negative timestamp') + '1970-01-01T07:59:59.900+0800', 'negative timestamp') end) test:test("Check :set{} and .new{} equal for all attributes", function(test)