Skip to content

Commit d56840d

Browse files
tsafinkyukhin
authored andcommitted
datetime: make date.new{} and date:set{} equivalent
Constructor date.new() and modifier date:set() should always produce same result for all attributes combinations. Fixed the problem for `timestamp` with `tzoffset`. Fixes tarantool#6793 @TarantoolBot document Title: datetime :set{} with tzoffset Constructor `date.new()` and modifier `date:set()` should always produce same result for all attributes combinations. Fixed the problem for `timestamp` with `tzoffset`. ``` tarantool> date.new{tzoffset = '+0800', timestamp = 1630359071} --- - 2021-08-30T21:31:11+0800 ... tarantool> date.new():set{tzoffset = '+0800', timestamp = 1630359071} --- - 2021-08-30T21:31:11+0800 ... ```
1 parent 12cf998 commit d56840d

File tree

3 files changed

+86
-33
lines changed

3 files changed

+86
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## bugfix/datetime
2+
3+
* Fixed a bug in datetime module when `date:set{tzoffset=XXX}` was not
4+
producing the same result with `date.new{tzoffset=XXX}` for the same
5+
set of attributes passed (gh-6793).

src/lua/datetime.lua

+38-24
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,16 @@ local function utc_secs(epoch, tzoffset)
376376
return epoch - tzoffset * 60
377377
end
378378

379+
local function time_delocalize(self)
380+
self.epoch = local_secs(self)
381+
self.tzoffset = 0
382+
end
383+
384+
local function time_localize(self, offset)
385+
self.epoch = utc_secs(self.epoch, offset)
386+
self.tzoffset = offset
387+
end
388+
379389
-- get epoch seconds, shift to the local timezone
380390
-- adjust from 1970-related to 0000-related time
381391
-- then return dt in those coordinates (number of days
@@ -942,14 +952,13 @@ local function datetime_totable(self)
942952
}
943953
end
944954

945-
local function datetime_update_dt(self, dt, new_offset)
946-
local epoch = local_secs(self)
955+
local function datetime_update_dt(self, dt)
956+
local epoch = self.epoch
947957
local secs_day = epoch % SECS_PER_DAY
948-
epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day
949-
self.epoch = utc_secs(epoch, new_offset)
958+
self.epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day
950959
end
951960

952-
local function datetime_ymd_update(self, y, M, d, new_offset)
961+
local function datetime_ymd_update(self, y, M, d)
953962
if d < 0 then
954963
d = builtin.tnt_dt_days_in_month(y, M)
955964
elseif d > 28 then
@@ -960,13 +969,13 @@ local function datetime_ymd_update(self, y, M, d, new_offset)
960969
end
961970
end
962971
local dt = dt_from_ymd_checked(y, M, d)
963-
datetime_update_dt(self, dt, new_offset)
972+
datetime_update_dt(self, dt)
964973
end
965974

966-
local function datetime_hms_update(self, h, m, s, new_offset)
967-
local epoch = local_secs(self)
975+
local function datetime_hms_update(self, h, m, s)
976+
local epoch = self.epoch
968977
local secs_day = epoch - (epoch % SECS_PER_DAY)
969-
self.epoch = utc_secs(secs_day + h * 3600 + m * 60 + s, new_offset)
978+
self.epoch = secs_day + h * 3600 + m * 60 + s
970979
end
971980

972981
local function datetime_set(self, obj)
@@ -1040,6 +1049,18 @@ local function datetime_set(self, obj)
10401049
end
10411050
end
10421051

1052+
local offset = obj.tzoffset
1053+
if offset ~= nil then
1054+
offset = get_timezone(offset, 'tzoffset')
1055+
check_range(offset, -720, 840, 'tzoffset')
1056+
end
1057+
offset = offset or self.tzoffset
1058+
1059+
local tzname = obj.tz
1060+
if tzname ~= nil then
1061+
offset, self.tzindex = parse_tzname(tzname)
1062+
end
1063+
10431064
local ts = obj.timestamp
10441065
if ts ~= nil then
10451066
if ymd then
@@ -1060,38 +1081,31 @@ local function datetime_set(self, obj)
10601081
'if nsec, usec, or msecs provided', 2)
10611082
end
10621083

1063-
self.epoch = sec_int
1084+
self.epoch = utc_secs(sec_int, offset)
10641085
self.nsec = nsec
1086+
self.tzoffset = offset
10651087

10661088
return self
10671089
end
10681090

1069-
local offset = obj.tzoffset
1070-
if offset ~= nil then
1071-
offset = get_timezone(offset, 'tzoffset')
1072-
check_range(offset, -720, 840, 'tzoffset')
1073-
end
1074-
offset = offset or self.tzoffset
1075-
1076-
local tzname = obj.tz
1077-
if tzname ~= nil then
1078-
offset, self.tzindex = parse_tzname(tzname)
1079-
end
1091+
-- normalize time to UTC from current timezone
1092+
time_delocalize(self)
10801093

