Skip to content

Commit ec6bff0

Browse files
datetime: make timezones only affect formatting
This patch makes `datetime` handle specified `tz` or `tzoffset` properly. It means timezones are now only affect the way datetime objects are formatted instead of timestamp value they represent. The patch involves this changes in the behavior: 1. Creating/setting a `timestamp` value with `tz`/`tzoffset` results with a day time corresponding to the timestamp value plus the timezone offset. E.g if we create a datetime object with a timestamp corresponding to 9:12 o'clock in `+0000` timezone and set `tzoffset = -60` the resulting timestamp will represent 8:12 o'clock. 2. Setting a new `tz`/`tzoffset` affects the time of day represented by the datetime. E.g. if you update a datetime object of 11:12 o'clock having `tzoffset = 180` with `tzoffset = 120` the resulting datetime object will represent 10:12. Closes tarantool#10363 NO_DOC=will be in tarantool/doc#4720
1 parent c50d2bb commit ec6bff0

File tree

4 files changed

+98
-13
lines changed

4 files changed

+98
-13
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## bugfix/datetime
2+
3+
* `datetime` methods now handle timezones properly. Now timezone changes
4+
only affect the way the object is formatted and don't alter the
5+
represented timestamp (gh-10363).

src/lua/datetime.lua

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,9 @@ local function datetime_new(obj)
621621
s = s - 1
622622
fraction = fraction + 1
623623
end
624+
625+
s = s + (offset or 0) * 60
626+
624627
-- if there are separate nsec, usec, or msec provided then
625628
-- timestamp should be integer
626629
if count_usec == 0 then
@@ -939,7 +942,20 @@ local function datetime_parse_from(str, obj)
939942
-- Override timezone, if it was not specified in a parsed
940943
-- string.
941944
if date.tz == '' and date.tzoffset == 0 then
942-
datetime_set(date, { tzoffset = tzoffset, tz = tzname })
945+
local offset = nil
946+
947+
if tzoffset ~= nil then
948+
offset = get_timezone(tzoffset, 'tzoffset')
949+
check_range(offset, -720, 840, 'tzoffset')
950+
end
951+
952+
if tzname ~= nil then
953+
offset, date.tzindex = parse_tzname(date.epoch, tzname)
954+
end
955+
956+
if offset ~= nil then
957+
time_localize(date, offset)
958+
end
943959
end
944960

945961
return date, len
@@ -1152,13 +1168,23 @@ function datetime_set(self, obj)
11521168
if tzname ~= nil then
11531169
offset, self.tzindex = parse_tzname(sec_int, tzname)
11541170
end
1155-
self.epoch = utc_secs(sec_int, offset)
1171+
self.epoch = sec_int
11561172
self.nsec = nsec
11571173
self.tzoffset = offset
11581174

11591175
return self
11601176
end
11611177

1178+
-- Only timezone is changed.
1179+
if not hms and not ymd then
1180+
if tzname ~= nil then
1181+
offset, self.tzindex = parse_tzname(self.epoch, tzname)
1182+
end
1183+
1184+
self.tzoffset = offset
1185+
return self
1186+
end
1187+
11621188
-- normalize time to UTC from current timezone
11631189
time_delocalize(self)
11641190

