[Tarantool-patches] [PATCH resend v2 09/11] lua, datetime: time intervals support
Oleg Babin
olegrok at tarantool.org
Thu Jul 29 21:58:27 MSK 2021
Thanks for your patch.
I wrote several comments below.
However it makes me think that such approach will work quite slow since
all functions is implemented in Lua, all arithmetic is in Lua.
On 28.07.2021 13:34, Timur Safin via Tarantool-patches wrote:
> * 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
> ---
> src/exports.h | 3 +
> src/lua/datetime.lua | 556 +++++++++++++++++++++++++++------
> test/app-tap/datetime.test.lua | 163 +++++++++-
> 3 files changed, 631 insertions(+), 91 deletions(-)
>
> diff --git a/src/exports.h b/src/exports.h
> index 3a1e8854c..6e7fe206d 100644
> --- a/src/exports.h
> +++ b/src/exports.h
> @@ -535,6 +535,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 7a208cef9..1466b923f 100644
> --- a/src/lua/datetime.lua
> +++ b/src/lua/datetime.lua
> @@ -37,6 +37,17 @@ ffi.cdef [[
> int dt_rdn (dt_t dt);
> dt_dow_t dt_dow (dt_t dt);
>
> + // dt_arithmetic.h
> + typedef enum {
> + DT_EXCESS,
> + DT_LIMIT,
> + DT_SNAP
> + } dt_adjust_t;
> +
> + dt_t dt_add_years (dt_t dt, int delta, dt_adjust_t adjust);
> + dt_t dt_add_quarters (dt_t dt, int delta, dt_adjust_t adjust);
> + dt_t dt_add_months (dt_t dt, int delta, dt_adjust_t adjust);
> +
> // dt_parse_iso.h
> size_t dt_parse_iso_date (const char *str, size_t len, dt_t *dt);
>
> @@ -158,58 +169,146 @@ local DT_EPOCH_1970_OFFSET = 719163LL
>
> local datetime_t = ffi.typeof('struct datetime_t')
> local interval_t = ffi.typeof('struct datetime_interval_t')
> +ffi.cdef [[
> + struct t_interval_months {
> + int m;
> + };
> +
> + struct t_interval_years {
> + int y;
> + };
> +]]
> +local interval_months_t = ffi.typeof('struct t_interval_months')
> +local interval_years_t = ffi.typeof('struct t_interval_years')
> +
> +local function is_interval(o)
> + return ffi.istype(interval_t, o) or
> + ffi.istype(interval_months_t, o) or
> + ffi.istype(interval_years_t, o)
> +end
> +
It will throw for non-cdata values:
tarantool> ffi.istype(interval_t, o)
---
- error: 'bad argument #1 to ''?'' (C type expected, got nil)'
...
tarantool> ffi.istype(interval_t, 123)
---
- error: 'bad argument #1 to ''?'' (C type expected, got nil)'
...
> +local function is_datetime(o)
> + return ffi.istype(o, datetime_t)
> +end
> +
>
> local function interval_new()
> local interval = ffi.new(interval_t)
> return interval
> end
>
> -local function adjusted_secs(dt)
> - return dt.secs - dt.offset * 60
> +local function check_number(n, message, lvl)
> + if lvl == nil then
> + lvl = 2
> + end
> + if type(n) ~= 'number' then
> + return error(('Usage: %s'):format(message), lvl)
> + end
> end
>
> -local function datetime_sub(lhs, rhs)
> - local s1 = adjusted_secs(lhs)
> - local s2 = adjusted_secs(rhs)
> - local d = interval_new()
> - d.secs = s2 - s1
> - d.nsec = rhs.nsec - lhs.nsec
> - if d.nsec < 0 then
> - d.secs = d.secs - 1
> - d.nsec = d.nsec + NANOS_PER_SEC
> +local function check_date(o, message, lvl)
> + if lvl == nil then
> + lvl = 2
> + end
> + if not is_datetime(o) then
> + return error(('Usage: %s'):format(message), lvl)
> end
> - return d
> end
>
> -local function datetime_add(lhs, rhs)
> - local s1 = adjusted_secs(lhs)
> - local s2 = adjusted_secs(rhs)
> - local d = interval_new()
> - d.secs = s2 + s1
> - d.nsec = rhs.nsec + lhs.nsec
> - if d.nsec >= NANOS_PER_SEC then
> - d.secs = d.secs + 1
> - d.nsec = d.nsec - NANOS_PER_SEC
> +local function check_date_interval(o, message, lvl)
> + if lvl == nil then
> + lvl = 2
> + end
> + if not (is_datetime(o) or is_interval(o)) then
> + return error(('Usage: %s'):format(message), lvl)
> end
> - return d
> 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
> - if rhs == nil then
> - return false
> +local function check_interval(o, message, lvl)
> + if lvl == nil then
> + lvl = 2
> + end
> + if not is_interval(o) then
> + return error(('Usage: %s'):format(message), lvl)
> end
> +end
> +
> +local function check_str(o, message, lvl)
> + if lvl == nil then
> + lvl = 2
> + end
> + if not type(o) == 'string' then
> + return error(('Usage: %s'):format(message), lvl)
> + 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 function datetime_eq(lhs, rhs)
> + check_date_interval(lhs, "datetime:__eq(date or interval)")
> + check_date_interval(rhs, "datetime:__eq(date or interval)")
> return (lhs.secs == rhs.secs) and (lhs.nsec == rhs.nsec)
> end
>
>
> local function datetime_lt(lhs, rhs)
> + check_date_interval(lhs, "datetime:__lt(date or interval)")
> + check_date_interval(rhs, "datetime:__lt(date or interval)")
> return (lhs.secs < rhs.secs) or
> (lhs.secs == rhs.secs and lhs.nsec < rhs.nsec)
> end
>
> local function datetime_le(lhs, rhs)
> + check_date_interval(lhs, "datetime:__le(date or interval)")
> + check_date_interval(rhs, "datetime:__le(date or interval)")
> return (lhs.secs <= rhs.secs) or
> (lhs.secs == rhs.secs and lhs.nsec <= rhs.nsec)
> end
> @@ -224,19 +323,123 @@ local function interval_serialize(self)
> return { secs = self.secs, nsec = self.nsec }
> end
>
> +local function local_rd(o)
> + return math.floor(tonumber(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 _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
> +
> +-- 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)
> + check_date(self, "interval_increment(date, object, -+1)")
> + 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 = {
The same as in one previous patch. It's too expensive to recreate table
and functions for each simple
action and for each iteration loop.
> + 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
> +
> + 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 = (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
> 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
> @@ -250,32 +453,20 @@ local datetime_index = function(self, key)
> days = function(self)
> return (tonumber(self.secs) + self.nsec / 1e9) / (60 * 60) / 24
> end,
> + add = function(self)
> + return function(self, o)
> + return interval_increment(self, o, 1)
> + end
> + end,
> + sub = function(self)
> + return function(self, o)
> + return interval_increment(self, o, -1)
> + end
> + end,
> }
> return attributes[key] ~= nil and attributes[key](self) or nil
> end
>
> -local datetime_mt = {
> - -- __tostring = datetime_tostring,
> - __serialize = datetime_serialize,
> - __eq = datetime_eq,
> - __lt = datetime_lt,
> - __le = datetime_le,
> - __sub = datetime_sub,
> - __add = datetime_add,
> - __index = datetime_index,
> -}
> -
> -local interval_mt = {
> - -- __tostring = interval_tostring,
> - __serialize = interval_serialize,
> - __eq = datetime_eq,
> - __lt = datetime_lt,
> - __le = datetime_le,
> - __sub = datetime_sub,
> - __add = datetime_add,
> - __index = datetime_index,
> -}
> -
> local function datetime_new_raw(secs, nsec, offset)
> local dt_obj = ffi.new(datetime_t)
> dt_obj.secs = secs
> @@ -284,14 +475,6 @@ local function datetime_new_raw(secs, nsec, offset)
> return dt_obj
> end
>
> -local function local_rd(o)
> - return math.floor(tonumber(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 +550,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,
>
> @@ -402,6 +586,153 @@ local function datetime_new(o)
> return mk_timestamp(dt, secs, frac, offset)
> end
>
> +local function datetime_tostring(o)
> + 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) == 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 = '+'
> +
> + if ts < 0 then
> + ts = -ts
> + sign = '-'
> + end
> +
> + 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 date_first(lhs, rhs)
> + if is_datetime(lhs) then
> + return lhs, rhs
> + else
> + return rhs, lhs
> + 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 |
> +]]
> +local function datetime_sub(lhs, rhs)
> + check_date_interval(lhs, "datetime:__sub(date or interval)")
> + 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 = cdt.dt_add_months(local_dt(lhs), -rhs.m, cdt.DT_LIMIT)
> + return mk_timestamp(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 = cdt.dt_add_years(local_dt(lhs), -rhs.y, cdt.DT_LIMIT)
> + return mk_timestamp(dt, lhs.secs % SECS_PER_DAY,
> + lhs.nsec, lhs.offset)
> + else
> + error("datetime:__sub(date or interval) - incompatible type of arguments", 2)
> + 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("datetime:__sub(date or interval) - incompatible type of arguments", 2)
> + 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, "datetime:__add(interval)")
> + check_interval(s, "datetime:__add(interval)")
tarantool> return require('datetime').now() + 1
---
- error: '[string "return require(''datetime'').now() + 1"]:1: Usage:
datetime:__add(interval)'
...
Looks a bit confusing. User doesn't know about metamethods.
> + local left_t = ffi.typeof(d)
> + local right_t = ffi.typeof(s)
> + local o
> +
More information about the Tarantool-patches
mailing list