diff --git a/changelogs/unreleased/gh-6793-datetime-set-with-tzoffset.md b/changelogs/unreleased/gh-6793-datetime-set-with-tzoffset.md new file mode 100644 index 0000000000000000000000000000000000000000..cda5cfaf41035403914b2da412950b85bfdc4df2 --- /dev/null +++ b/changelogs/unreleased/gh-6793-datetime-set-with-tzoffset.md @@ -0,0 +1,5 @@ +## 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). diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index 12e1ebab240e3324322f6dc5844d0da09d9087b5..ae10b56cf6a5a8957aeb0b63b05092d15cd51a82 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -376,6 +376,16 @@ local function utc_secs(epoch, tzoffset) return epoch - tzoffset * 60 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 -- adjust from 1970-related to 0000-related time -- then return dt in those coordinates (number of days @@ -942,14 +952,13 @@ local function datetime_totable(self) } end -local function datetime_update_dt(self, dt, new_offset) - local epoch = local_secs(self) +local function datetime_update_dt(self, dt) + local epoch = self.epoch local secs_day = epoch % SECS_PER_DAY - epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day - self.epoch = utc_secs(epoch, new_offset) + self.epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day 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 d = builtin.tnt_dt_days_in_month(y, M) elseif d > 28 then @@ -960,13 +969,13 @@ local function datetime_ymd_update(self, y, M, d, new_offset) end end local dt = dt_from_ymd_checked(y, M, d) - datetime_update_dt(self, dt, new_offset) + datetime_update_dt(self, dt) end -local function datetime_hms_update(self, h, m, s, new_offset) - local epoch = local_secs(self) +local function datetime_hms_update(self, h, m, s) + local epoch = self.epoch 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 local function datetime_set(self, obj) @@ -1040,6 +1049,18 @@ local function datetime_set(self, obj) 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 if ts ~= nil then if ymd then @@ -1060,38 +1081,31 @@ local function datetime_set(self, obj) 'if nsec, usec, or msecs provided', 2) end - self.epoch = sec_int + self.epoch = utc_secs(sec_int, offset) self.nsec = nsec + self.tzoffset = offset return self 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 + -- normalize time to UTC from current timezone + time_delocalize(self) -- .year, .month, .day if ymd then y = y or y0 M = M or M0 d = d or d0 - datetime_ymd_update(self, y, M, d, offset) + datetime_ymd_update(self, y, M, d) end -- .hour, .minute, .second 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 - self.tzoffset = offset + -- denormalize back to local timezone + time_localize(self, offset) return self end diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index 398b2848fd61872ce74cf2cfad359a786e364c95..48e4e8f7dd3b56dc8c909b929c7d6f9c4e5f301f 100755 --- a/test/app-tap/datetime.test.lua +++ b/test/app-tap/datetime.test.lua @@ -11,7 +11,7 @@ local ffi = require('ffi') --]] if jit.arch == 'arm64' then jit.off() end -test:plan(36) +test:plan(37) -- minimum supported date - -5879610-06-22 local MIN_DATE_YEAR = -5879610 @@ -804,7 +804,7 @@ local strftime_formats = { test:test("Datetime string formatting detailed", function(test) test:plan(77) 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()') for _, row in pairs(strftime_formats) do @@ -817,7 +817,7 @@ end) test:test("Datetime string parsing by format (detailed)", function(test) test:plan(68) 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()') for _, row in pairs(strftime_formats) do @@ -1667,20 +1667,54 @@ test:test("Time :set{} operations", function(test) 'hour 6') test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+0300', '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' ) - 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' ) + -- timestamp 1630359071.125 is 2021-08-30T21:31:11.125Z test:is(tostring(ts:set{ timestamp = 1630359071.125 }), - '2021-08-31T05:31:11.125+0800', 'timestamp 1630359071.125' ) - test:is(tostring(ts:set{ msec = 123}), '2021-08-31T05:31:11.123+0800', + '2021-08-30T21:31:11.125+0800', 'timestamp 1630359071.125' ) + test:is(tostring(ts:set{ msec = 123}), '2021-08-30T21:31:11.123+0800', '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') - 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') 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:plan(84)