test/app-luatest/datetime_test.lua

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,3 +2098,56 @@ for supported_by, standard_cases in pairs(UNSUPPORTED_DATETIME_FORMATS) do
20982098
end
20992099
end
21002100
end
2101+
2102+
-- Test providing a timezone doesn't affect the timestamp
2103+
-- value but instead it affects hours/minutes represented by
2104+
-- datetime objects.
2105+
--
2106+
-- The scenario has been broken before gh-10363.
2107+
pg.test_timestamp_with_tz_in_new_preserves_timestamp = function()
2108+
local now = dt.now()
2109+
local tzoffset = now.tzoffset
2110+
2111+
local now_from_timestamp_with_tzoffset = dt.new({
2112+
timestamp = now.timestamp,
2113+
tzoffset = tzoffset,
2114+
})
2115+
local now_from_timestamp_different_tzoffset = dt.new({
2116+
timestamp = now.timestamp,
2117+
tzoffset = tzoffset + 60,
2118+
})
2119+
2120+
-- Provided timestamp values aren't affected by tzoffsets.
2121+
t.assert_equals(now.timestamp, now_from_timestamp_with_tzoffset.timestamp)
2122+
t.assert_equals(now.timestamp,
2123+
now_from_timestamp_different_tzoffset.timestamp)
2124+
2125+
-- Hours are updated correspondingly to the provided
2126+
-- tzoffsets.
2127+
t.assert_equals(now.hour, now_from_timestamp_with_tzoffset.hour)
2128+
t.assert_equals(now.hour + 1, now_from_timestamp_different_tzoffset.hour)
2129+
end
2130+
2131+
-- Test setting a timezone also changes the hour represented
2132+
-- by a datetime object and a corresponding timestamp remains
2133+
-- the same.
2134+
--
2135+
-- The scenario has been broken before gh-10363.
2136+
pg.test_tz_updates_not_change_timestamps = function()
2137+
local now = dt.now()
2138+
local tzoffset = now.tzoffset
2139+
2140+
local now_with_tzoffset_plus_60 = dt.new({timestamp = now.timestamp})
2141+
:set({tzoffset = tzoffset + 60})
2142+
local now_with_tzoffset_minus_60 = dt.new({timestamp = now.timestamp})
2143+
:set({tzoffset = tzoffset - 60})
2144+
2145+
-- Timestamp values aren't affected by tzoffset changes.
2146+
t.assert_equals(now.timestamp, now_with_tzoffset_plus_60.timestamp)
2147+
t.assert_equals(now.timestamp, now_with_tzoffset_minus_60.timestamp)
2148+
2149+
-- Hours are updated correspondingly to the tzoffset
2150+
-- changes.
2151+
t.assert_equals(now.hour + 1, now_with_tzoffset_plus_60.hour)
2152+
t.assert_equals(now.hour - 1, now_with_tzoffset_minus_60.hour)
2153+
end

test/app-tap/datetime.test.lua

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2819,27 +2819,28 @@ test:test("Time :set{} operations", function(test)
28192819
'hour 6')
28202820
test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T06:12:23+0300',
28212821
'min 12, sec 23')
2822-
test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-09T06:12:23-0800',
2822+
test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-08T19:12:23-0800',
28232823
'offset -0800' )
2824-
test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T06:12:23+0800',
2824+
test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T11:12:23+0800',
28252825
'offset +0800' )
2826-
-- timestamp 1630359071.125 is 2021-08-30T21:31:11.125Z
2826+
-- Timestamp 1630359071.125 is 2021-08-30T21:31:11.125Z+0000
2827+
-- or 2021-08-31T05:31:11.125Z+0800.
28272828
test:is(tostring(ts:set{ timestamp = 1630359071.125 }),
2828-
'2021-08-30T21:31:11.125+0800', 'timestamp 1630359071.125' )
2829-
test:is(tostring(ts:set{ msec = 123}), '2021-08-30T21:31:11.123+0800',
2829+
'2021-08-31T05:31:11.125+0800', 'timestamp 1630359071.125' )
2830+
test:is(tostring(ts:set{ msec = 123}), '2021-08-31T05:31:11.123+0800',
28302831
'msec = 123')
2831-
test:is(tostring(ts:set{ usec = 123}), '2021-08-30T21:31:11.000123+0800',
2832+
test:is(tostring(ts:set{ usec = 123}), '2021-08-31T05:31:11.000123+0800',
28322833
'usec = 123')
2833-
test:is(tostring(ts:set{ nsec = 123}), '2021-08-30T21:31:11.000000123+0800',
2834+
test:is(tostring(ts:set{ nsec = 123}), '2021-08-31T05:31:11.000000123+0800',
28342835
'nsec = 123')
28352836
test:is(tostring(ts:set{timestamp = 1630359071, msec = 123}),
2836-
'2021-08-30T21:31:11.123+0800', 'timestamp + msec')
2837+
'2021-08-31T05:31:11.123+0800', 'timestamp + msec')
28372838
test:is(tostring(ts:set{timestamp = 1630359071, usec = 123}),
2838-
'2021-08-30T21:31:11.000123+0800', 'timestamp + usec')
2839+
'2021-08-31T05:31:11.000123+0800', 'timestamp + usec')
28392840
test:is(tostring(ts:set{timestamp = 1630359071, nsec = 123}),
2840-
'2021-08-30T21:31:11.000000123+0800', 'timestamp + nsec')
2841+
'2021-08-31T05:31:11.000000123+0800', 'timestamp + nsec')
28412842
test:is(tostring(ts:set{timestamp = -0.1}),
2842-
'1969-12-31T23:59:59.900+0800', 'negative timestamp')
2843+
'1970-01-01T07:59:59.900+0800', 'negative timestamp')
28432844
end)
28442845

28452846
test:test("Check :set{} and .new{} equal for all attributes", function(test)

0 commit comments

Comments
 (0)