10811094
-- .year, .month, .day
10821095
if ymd then
10831096
y = y or y0
10841097
M = M or M0
10851098
d = d or d0
1086-
datetime_ymd_update(self, y, M, d, offset)
1099+
datetime_ymd_update(self, y, M, d)
10871100
end
10881101

10891102
-- .hour, .minute, .second
10901103
if hms then
1091-
datetime_hms_update(self, h or h0, m or m0, sec or sec0, offset)
1104+
datetime_hms_update(self, h or h0, m or m0, sec or sec0)
10921105
end
10931106

1094-
self.tzoffset = offset
1107+
-- denormalize back to local timezone
1108+
time_localize(self, offset)
10951109

10961110
return self
10971111
end

test/app-tap/datetime.test.lua

+43-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ local ffi = require('ffi')
1111
--]]
1212
if jit.arch == 'arm64' then jit.off() end
1313

14-
test:plan(36)
14+
test:plan(37)
1515

1616
-- minimum supported date - -5879610-06-22
1717
local MIN_DATE_YEAR = -5879610
@@ -804,7 +804,7 @@ local strftime_formats = {
804804
test:test("Datetime string formatting detailed", function(test)
805805
test:plan(77)
806806
local T = date.new{ timestamp = 0.125 }
807-
T:set{ tzoffset = 180 }
807+
T:set{ hour = 3, tzoffset = 180 }
808808
test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
809809

810810
for _, row in pairs(strftime_formats) do
@@ -817,7 +817,7 @@ end)
817817
test:test("Datetime string parsing by format (detailed)", function(test)
818818
test:plan(68)
819819
local T = date.new{ timestamp = 0.125 }
820-
T:set{ tzoffset = 180 }
820+
T:set{ hour = 3, tzoffset = 180 }
821821
test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
822822

823823
for _, row in pairs(strftime_formats) do
@@ -1667,20 +1667,54 @@ test:test("Time :set{} operations", function(test)
16671667
'hour 6')
16681668
test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+0300',
16691669
'min 12, sec 23')
1670-
test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-08T17:12:23-0800',
1670+
test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-09T04:12:23-0800',
16711671
'offset -0800' )
1672-
test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T09:12:23+0800',
1672+
test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T04:12:23+0800',
16731673
'offset +0800' )
1674+
-- timestamp 1630359071.125 is 2021-08-30T21:31:11.125Z
16741675
test:is(tostring(ts:set{ timestamp = 1630359071.125 }),
1675-
'2021-08-31T05:31:11.125+0800', 'timestamp 1630359071.125' )
1676-
test:is(tostring(ts:set{ msec = 123}), '2021-08-31T05:31:11.123+0800',
1676+
'2021-08-30T21:31:11.125+0800', 'timestamp 1630359071.125' )
1677+
test:is(tostring(ts:set{ msec = 123}), '2021-08-30T21:31:11.123+0800',
16771678
'msec = 123')
1678-
test:is(tostring(ts:set{ usec = 123}), '2021-08-31T05:31:11.000123+0800',
1679+
test:is(tostring(ts:set{ usec = 123}), '2021-08-30T21:31:11.000123+0800',
16791680
'usec = 123')
1680-
test:is(tostring(ts:set{ nsec = 123}), '2021-08-31T05:31:11.000000123+0800',
1681+
test:is(tostring(ts:set{ nsec = 123}), '2021-08-30T21:31:11.000000123+0800',
16811682
'nsec = 123')
16821683
end)
16831684

1685+
test:test("Check :set{} and .new{} equal for all attributes", function(test)
1686+
test:plan(11)
1687+
local ts, ts2
1688+
local obj = {}
1689+
local attribs = {
1690+
{'year', 2000},
1691+
{'month', 11},
1692+
{'day', 30},
1693+
{'hour', 6},
1694+
{'min', 12},
1695+
{'sec', 23},
1696+
{'tzoffset', -8*60},
1697+
{'tzoffset', '+0800'},
1698+
{'tz', 'MSK'},
1699+
{'nsec', 560000},
1700+
}
1701+
for _, row in pairs(attribs) do
1702+
local key, value = unpack(row)
1703+
obj[key] = value
1704+
ts = date.new(obj)
1705+
ts2 = date.new():set(obj)
1706+
test:is(ts, ts2, ('[%s] = %s (%s = %s)'):
1707+
format(key, tostring(value), tostring(ts), tostring(ts2)))
1708+
end
1709+
1710+
obj = {timestamp = 1630359071.125, tzoffset = '+0800'}
1711+
ts = date.new(obj)
1712+
ts2 = date.new():set(obj)
1713+
test:is(ts, ts2, ('timestamp+tzoffset (%s = %s)'):
1714+
format(tostring(ts), tostring(ts2)))
1715+
end)
1716+
1717+
16841718
test:test("Time invalid :set{} operations", function(test)
16851719
test:plan(84)
16861720

0 commit comments

Comments
 (0)