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 A67706EC40; Fri, 13 Aug 2021 01:37:24 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org A67706EC40 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1628807844; bh=S4Ct5v5BrnxvHjyvm479hBBIfbKWU8l/0qq6PIkDdsc=; 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=zfoywT1JZQOfN8DevFjeXfzkK4QYE83qLvgmHS8wZbwX4Gyhld6qQXSz+5GSqq3Aw Ro1ctgf6jk4s+rFNWzE+gO0nxaA4VbE8yZK7IfQNwo9TkkKtQsDlOdc9eVAcpiW8Y1 bnacGThLkioGdzWHgIvd8kpVgOkeDdCRHdUrpM88= Received: from smtp52.i.mail.ru (smtp52.i.mail.ru [94.100.177.112]) (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 7CF466EC45 for ; Fri, 13 Aug 2021 01:34:22 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 7CF466EC45 Received: by smtp52.i.mail.ru with esmtpa (envelope-from ) id 1mEJHI-0003Mm-SS; Fri, 13 Aug 2021 01:34:21 +0300 To: imun@tarantool.org, v.shpilevoy@tarantool.org Date: Fri, 13 Aug 2021 01:33:45 +0300 Message-Id: <5c5f5da6ed96bd98fd85b65359d7e6e418793549.1628805259.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: 646B95376F6C166E X-77F55803: 4F1203BC0FB41BD92087353F0EC44DD9BCE6B93DE0C6C3914462CDB1732D383C182A05F5380850409F9125FA6D56103DB866387E7FB58DBBB907CB71F2F1EC217AC7A1C5857EA660 X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE7AD2F2D6F6013FF7FC2099A533E45F2D0395957E7521B51C2CFCAF695D4D8E9FCEA1F7E6F0F101C6778DA827A17800CE7E9A0F80F179600C6EA1F7E6F0F101C6723150C8DA25C47586E58E00D9D99D84E1BDDB23E98D2D38BBCA57AF85F7723F2C0BC2D1295DD3299FE2970553A9AEAE8CC7F00164DA146DAFE8445B8C89999728AA50765F7900637F6B57BC7E64490618DEB871D839B7333395957E7521B51C2DFABB839C843B9C08941B15DA834481F8AA50765F7900637F6B57BC7E6449061A352F6E88A58FB86F5D81C698A659EA73AA81AA40904B5D9A18204E546F3947C081AAF44600346E76E0066C2D8992A164AD6D5ED66289B52698AB9A7B718F8C46E0066C2D8992A16725E5C173C3A84C351CCD24836465D6CBA3038C0950A5D36B5C8C57E37DE458B0BC6067A898B09E46D1867E19FE14079C09775C1D3CA48CF3D321E7403792E342EB15956EA79C166A417C69337E82CC275ECD9A6C639B01B78DA827A17800CE7D699F3A2029486C7731C566533BA786AA5CC5B56E945C8DA X-B7AD71C0: AC4F5C86D027EB782CDD5689AFBDA7A213B5FB47DCBC3458F0AFF96BAACF4158235E5A14AD4A4A4625E192CAD1D9E79D94463893BF8742D017DCDD9527101746 X-C1DE0DAB: C20DE7B7AB408E4181F030C43753B8186998911F362727C4C7A0BC55FA0FE5FCF5ACD438C139E9CEBA8D2FD4B9742ED724A611A7D1B64A4BB1881A6453793CE9C32612AADDFBE061C61BE10805914D3804EBA3D8E7E5B87ABF8C51168CD8EBDBC934FE2FA6BC6FACDC48ACC2A39D04F89CDFB48F4795C241BDAD6C7F3747799A X-C8649E89: 4E36BF7865823D7055A7F0CF078B5EC49A30900B95165D34AA13E2DDB90678626BBB2915F7A598F909620091C6168215D87F135E68076E350A407682E03A65CE1D7E09C32AA3244C25B7C91A444A737E4C4B1F058FAD7B8DFE8DA44ABE2443F78D5DD81C2BAB7D1D X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu530nj6fImhcD4MUrOEAnl0W826KZ9Q+tr5ycPtXkTV4k65bRjmOUUP8cvGozZ33TWg5HZplvhhXbhDGzqmQDTd6OAevLeAnq3Ra9uf7zvY2zzsIhlcp/Y7m53TZgf2aB4JOg4gkr2bioj0dLV0c3jbkxs5FWq3T39Hw== X-Mailru-Sender: B5B6A6EBBD94DAD86A422A53D9D37A7D23E429C30DF02D8EA0B9F6408CB078448DA7690376CE89B31EC9E4A2C82A33BC8C24925A86E657CE0C70AEE3C9A96FBAB3D7EE8ED63280BE112434F685709FCF0DA7A0AF5A3A8387 X-Mras: Ok Subject: [Tarantool-patches] [PATCH v4 6/7] lua, datetime: time intervals 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" * created few entry points (months(N), years(N), days(N), etc.) for easier datetime arithmetic; * additions/subtractions of years/months use `dt_add_years()` and `dt_add_months()` from 3rd party c-dt library; * also there are `:add{}` and `:sub{}` methods in datetime object to add or substract more complex intervals; * introduced `is_datetime()` and `is_interval()` helpers for checking of validity of passed arguments; * human-readable stringization implemented for interval objects. Note, that additions/subtractions completed for all _reasonable_ combinations of values of date and interval types; Time + Interval => Time Interval + Time => Time Time - Time => Interval Time - Interval => Time Interval + Interval => Interval Interval - Interval => Interval Part of #5941 --- extra/exports | 5 + src/lua/datetime.lua | 349 ++++++++++++++++++++++++++++++++- test/app-tap/datetime.test.lua | 159 ++++++++++++++- 3 files changed, 508 insertions(+), 5 deletions(-) diff --git a/extra/exports b/extra/exports index 345ffbf25..b92623dc4 100644 --- a/extra/exports +++ b/extra/exports @@ -151,11 +151,16 @@ csv_setopt datetime_asctime datetime_ctime datetime_now +datetime_pack datetime_strftime datetime_to_string +datetime_unpack decimal_unpack decimal_from_string decimal_unpack +tnt_dt_add_years +tnt_dt_add_quarters +tnt_dt_add_months tnt_dt_dow tnt_dt_from_rdn tnt_dt_from_struct_tm diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index 00d50a7f1..8206f175a 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -24,6 +24,17 @@ ffi.cdef [[ int tnt_dt_rdn (dt_t dt); + // dt_arithmetic.h + typedef enum { + DT_EXCESS, + DT_LIMIT, + DT_SNAP + } dt_adjust_t; + + dt_t tnt_dt_add_years (dt_t dt, int delta, dt_adjust_t adjust); + dt_t tnt_dt_add_quarters (dt_t dt, int delta, dt_adjust_t adjust); + dt_t tnt_dt_add_months (dt_t dt, int delta, dt_adjust_t adjust); + // dt_parse_iso.h size_t tnt_dt_parse_iso_date (const char *str, size_t len, dt_t *dt); size_t tnt_dt_parse_iso_time (const char *str, size_t len, int *sod, int *nsec); @@ -50,8 +61,10 @@ ffi.cdef [[ local builtin = ffi.C local math_modf = math.modf +local math_floor = math.floor local SECS_PER_DAY = 86400 +local NANOS_PER_SEC = 1000000000LL -- c-dt/dt_config.h @@ -62,8 +75,23 @@ local DT_EPOCH_1970_OFFSET = 719163LL local datetime_t = ffi.typeof('struct datetime') local interval_t = ffi.typeof('struct datetime_interval') +ffi.cdef [[ + struct interval_months { + int m; + }; + + struct interval_years { + int y; + }; +]] +local interval_months_t = ffi.typeof('struct interval_months') +local interval_years_t = ffi.typeof('struct interval_years') + local function is_interval(o) - return type(o) == 'cdata' and ffi.istype(interval_t, o) + return type(o) == 'cdata' and + (ffi.istype(interval_t, o) or + ffi.istype(interval_months_t, o) or + ffi.istype(interval_years_t, o)) end local function is_datetime(o) @@ -72,7 +100,10 @@ end local function is_date_interval(o) return type(o) == 'cdata' and - (ffi.istype(interval_t, o) or ffi.istype(datetime_t, o)) + (ffi.istype(datetime_t, o) or + ffi.istype(interval_t, o) or + ffi.istype(interval_months_t, o) or + ffi.istype(interval_years_t, o)) end local function interval_new() @@ -80,6 +111,13 @@ local function interval_new() return interval end +local function check_number(n, message) + if type(n) ~= 'number' then + return error(("%s: expected number, but received %s"): + format(message, n), 2) + end +end + local function check_date(o, message) if not is_datetime(o) then return error(("%s: expected datetime, but received %s"): @@ -87,6 +125,20 @@ local function check_date(o, message) end end +local function check_date_interval(o, message) + if not is_datetime(o) and not is_interval(o) then + return error(("%s: expected datetime or interval, but received %s"): + format(message, o), 2) + end +end + +local function check_interval(o, message) + if not is_interval(o) then + return error(("%s: expected interval, but received %s"): + format(message, o), 2) + end +end + local function check_str(s, message) if not type(s) == 'string' then return error(("%s: expected string, but received %s"): @@ -102,6 +154,77 @@ local function check_range(v, range, txt) end end +local function interval_years_new(y) + check_number(y, "years(number)") + local o = ffi.new(interval_years_t) + o.y = y + return o +end + +local function interval_months_new(m) + check_number(m, "months(number)") + local o = ffi.new(interval_months_t) + o.m = m + return o +end + +local function interval_weeks_new(w) + check_number(w, "weeks(number)") + local o = ffi.new(interval_t) + o.secs = w * SECS_PER_DAY * 7 + return o +end + +local function interval_days_new(d) + check_number(d, "days(number)") + local o = ffi.new(interval_t) + o.secs = d * SECS_PER_DAY + return o +end + +local function interval_hours_new(h) + check_number(h, "hours(number)") + local o = ffi.new(interval_t) + o.secs = h * 60 * 60 + return o +end + +local function interval_minutes_new(m) + check_number(m, "minutes(number)") + local o = ffi.new(interval_t) + o.secs = m * 60 + return o +end + +local function interval_seconds_new(s) + check_number(s, "seconds(number)") + local o = ffi.new(interval_t) + o.nsec = s % 1 * 1e9 + o.secs = s - (s % 1) + return o +end + +local SECS_EPOCH_OFFSET = (DT_EPOCH_1970_OFFSET * SECS_PER_DAY) + +local function local_rd(secs) + return math_floor(tonumber((secs + SECS_EPOCH_OFFSET) / SECS_PER_DAY)) +end + +local function local_dt(secs) + return builtin.tnt_dt_from_rdn(local_rd(secs)) +end + +local function normalize_nsec(secs, nsec) + if nsec < 0 then + secs = secs - 1 + nsec = nsec + NANOS_PER_SEC + elseif nsec >= NANOS_PER_SEC then + secs = secs + 1 + nsec = nsec - NANOS_PER_SEC + end + return secs, nsec +end + local function datetime_cmp(lhs, rhs) if not is_date_interval(lhs) or not is_date_interval(rhs) then @@ -256,6 +379,10 @@ local function datetime_tostring(o) local len = builtin.datetime_to_string(o, buff, sz) assert(len < sz) return ffi.string(buff) + elseif ffi.typeof(o) == interval_years_t then + return ('%+d years'):format(o.y) + elseif ffi.typeof(o) == interval_months_t then + return ('%+d months'):format(o.m) elseif ffi.typeof(o) == interval_t then local ts = o.timestamp local sign = '+' @@ -279,6 +406,126 @@ local function datetime_tostring(o) end end +local function date_first(lhs, rhs) + if is_datetime(lhs) then + return lhs, rhs + else + return rhs, lhs + end +end + +local function error_incompatible(name) + error(("datetime:%s() - incompatible type of arguments"): + format(name), 3) +end + +--[[ +Matrix of subtraction operands eligibility and their result type + +| | datetime | interval | interval_months | interval_years | ++-----------------+-----------+----------+-----------------+----------------+ +| datetime | interval | datetime | datetime | datetime | +| interval | | interval | | | +| interval_months | | | interval_months | | +| interval_years | | | | interval_years | +]] +local function datetime_sub(lhs, rhs) + check_date_interval(lhs, "operator -") + local d, s = lhs, rhs + local left_t = ffi.typeof(d) + local right_t = ffi.typeof(s) + local o + + if left_t == datetime_t then + -- 1. left is date, right is date or generic interval + if right_t == datetime_t or right_t == interval_t then + o = right_t == datetime_t and interval_new() or datetime_new() + o.secs, o.nsec = normalize_nsec(lhs.secs - rhs.secs, + lhs.nsec - rhs.nsec) + return o + -- 2. left is date, right is interval in months + elseif right_t == interval_months_t then + local dt = local_dt(lhs.secs) + dt = builtin.tnt_dt_add_months(dt, -rhs.m, builtin.DT_LIMIT) + return datetime_new_dt(dt, lhs.secs % SECS_PER_DAY, + lhs.nsec, lhs.offset) + + -- 3. left is date, right is interval in years + elseif right_t == interval_years_t then + local dt = local_dt(lhs.secs) + dt = builtin.tnt_dt_add_years(dt, -rhs.y, builtin.DT_LIMIT) + return datetime_new_dt(dt, lhs.secs % SECS_PER_DAY, + lhs.nsec, lhs.offset) + else + error_incompatible("operator -") + end + -- 4. both left and right are generic intervals + elseif left_t == interval_t and right_t == interval_t then + o = interval_new() + o.secs, o.nsec = normalize_nsec(lhs.secs - rhs.secs, + lhs.nsec - rhs.nsec) + return o + -- 5. both left and right are intervals in months + elseif left_t == interval_months_t and right_t == interval_months_t then + return interval_months_new(lhs.m - rhs.m) + -- 5. both left and right are intervals in years + elseif left_t == interval_years_t and right_t == interval_years_t then + return interval_years_new(lhs.y - rhs.y) + else + error_incompatible("operator -") + end +end + +--[[ +Matrix of addition operands eligibility and their result type + +| | datetime | interval | interval_months | interval_years | ++-----------------+-----------+----------+-----------------+----------------+ +| datetime | datetime | datetime | datetime | datetime | +| interval | datetime | interval | | | +| interval_months | datetime | | interval_months | | +| interval_years | datetime | | | interval_years | +]] +local function datetime_add(lhs, rhs) + local d, s = date_first(lhs, rhs) + + check_date_interval(d, "operator +") + check_interval(s, "operator +") + local left_t = ffi.typeof(d) + local right_t = ffi.typeof(s) + local o + + -- 1. left is date, right is date or interval + if left_t == datetime_t and right_t == interval_t then + o = datetime_new() + o.secs, o.nsec = normalize_nsec(d.secs + s.secs, d.nsec + s.nsec) + return o + -- 2. left is date, right is interval in months + elseif left_t == datetime_t and right_t == interval_months_t then + local dt = builtin.tnt_dt_add_months(local_dt(d.secs), s.m, builtin.DT_LIMIT) + local secs = d.secs % SECS_PER_DAY + return datetime_new_dt(dt, secs, d.nsec, d.offset or 0) + + -- 3. left is date, right is interval in years + elseif left_t == datetime_t and right_t == interval_years_t then + local dt = builtin.tnt_dt_add_years(local_dt(d.secs), s.y, builtin.DT_LIMIT) + local secs = d.secs % SECS_PER_DAY + return datetime_new_dt(dt, secs, d.nsec, d.offset or 0) + -- 4. both left and right are generic intervals + elseif left_t == interval_t and right_t == interval_t then + o = interval_new() + o.secs, o.nsec = normalize_nsec(d.secs + s.secs, d.nsec + s.nsec) + return o + -- 5. both left and right are intervals in months + elseif left_t == interval_months_t and right_t == interval_months_t then + return interval_months_new(d.m + s.m) + -- 6. both left and right are intervals in years + elseif left_t == interval_years_t and right_t == interval_years_t then + return interval_years_new(d.y + s.y) + else + error_incompatible("operator +") + end +end --[[ Basic Extended @@ -400,6 +647,75 @@ local function local_now() return d end +-- addition or subtraction from date/time of a given interval +-- described via table direction should be +1 or -1 +local function interval_increment(self, o, direction) + assert(direction == -1 or direction == 1) + local title = direction > 0 and "datetime.add" or "datetime.sub" + check_date(self, title) + if type(o) ~= 'table' then + error(('%s - object expected'):format(title), 2) + end + + local ym_updated = false + local dhms_updated = false + + local secs, nsec + secs, nsec = self.secs, self.nsec + -- operations with intervals should be done using human dates + -- not UTC dates, thus we normalize to UTC + local dt = local_dt(secs) + + for key, value in pairs(o) do + if key == 'years' then + check_range(value, {0, 9999}, key) + dt = builtin.tnt_dt_add_years(dt, direction * value, builtin.DT_LIMIT) + ym_updated = true + elseif key == 'months' then + check_range(value, {0, 12}, key) + dt = builtin.tnt_dt_add_months(dt, direction * value, builtin.DT_LIMIT) + ym_updated = true + elseif key == 'weeks' then + check_range(value, {0, 52}, key) + secs = secs + direction * 7 * value * SECS_PER_DAY + dhms_updated = true + elseif key == 'days' then + check_range(value, {0, 31}, key) + secs = secs + direction * value * SECS_PER_DAY + dhms_updated = true + elseif key == 'hours' then + check_range(value, {0, 23}, key) + secs = secs + direction * 60 * 60 * value + dhms_updated = true + elseif key == 'minutes' then + check_range(value, {0, 59}, key) + secs = secs + direction * 60 * value + elseif key == 'seconds' then + check_range(value, {0, 60}, key) + local s, frac = math.modf(value) + secs = secs + direction * s + nsec = nsec + direction * frac * 1e9 + dhms_updated = true + end + end + + secs, nsec = normalize_nsec(secs, nsec) + + -- .days, .hours, .minutes, .seconds + if dhms_updated then + self.secs = secs + self.nsec = nsec + end + + -- .years, .months updated + if ym_updated then + self.secs = (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY + + secs % SECS_PER_DAY + end + + return self +end + -- Change the time-zone to the provided target_offset -- Time `.secs`/`.nsec` are always UTC normalized, we need only to -- reattribute object with different `.offset` @@ -437,6 +753,14 @@ local function datetime_index(self, key) return (tonumber(self.secs) + self.nsec / 1e9) / (60 * 60) elseif key == 'd' or key == 'days' then return (tonumber(self.secs) + self.nsec / 1e9) / (24 * 60 * 60) + elseif key == 'add' then + return function(self, obj) + return interval_increment(self, obj, 1) + end + elseif key == 'sub' then + return function(self, obj) + return interval_increment(self, obj, -1) + end elseif key == 'to_utc' then return function(self) return datetime_to_tz(self, 0) @@ -493,6 +817,8 @@ local datetime_mt = { __eq = datetime_eq, __lt = datetime_lt, __le = datetime_le, + __sub = datetime_sub, + __add = datetime_add, __index = datetime_index, __newindex = datetime_newindex, } @@ -503,16 +829,35 @@ local interval_mt = { __eq = datetime_eq, __lt = datetime_lt, __le = datetime_le, + __sub = datetime_sub, + __add = datetime_add, + __index = datetime_index, +} + +local interval_tiny_mt = { + __tostring = datetime_tostring, + __serialize = interval_serialize, + __sub = datetime_sub, + __add = datetime_add, __index = datetime_index, } ffi.metatype(interval_t, interval_mt) ffi.metatype(datetime_t, datetime_mt) +ffi.metatype(interval_years_t, interval_tiny_mt) +ffi.metatype(interval_months_t, interval_tiny_mt) return setmetatable( { new = datetime_new, new_raw = datetime_new_obj, + years = interval_years_new, + months = interval_months_new, + weeks = interval_weeks_new, + days = interval_days_new, + hours = interval_hours_new, + minutes = interval_minutes_new, + seconds = interval_seconds_new, interval = interval_new, parse = parse, diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index 244ec2575..a2d7fac57 100755 --- a/test/app-tap/datetime.test.lua +++ b/test/app-tap/datetime.test.lua @@ -6,7 +6,7 @@ local date = require('datetime') local ffi = require('ffi') -test:plan(7) +test:plan(10) test:test("Simple tests for parser", function(test) test:plan(2) @@ -207,11 +207,164 @@ test:test("Parse tiny date into seconds and other parts", function(test) test:ok(tiny.hours == 0.00848, "hours") end) -test:test("Stringization of date", function(test) - test:plan(1) +test:test("Stringization of dates and intervals", 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 interval 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) + +local function catchadd(A, B) + return pcall(function() return A + B end) +end + +--[[ +Matrix of addition operands eligibility and their result type + +| | datetime | interval | interval_months | interval_years | ++-----------------+-----------+----------+-----------------+----------------+ +| datetime | datetime | datetime | datetime | datetime | +| interval | datetime | interval | | | +| interval_months | datetime | | interval_months | | +| interval_years | datetime | | | interval_years | +]] + +test:test("Matrix of allowed time and interval additions", function(test) + test:plan(20) + + -- check arithmetic with leap dates + local T1970 = date('1970-01-01') + local T2000 = date('2000-01-01') + local I1 = date.days(1) + local M2 = date.months(2) + local M10 = date.months(10) + local Y1 = date.years(1) + local Y5 = date.years(5) + + test:ok(catchadd(T1970, I1) == true, "status: T + I") + test:ok(catchadd(T1970, M2) == true, "status: T + M") + test:ok(catchadd(T1970, Y1) == true, "status: T + Y") + test:ok(catchadd(T1970, T2000) == false, "status: T + T") + test:ok(catchadd(I1, T1970) == true, "status: I + T") + test:ok(catchadd(M2, T1970) == true, "status: M + T") + test:ok(catchadd(Y1, T1970) == true, "status: Y + T") + test:ok(catchadd(I1, Y1) == false, "status: I + Y") + test:ok(catchadd(M2, Y1) == false, "status: M + Y") + test:ok(catchadd(I1, Y1) == false, "status: I + Y") + test:ok(catchadd(Y5, M10) == false, "status: Y + M") + test:ok(catchadd(Y5, I1) == false, "status: Y + I") + test:ok(catchadd(Y5, Y1) == true, "status: Y + Y") + + test:ok(tostring(T1970 + I1) == "1970-01-02T00:00Z", "value: T + I") + test:ok(tostring(T1970 + M2) == "1970-03-01T00:00Z", "value: T + M") + test:ok(tostring(T1970 + Y1) == "1971-01-01T00:00Z", "value: T + Y") + test:ok(tostring(I1 + T1970) == "1970-01-02T00:00Z", "value: I + T") + test:ok(tostring(M2 + T1970) == "1970-03-01T00:00Z", "value: M + T") + test:ok(tostring(Y1 + T1970) == "1971-01-01T00:00Z", "value: Y + T") + test:ok(tostring(Y5 + Y1) == "+6 years", "Y + Y") + +end) + +local function catchsub_status(A, B) + return pcall(function() return A - B end) +end + +--[[ +Matrix of subtraction operands eligibility and their result type + +| | datetime | interval | interval_months | interval_years | ++-----------------+-----------+----------+-----------------+----------------+ +| datetime | interval | datetime | datetime | datetime | +| interval | | interval | | | +| interval_months | | | interval_months | | +| interval_years | | | | interval_years | +]] +test:test("Matrix of allowed time and interval subtractions", function(test) + test:plan(18) + + -- check arithmetic with leap dates + local T1970 = date('1970-01-01') + local T2000 = date('2000-01-01') + local I1 = date.days(1) + local M2 = date.months(2) + local M10 = date.months(10) + local Y1 = date.years(1) + local Y5 = date.years(5) + + test:ok(catchsub_status(T1970, I1) == true, "status: T - I") + test:ok(catchsub_status(T1970, M2) == true, "status: T - M") + test:ok(catchsub_status(T1970, Y1) == true, "status: T - Y") + test:ok(catchsub_status(T1970, T2000) == true, "status: T - T") + test:ok(catchsub_status(I1, T1970) == false, "status: I + T") + test:ok(catchsub_status(M2, T1970) == false, "status: M + T") + test:ok(catchsub_status(Y1, T1970) == false, "status: Y + T") + test:ok(catchsub_status(I1, Y1) == false, "status: I - Y") + test:ok(catchsub_status(M2, Y1) == false, "status: M - Y") + test:ok(catchsub_status(I1, Y1) == false, "status: I - Y") + test:ok(catchsub_status(Y5, M10) == false, "status: Y - M") + test:ok(catchsub_status(Y5, I1) == false, "status: Y - I") + test:ok(catchsub_status(Y5, Y1) == true, "status: Y - Y") + + test:ok(tostring(T1970 - I1) == "1969-12-31T00:00Z", "value: T - I") + test:ok(tostring(T1970 - M2) == "1969-11-01T00:00Z", "value: T - M") + test:ok(tostring(T1970 - Y1) == "1969-01-01T00:00Z", "value: T - Y") + test:ok(tostring(T1970 - T2000) == "-10957 days, 0 hours, 0 minutes, 0 seconds", + "value: T - T") + test:ok(tostring(Y5 - Y1) == "+4 years", "value: Y - Y") + end) os.exit(test:check() and 0 or 1) -- 2.29.2