[Tarantool-patches] [RFC PATCH 13/13] lua: complete time duration support
Timur Safin
tsafin at tarantool.org
Thu Jul 15 11:18:19 MSK 2021
* 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
More information about the Tarantool-patches
mailing list