From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from [87.239.111.99] (localhost [127.0.0.1]) by dev.tarantool.org (Postfix) with ESMTP id 1A43A6EC5A; Thu, 15 Jul 2021 11:25:27 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 1A43A6EC5A DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1626337527; bh=JA+LPNWgYXBK7JpF7M9r/t9mwO9iP9yDWkmegbBlM7A=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc: From; b=qfHiebQr+n1H/QSbFCyGHlB4eIf/5MMOJXqGPzrMDH7MQUg0cgv2+fLH15Uea4SKx I26o4y5ieCzHY4hiHihHw//IxDPjySiBivwmw8cEPjZXv0LBIKm6g9Kk7X+qHxmDFf tlhMxxVz1B7HHIYotUO/ZUvSxIVLQSR/i0MFqq14= Received: from smtp57.i.mail.ru (smtp57.i.mail.ru [217.69.128.37]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id 4752B6F3D0 for ; Thu, 15 Jul 2021 11:19:06 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 4752B6F3D0 Received: by smtp57.i.mail.ru with esmtpa (envelope-from ) id 1m3waG-00070N-Pw; Thu, 15 Jul 2021 11:19:05 +0300 To: v.shpilevoy@tarantool.org Date: Thu, 15 Jul 2021 11:18:19 +0300 Message-Id: <57fc49a61811f360e129aefa654c191171066d4f.1626335242.git.tsafin@tarantool.org> X-Mailer: git-send-email 2.29.2 In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-4EC0790: 10 X-7564579A: B8F34718100C35BD X-77F55803: 4F1203BC0FB41BD941C43E597735A9C30288BCF456A452ECBCD3030350682103182A05F538085040729886DB3C84F5E82DCD2DECEF5311648857EFDD3397D2726410096E3296F509 X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE7466896EF24E80F12EA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F79006370BEBC60587DC626C8638F802B75D45FF36EB9D2243A4F8B5A6FCA7DBDB1FC311F39EFFDF887939037866D6147AF826D82DEDD80CB4E9D8C89B31D9E3F3DF7DE9117882F4460429724CE54428C33FAD305F5C1EE8F4F765FCAA867293B0326636D2E47CDBA5A96583BD4B6F7A4D31EC0BC014FD901B82EE079FA2833FD35BB23D27C277FBC8AE2E8BAA867293B0326636D2E47CDBA5A96583BA9C0B312567BB2376E601842F6C81A19E625A9149C048EEC24E1E72F37C03A020DA3B6C150F7642D8FC6C240DEA7642DBF02ECDB25306B2B78CF848AE20165D0A6AB1C7CE11FEE3E753FA5741D1AD02C0837EA9F3D19764C4224003CC836476EA7A3FFF5B025636E2021AF6380DFAD1A18204E546F3947CB11811A4A51E3B096D1867E19FE1407959CC434672EE6371089D37D7C0E48F6C8AA50765F7900637AD0424077D726551EFF80C71ABB335746BA297DBC24807EABDAD6C7F3747799A X-C1DE0DAB: C20DE7B7AB408E4181F030C43753B8186998911F362727C414F749A5E30D975CE68746B1F2AB10C60012442FA62CA4C7DD3D35F8FACDEF529C2B6934AE262D3EE7EAB7254005DCED7532B743992DF240BDC6A1CF3F042BAD6DF99611D93F60EF3033054805BDE987699F904B3F4130E343918A1A30D5E7FCCB5012B2E24CD356 X-C8649E89: 4E36BF7865823D7055A7F0CF078B5EC49A30900B95165D3467D08F30473A5842211F7F1F01A5F95342B65A44B424954FC78F43B41119CBFC7455BD287801D28F1D7E09C32AA3244C3713B9457D35770344A6734B2B472FF1E8FBBEFAE1C4874C83B48618A63566E0 X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu530nj6fImhcD4MUrOEAnl0W826KZ9Q+tr5ycPtXkTV4k65bRjmOUUP8cvGozZ33TWg5HZplvhhXbhDGzqmQDTd6OAevLeAnq3Ra9uf7zvY2zzsIhlcp/Y7m53TZgf2aB4JOg4gkr2biojbL9S8ysBdXjnYxxC9kpVbw8rCzULmkdW X-Mailru-Sender: B5B6A6EBBD94DAD8DA84A184D75F19DB2E7B7B20C3E75D0F52D48A542406832523B38D4FA7FB58851EC9E4A2C82A33BC8C24925A86E657CE0C70AEE3C9A96FBAB3D7EE8ED63280BE112434F685709FCF0DA7A0AF5A3A8387 X-Mras: Ok Subject: [Tarantool-patches] [RFC PATCH 13/13] lua: complete time duration support X-BeenThere: tarantool-patches@dev.tarantool.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Timur Safin via Tarantool-patches Reply-To: Timur Safin Cc: tarantool-patches@dev.tarantool.org Errors-To: tarantool-patches-bounces@dev.tarantool.org Sender: "Tarantool-patches" * reworked code which adds/subs years and months to use `dt_add_years()` and `dt_add_months()` instead of manual plumbing as before * introduced `:add{}` and `:sub{}` methods to datetime object to add or substract complex periods of times * implemented stringization of period objects --- src/exports.h | 3 + src/lua/datetime.lua | 276 ++++++++++++++++++++++++--------- test/app-tap/datetime.test.lua | 80 ++++++++-- 3 files changed, 280 insertions(+), 79 deletions(-) diff --git a/src/exports.h b/src/exports.h index 7397010e0..3388fb685 100644 --- a/src/exports.h +++ b/src/exports.h @@ -533,6 +533,9 @@ EXPORT(uuid_nil) EXPORT(uuid_unpack) EXPORT(datetime_unpack) EXPORT(datetime_pack) +EXPORT(dt_add_months) +EXPORT(dt_add_years) +EXPORT(dt_add_quarters) EXPORT(dt_from_rdn) EXPORT(dt_from_yd) EXPORT(dt_from_ymd) diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index 698c55ebd..d28f931f6 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -216,6 +216,13 @@ local function duration_minutes_new(m) return o end +local function duration_seconds_new(s) + local o = ffi.new(duration_t) + o.nsec = s % 1 * 1e9 + o.secs = s - (s % 1) + return o +end + local function datetime_eq(lhs, rhs) -- we usually don't need to check nullness -- but older tarantool console will call us checking for equality to nil @@ -246,31 +253,141 @@ local function duration_serialize(self) return { secs = self.secs, nsec = self.nsec } end +local function local_rd(o) + return math.floor(o.secs / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET +end + +local function local_dt(o) + return cdt.dt_from_rdn(local_rd(o)) +end + +-- addition or subtraction from date/time of a given period described via table +-- direction should be +1 or -1 +local function duration_increment(self, o, direction) + assert(direction == -1 or direction == 1) + assert(ffi.typeof(self) == datetime_t) + assert(type(o) == 'table') + + local ym_updated = false + local dhms_updated = false + + local dt = local_dt(self) + local secs, nsec + secs, nsec = self.secs, self.nsec + + for key, value in pairs(o) do + local handlers = { + years = function(v) + assert(v > 0 and v < 10000) + dt = cdt.dt_add_years(dt, direction * v, cdt.DT_LIMIT) + ym_updated = true + end, + + months = function(v) + assert(v > 0 and v < 13 ) + dt = cdt.dt_add_months(dt, direction * v, cdt.DT_LIMIT) + ym_updated = true + end, + + weeks = function(v) + assert(v > 0 and v < 32) + secs = secs + direction * 7 * v * SECS_PER_DAY + dhms_updated = true + end, + + days = function(v) + assert(v > 0 and v < 32) + secs = secs + direction * v * SECS_PER_DAY + dhms_updated = true + end, + + hours = function(v) + assert(v >= 0 and v < 24) + secs = secs + direction * 60 * 60 * v + dhms_updated = true + end, + + minutes = function(v) + assert(v >= 0 and v < 60) + secs = secs + direction * 60 * v + end, + + seconds = function(v) + assert(v >= 0 and v < 61) + local s, frac + frac = v % 1 + if frac > 0 then + s = v - (v % 1) + else + s = v + end + secs = secs + direction * s + nsec = nsec + direction * frac * 1e9 -- convert fraction to nanoseconds + dhms_updated = true + end, + } + handlers[key](value) + end + + -- normalize values + if nsec < 0 then + nsec = nsec + NANOS_PER_SEC + secs = secs - 1 + elseif nsec >= NANOS_PER_SEC then + nsec = nsec - NANOS_PER_SEC + secs = secs + 1 + end + + -- .days, .hours, .minutes, .seconds + if dhms_updated then + self.secs = secs + self.nsec = nsec + end + + -- .years, .months updated + if ym_updated then + self.secs = (cdt.dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY + + secs % SECS_PER_DAY + end + + return self +end + local datetime_index = function(self, key) local attributes = { timestamp = function(self) - return tonumber(self.secs + self.nsec / 1e9) + return self.secs + self.nsec / 1e9 end, nanoseconds = function(self) - return tonumber(self.secs * 1e9 + self.nsec) + return self.secs * 1e9 + self.nsec end, microseconds = function(self) - return tonumber(self.secs * 1e6 + self.nsec / 1e3) + return self.secs * 1e6 + self.nsec / 1e3 end, milliseconds = function(self) - return tonumber(self.secs * 1e3 + self.nsec / 1e6) + return self.secs * 1e3 + self.nsec / 1e6 end, seconds = function(self) - return tonumber(self.secs + self.nsec / 1e9) + return self.secs + self.nsec / 1e9 end, minutes = function(self) - return tonumber((self.secs + self.nsec / 1e9) / 60 % 60) + return (self.secs + self.nsec / 1e9) / 60 end, hours = function(self) - return tonumber((self.secs + self.nsec / 1e9) / (60 * 60)) + return (self.secs + self.nsec / 1e9) / (60 * 60) end, days = function(self) - return tonumber((self.secs + self.nsec / 1e9) / (60 * 60)) / 24 + return (self.secs + self.nsec / 1e9) / (60 * 60) / 24 + end, + add = function(self) + return function(self, o) + return duration_increment(self, o, 1) + end + end, + sub = function(self) + return function(self, o) + return duration_increment(self, o, -1) + end end, } return attributes[key] ~= nil and attributes[key](self) or nil @@ -284,14 +401,6 @@ local function datetime_new_raw(secs, nsec, offset) return dt_obj end -local function local_rd(o) - return math.floor(o.secs / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET -end - -local function local_dt(o) - return cdt.dt_from_rdn(local_rd(o)) -end - local function mk_timestamp(dt, sp, fp, offset) local epochV = dt ~= nil and (cdt.dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY or 0 local spV = sp ~= nil and sp or 0 @@ -367,11 +476,12 @@ local function datetime_new(o) second = function(v) assert(v >= 0 and v < 61) frac = v % 1 - if frac then + if frac > 0 then s = v - (v % 1) else s = v end + frac = frac * 1e9 -- convert fraction to nanoseconds hms = true end, @@ -403,22 +513,37 @@ local function datetime_new(o) end local function datetime_tostring(o) - print(ffi.typeof(o)) - assert(ffi.typeof(o) == datetime_t) - local sz = 48 - local buff = ffi.new('char[?]', sz) - local len = native.datetime_to_string(o, buff, sz) - assert(len < sz) - return ffi.string(buff) -end + if ffi.typeof(o) == datetime_t then + local sz = 48 + local buff = ffi.new('char[?]', sz) + local len = native.datetime_to_string(o, buff, sz) + assert(len < sz) + return ffi.string(buff) + elseif ffi.typeof(o) == duration_years_t then + return ('%+d years'):format(o.y) + elseif ffi.typeof(o) == duration_months_t then + return ('%+d months'):format(o.m) + elseif ffi.typeof(o) == duration_t then + local ts = o.timestamp + local sign = '+' + + if ts < 0 then + ts = -ts + sign = '-' + end -local function dt_to_ymd(dt) - local y, m, d - y = ffi.new('int[1]') - m = ffi.new('int[1]') - d = ffi.new('int[1]') - cdt.dt_to_ymd(dt, y, m, d) - return y[0], m[0], d[0] + if ts < 60 then + return ('%s%s secs'):format(sign, ts) + elseif ts < 60 * 60 then + return ('%+d minutes, %s seconds'):format(o.minutes, ts % 60) + elseif ts < 24 * 60 * 60 then + return ('%+d hours, %d minutes, %s seconds'):format( + o.hours, o.minutes % 60, ts % 60) + else + return ('%+d days, %d hours, %d minutes, %s seconds'):format( + o.days, o.hours % 24, o.minutes % 60, ts % 60) + end + end end local function check_date(o) @@ -433,40 +558,36 @@ local function date_first(lhs, rhs) end end -local function shift_months(y, M, deltaM) - M = M + deltaM - local newM = (M - 1) % 12 + 1 - local newY = y + math.floor((M - 1)/12) - assert(newM >= 1 and newM <= 12, "month value is outside of range") - return newY, newM -end - local function datetime_sub(lhs, rhs) check_date(lhs) -- make sure left is date local d, s = lhs, rhs -- 1. left is date, right is date or delta if (ffi.typeof(s) == datetime_t) or (ffi.typeof(s) == duration_t) then - d.secs = d.secs - s.secs - d.nsec = s.nsec - s.nsec - if d.nsec < 0 then - d.secs = d.secs - 1 - d.nsec = d.nsec + NANOS_PER_SEC + local o + -- if they are both dates then result is delta + if ffi.typeof(s) == datetime_t then + o = duration_new() + else + o = datetime_new() + end + o.secs = d.secs - s.secs + o.nsec = d.nsec - s.nsec + if o.nsec < 0 then + o.secs = d.secs - 1 + o.nsec = d.nsec + NANOS_PER_SEC end + return o -- 2. left is date, right is duration in months elseif ffi.typeof(s) == duration_months_t then - local y, M, D = dt_to_ymd(local_dt(d)) - y, M = shift_months(y, M, -s.m) - local dt = cdt.dt_from_ymd(y, M, D) + local dt = cdt.dt_add_months(local_dt(d), -s.m, cdt.DT_LIMIT) local secs = d.secs % SECS_PER_DAY return mk_timestamp(dt, secs, d.nsec, d.offset or 0) -- 2. left is date, right is duration in years elseif ffi.typeof(s) == duration_years_t then - local y, M, D = dt_to_ymd(local_dt(d)) - y = y - s.y - local dt = cdt.dt_from_ymd(y, M, D) + local dt = cdt.dt_add_years(local_dt(d), -s.y, cdt.DT_LIMIT) local secs = d.secs % SECS_PER_DAY return mk_timestamp(dt, secs, d.nsec, d.offset or 0) else @@ -479,27 +600,25 @@ local function datetime_add(lhs, rhs) -- 1. left is date, right is date or delta if (ffi.typeof(s) == datetime_t) or (ffi.typeof(s) == duration_t) then - d.secs = d.secs + s.secs - d.nsec = d.nsec + s.nsec - if d.nsec >= NANOS_PER_SEC then - d.secs = d.secs + 1 - d.nsec = d.nsec - NANOS_PER_SEC + local o = datetime_new() + + o.secs = d.secs + s.secs + o.nsec = d.nsec + s.nsec + if o.nsec >= NANOS_PER_SEC then + o.secs = d.secs + 1 + o.nsec = d.nsec - NANOS_PER_SEC end - return d + return o -- 2. left is date, right is duration in months elseif ffi.typeof(s) == duration_months_t then - local y, M, D = dt_to_ymd(local_dt(d)) - y, M = shift_months(y, M, s.m) - local dt = cdt.dt_from_ymd(y, M, D) + local dt = cdt.dt_add_months(local_dt(d), s.m, cdt.DT_LIMIT) local secs = d.secs % SECS_PER_DAY return mk_timestamp(dt, secs, d.nsec, d.offset or 0) -- 2. left is date, right is duration in years elseif ffi.typeof(s) == duration_years_t then - local y, M, D = dt_to_ymd(local_dt(d)) - y = y + s.y - local dt = cdt.dt_from_ymd(y, M, D) + local dt = cdt.dt_add_years(local_dt(d), s.y, cdt.DT_LIMIT) local secs = d.secs % SECS_PER_DAY return mk_timestamp(dt, secs, d.nsec, d.offset or 0) else @@ -636,7 +755,7 @@ end local function datetime_to_tm_ptr(o) local p_tm = ffi.new 'struct tm[1]' - assert(ffi.typeof(o) == datetime_t) + check_date(o) -- dt_to_struct_tm() fills only date data cdt.dt_to_struct_tm(local_dt(o), p_tm) @@ -655,20 +774,20 @@ local function datetime_to_tm_ptr(o) end local function asctime(o) - assert(ffi.typeof(o) == datetime_t) + check_date(o) local p_tm = datetime_to_tm_ptr(o) return ffi.string(native.asctime(p_tm)) end local function ctime(o) - assert(ffi.typeof(o) == datetime_t) + check_date(o) local p_time = ffi.new 'time_t[1]' p_time[0] = o.secs return ffi.string(native.ctime(p_time)) end local function strftime(fmt, o) - assert(ffi.typeof(o) == datetime_t) + check_date(o) local sz = 50 local buff = ffi.new('char[?]', sz) local p_tm = datetime_to_tm_ptr(o) @@ -678,7 +797,7 @@ end local datetime_mt = { - -- __tostring = datetime_tostring, + __tostring = datetime_tostring, __serialize = datetime_serialize, __eq = datetime_eq, __lt = datetime_lt, @@ -686,10 +805,18 @@ local datetime_mt = { __sub = datetime_sub, __add = datetime_add, __index = datetime_index, + add = function(self, o) + self = duration_increment(self, o, 1) + return self + end, + sub = function(self, o) + self = duration_increment(self, o, -1) + return self + end } local duration_mt = { - -- __tostring = duration_tostring, + __tostring = datetime_tostring, __serialize = duration_serialize, __eq = datetime_eq, __lt = datetime_lt, @@ -699,8 +826,18 @@ local duration_mt = { __index = datetime_index, } +local duration_tiny_mt = { + __tostring = datetime_tostring, + __serialize = duration_serialize, + __sub = datetime_sub, + __add = datetime_add, + __index = datetime_index, +} + ffi.metatype(duration_t, duration_mt) ffi.metatype(datetime_t, datetime_mt) +ffi.metatype(duration_years_t, duration_tiny_mt) +ffi.metatype(duration_months_t, duration_tiny_mt) return setmetatable( { @@ -710,6 +847,7 @@ return setmetatable( days = duration_days_new, hours = duration_hours_new, minutes = duration_minutes_new, + seconds = duration_seconds_new, delta = duration_new, parse = parse_str, diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index 06720b241..f911412bf 100755 --- a/test/app-tap/datetime.test.lua +++ b/test/app-tap/datetime.test.lua @@ -4,7 +4,7 @@ local tap = require('tap') local test = tap.test("errno") local date = require('datetime') -test:plan(6) +test:plan(8) test:test("Simple tests for parser", function(test) test:plan(2) @@ -192,15 +192,75 @@ test:test("Parse tiny date into seconds and other parts", function(test) test:plan(9) local str = '19700101 00:00:30.528' local tiny = date(str) - test:ok(tiny.secs, 30, ("secs of '%s'"):format(str)) - test:ok(tiny.nsec, 528000000, ("nsec of '%s'"):format(str)) - test:ok(tiny.nanoseconds, 30528000000, "nanoseconds") - test:ok(tiny.microseconds, 30528000, "microseconds") - test:ok(tiny.milliseconds, 30528, "milliseconds") - test:ok(tiny.seconds, 30.528, "seconds") - test:ok(tiny.timestamp, 30.528, "timestamp") - test:ok(tiny.minutes, 0.5088, "minuts") - test:ok(tiny.hours, 0.00848, "hours") + test:ok(tiny.secs == 30, ("secs of '%s'"):format(str)) + test:ok(tiny.nsec == 528000000, ("nsec of '%s'"):format(str)) + test:ok(tiny.nanoseconds == 30528000000, "nanoseconds") + test:ok(tiny.microseconds == 30528000, "microseconds") + test:ok(tiny.milliseconds == 30528, "milliseconds") + test:ok(tiny.seconds == 30.528, "seconds") + test:ok(tiny.timestamp == 30.528, "timestamp") + test:ok(tiny.minutes == 0.5088, "minuts") + test:ok(tiny.hours == 0.00848, "hours") +end) + +test:test("Stringization of dates and periods", function(test) + test:plan(13) + local str = '19700101Z' + local dt = date(str) + test:ok(tostring(dt) == '1970-01-01T00:00Z', ('tostring(%s)'):format(str)) + test:ok(tostring(date.seconds(12)) == '+12 secs', '+12 seconds') + test:ok(tostring(date.seconds(-12)) == '-12 secs', '-12 seconds') + test:ok(tostring(date.minutes(12)) == '+12 minutes, 0 seconds', '+12 minutes') + test:ok(tostring(date.minutes(-12)) == '-12 minutes, 0 seconds', '-12 minutes') + test:ok(tostring(date.hours(12)) == '+12 hours, 0 minutes, 0 seconds', + '+12 hours') + test:ok(tostring(date.hours(-12)) == '-12 hours, 0 minutes, 0 seconds', + '-12 hours') + test:ok(tostring(date.days(12)) == '+12 days, 0 hours, 0 minutes, 0 seconds', + '+12 days') + test:ok(tostring(date.days(-12)) == '-12 days, 0 hours, 0 minutes, 0 seconds', + '-12 days') + test:ok(tostring(date.months(5)) == '+5 months', '+5 months') + test:ok(tostring(date.months(-5)) == '-5 months', '-5 months') + test:ok(tostring(date.years(4)) == '+4 years', '+4 years') + test:ok(tostring(date.years(-4)) == '-4 years', '-4 years') +end) + +test:test("Time duration operations", function(test) + test:plan(12) + + -- check arithmetic with leap dates + local T = date('1972-02-29') + local M = date.months(2) + local Y = date.years(1) + test:ok(tostring(T + M) == '1972-04-29T00:00Z', ('T(%s) + M(%s'):format(T, M)) + test:ok(tostring(T + Y) == '1973-03-01T00:00Z', ('T(%s) + Y(%s'):format(T, Y)) + test:ok(tostring(T + M + Y) == '1973-04-30T00:00Z', + ('T(%s) + M(%s) + Y(%s'):format(T, M, Y)) + test:ok(tostring(T + Y + M) == '1973-05-01T00:00Z', + ('T(%s) + M(%s) + Y(%s'):format(T, M, Y)) + test:ok(tostring(T:add{years = 1, months = 2}) == '1973-04-30T00:00Z', + ('T:add{years=1,months=2}(%s)'):format(T)) + + -- check average, not leap dates + T = date('1970-01-08') + test:ok(tostring(T + M) == '1970-03-08T00:00Z', ('T(%s) + M(%s'):format(T, M)) + test:ok(tostring(T + Y) == '1971-01-08T00:00Z', ('T(%s) + Y(%s'):format(T, Y)) + test:ok(tostring(T + M + Y) == '1971-03-08T00:00Z', + ('T(%s) + M(%s) + Y(%s'):format(T, M, Y)) + test:ok(tostring(T + Y + M) == '1971-03-08T00:00Z', + ('T(%s) + Y(%s) + M(%s'):format(T, Y, M)) + test:ok(tostring(T:add{years = 1, months = 2}) == '1971-03-08T00:00Z', + ('T:add{years=1,months=2}(%s)'):format(T)) + + + -- subtraction of 2 dates + local T2 = date('19700103') + local T1 = date('1970-01-01') + test:ok(tostring(T2 - T1) == '+2 days, 0 hours, 0 minutes, 0 seconds', + ('T2(%s) - T1(%s'):format(T2, T1)) + test:ok(tostring(T1 - T2) == '-2 days, 0 hours, 0 minutes, 0 seconds', + ('T2(%s) - T1(%s'):format(T2, T1)) end) os.exit(test:check() and 0 or 1) -- 2.29.2