Skip to content
Snippets Groups Projects
Commit d56840d7 authored by Timur Safin's avatar Timur Safin Committed by Kirill Yukhin
Browse files

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 #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
...
```
parent 12cf9980
No related branches found
No related tags found
No related merge requests found
## bugfix/datetime
* Fixed a bug in datetime module when `date:set{tzoffset=XXX}` was not
producing the same result with `date.new{tzoffset=XXX}` for the same
set of attributes passed (gh-6793).
...@@ -376,6 +376,16 @@ local function utc_secs(epoch, tzoffset) ...@@ -376,6 +376,16 @@ local function utc_secs(epoch, tzoffset)
return epoch - tzoffset * 60 return epoch - tzoffset * 60
end end
local function time_delocalize(self)
self.epoch = local_secs(self)
self.tzoffset = 0
end
local function time_localize(self, offset)
self.epoch = utc_secs(self.epoch, offset)
self.tzoffset = offset
end
-- get epoch seconds, shift to the local timezone -- get epoch seconds, shift to the local timezone
-- adjust from 1970-related to 0000-related time -- adjust from 1970-related to 0000-related time
-- then return dt in those coordinates (number of days -- then return dt in those coordinates (number of days
...@@ -942,14 +952,13 @@ local function datetime_totable(self) ...@@ -942,14 +952,13 @@ local function datetime_totable(self)
} }
end end
local function datetime_update_dt(self, dt, new_offset) local function datetime_update_dt(self, dt)
local epoch = local_secs(self) local epoch = self.epoch
local secs_day = epoch % SECS_PER_DAY local secs_day = epoch % SECS_PER_DAY
epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day self.epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day
self.epoch = utc_secs(epoch, new_offset)
end end
local function datetime_ymd_update(self, y, M, d, new_offset) local function datetime_ymd_update(self, y, M, d)
if d < 0 then if d < 0 then
d = builtin.tnt_dt_days_in_month(y, M) d = builtin.tnt_dt_days_in_month(y, M)
elseif d > 28 then elseif d > 28 then
...@@ -960,13 +969,13 @@ local function datetime_ymd_update(self, y, M, d, new_offset) ...@@ -960,13 +969,13 @@ local function datetime_ymd_update(self, y, M, d, new_offset)
end end
end end
local dt = dt_from_ymd_checked(y, M, d) local dt = dt_from_ymd_checked(y, M, d)
datetime_update_dt(self, dt, new_offset) datetime_update_dt(self, dt)
end end
local function datetime_hms_update(self, h, m, s, new_offset) local function datetime_hms_update(self, h, m, s)
local epoch = local_secs(self) local epoch = self.epoch
local secs_day = epoch - (epoch % SECS_PER_DAY) local secs_day = epoch - (epoch % SECS_PER_DAY)
self.epoch = utc_secs(secs_day + h * 3600 + m * 60 + s, new_offset) self.epoch = secs_day + h * 3600 + m * 60 + s
end end
local function datetime_set(self, obj) local function datetime_set(self, obj)
...@@ -1040,6 +1049,18 @@ local function datetime_set(self, obj) ...@@ -1040,6 +1049,18 @@ local function datetime_set(self, obj)
end end
end end
local offset = obj.tzoffset
if offset ~= nil then
offset = get_timezone(offset, 'tzoffset')
check_range(offset, -720, 840, 'tzoffset')
end
offset = offset or self.tzoffset
local tzname = obj.tz
if tzname ~= nil then
offset, self.tzindex = parse_tzname(tzname)
end
local ts = obj.timestamp local ts = obj.timestamp
if ts ~= nil then if ts ~= nil then
if ymd then if ymd then
...@@ -1060,38 +1081,31 @@ local function datetime_set(self, obj) ...@@ -1060,38 +1081,31 @@ local function datetime_set(self, obj)
'if nsec, usec, or msecs provided', 2) 'if nsec, usec, or msecs provided', 2)
end end
self.epoch = sec_int self.epoch = utc_secs(sec_int, offset)
self.nsec = nsec self.nsec = nsec
self.tzoffset = offset
return self return self
end end
local offset = obj.tzoffset -- normalize time to UTC from current timezone
if offset ~= nil then time_delocalize(self)
offset = get_timezone(offset, 'tzoffset')
check_range(offset, -720, 840, 'tzoffset')
end
offset = offset or self.tzoffset
local tzname = obj.tz
if tzname ~= nil then
offset, self.tzindex = parse_tzname(tzname)
end
-- .year, .month, .day -- .year, .month, .day
if ymd then if ymd then
y = y or y0 y = y or y0
M = M or M0 M = M or M0
d = d or d0 d = d or d0
datetime_ymd_update(self, y, M, d, offset) datetime_ymd_update(self, y, M, d)
end end
-- .hour, .minute, .second -- .hour, .minute, .second
if hms then if hms then
datetime_hms_update(self, h or h0, m or m0, sec or sec0, offset) datetime_hms_update(self, h or h0, m or m0, sec or sec0)
end end
self.tzoffset = offset -- denormalize back to local timezone
time_localize(self, offset)
return self return self
end end
......
...@@ -11,7 +11,7 @@ local ffi = require('ffi') ...@@ -11,7 +11,7 @@ local ffi = require('ffi')
--]] --]]
if jit.arch == 'arm64' then jit.off() end if jit.arch == 'arm64' then jit.off() end
test:plan(36) test:plan(37)
-- minimum supported date - -5879610-06-22 -- minimum supported date - -5879610-06-22
local MIN_DATE_YEAR = -5879610 local MIN_DATE_YEAR = -5879610
...@@ -804,7 +804,7 @@ local strftime_formats = { ...@@ -804,7 +804,7 @@ local strftime_formats = {
test:test("Datetime string formatting detailed", function(test) test:test("Datetime string formatting detailed", function(test)
test:plan(77) test:plan(77)
local T = date.new{ timestamp = 0.125 } local T = date.new{ timestamp = 0.125 }
T:set{ tzoffset = 180 } T:set{ hour = 3, tzoffset = 180 }
test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()') test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
for _, row in pairs(strftime_formats) do for _, row in pairs(strftime_formats) do
...@@ -817,7 +817,7 @@ end) ...@@ -817,7 +817,7 @@ end)
test:test("Datetime string parsing by format (detailed)", function(test) test:test("Datetime string parsing by format (detailed)", function(test)
test:plan(68) test:plan(68)
local T = date.new{ timestamp = 0.125 } local T = date.new{ timestamp = 0.125 }
T:set{ tzoffset = 180 } T:set{ hour = 3, tzoffset = 180 }
test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()') test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
for _, row in pairs(strftime_formats) do for _, row in pairs(strftime_formats) do
...@@ -1667,20 +1667,54 @@ test:test("Time :set{} operations", function(test) ...@@ -1667,20 +1667,54 @@ test:test("Time :set{} operations", function(test)
'hour 6') 'hour 6')
test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+0300', test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+0300',
'min 12, sec 23') 'min 12, sec 23')
test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-08T17:12:23-0800', test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-09T04:12:23-0800',
'offset -0800' ) 'offset -0800' )
test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T09:12:23+0800', test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T04:12:23+0800',
'offset +0800' ) 'offset +0800' )
-- timestamp 1630359071.125 is 2021-08-30T21:31:11.125Z
test:is(tostring(ts:set{ timestamp = 1630359071.125 }), test:is(tostring(ts:set{ timestamp = 1630359071.125 }),
'2021-08-31T05:31:11.125+0800', 'timestamp 1630359071.125' ) '2021-08-30T21:31:11.125+0800', 'timestamp 1630359071.125' )
test:is(tostring(ts:set{ msec = 123}), '2021-08-31T05:31:11.123+0800', test:is(tostring(ts:set{ msec = 123}), '2021-08-30T21:31:11.123+0800',
'msec = 123') 'msec = 123')
test:is(tostring(ts:set{ usec = 123}), '2021-08-31T05:31:11.000123+0800', test:is(tostring(ts:set{ usec = 123}), '2021-08-30T21:31:11.000123+0800',
'usec = 123') 'usec = 123')
test:is(tostring(ts:set{ nsec = 123}), '2021-08-31T05:31:11.000000123+0800', test:is(tostring(ts:set{ nsec = 123}), '2021-08-30T21:31:11.000000123+0800',
'nsec = 123') 'nsec = 123')
end) end)
test:test("Check :set{} and .new{} equal for all attributes", function(test)
test:plan(11)
local ts, ts2
local obj = {}
local attribs = {
{'year', 2000},
{'month', 11},
{'day', 30},
{'hour', 6},
{'min', 12},
{'sec', 23},
{'tzoffset', -8*60},
{'tzoffset', '+0800'},
{'tz', 'MSK'},
{'nsec', 560000},
}
for _, row in pairs(attribs) do
local key, value = unpack(row)
obj[key] = value
ts = date.new(obj)
ts2 = date.new():set(obj)
test:is(ts, ts2, ('[%s] = %s (%s = %s)'):
format(key, tostring(value), tostring(ts), tostring(ts2)))
end
obj = {timestamp = 1630359071.125, tzoffset = '+0800'}
ts = date.new(obj)
ts2 = date.new():set(obj)
test:is(ts, ts2, ('timestamp+tzoffset (%s = %s)'):
format(tostring(ts), tostring(ts2)))
end)
test:test("Time invalid :set{} operations", function(test) test:test("Time invalid :set{} operations", function(test)
test:plan(84) test:plan(84)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment