Commit Diff


commit - 05ed2619457dea473828d9adb226665354a6bce0
commit + 0fde12ee7e3409f4e14dbc784d8ab232bb5ff694
blob - fa42293e1dca16558dfde4971a083f9e126a7385
blob + c7d6d206f693e55e13c0f721cb0421829abdc885
--- tests/tarantool_datetime_new.lua
+++ tests/tarantool_datetime_new.lua
@@ -1,254 +1,25 @@
---[[
-    https://web.archive.org/web/20150906092420/http://www.cs.tau.ac.il/~nachumd/horror.html
-    https://web.archive.org/web/20150908004245/http://www.merlyn.demon.co.uk/critdate.htm
-    https://en.wikipedia.org/wiki/Epoch#Notable_epoch_dates_in_computing
-    https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs
-    https://en.wikipedia.org/wiki/Leap_year_problem
-    https://medium.com/grandcentrix/property-based-test-driven-development-in-elixir-49cc70c7c3c4
-    https://groups.google.com/g/comp.dcom.telecom/c/Qq0lwZYG_fI
-
-    https://cs.opensource.google/go/go/+/refs/tags/go1.18.2:src/time/zoneinfo_test.go
-    https://github.com/dateutil/dateutil/blob/master/tests/test_tz.py
-    https://github.com/Zac-HD/stdlib-property-tests/blob/master/tests/test_datetime.py
-
-"datetime.parse_date"
-"datetime.now"
-"datetime.interval"
-
-https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/
-
--- Property: Timezone changes when DST applied.
--- Property: The day before Saturday is always Friday.
--- Property: 29.02.YYYY + 1 day == 01.03.(YYYY + 1), where YYYY is a leap year.
--- Property: 28.02.YYYY + 1 day == 01.03.(YYYY + 1), where YYYY is a non-leap year.
-
--- Прибавление месяцев к времени даёт ту же дату в другом месяце, кроме
--- случаев, когда в итоговом месяце меньше дней нежели в исходном. В этом
--- случае получаем последний день.
--- 31 января + 1 месяц = 28 или 29 февраля
--- 30 января + 1 месяц = 28 или 29 февраля
--- 29 февраля + 1 месяц = 29 марта
--- 31 марта + 1 месяц = 30 апреля
-
--- Прибавление месяцев к последнему дню месяца (требует обсуждения). При
--- прибавлении месяцев к последнему дню месяца надо получать последний день
--- месяца.
--- 31 января + 1 месяц  = 28 или 29 февраля
--- 29 февраля + 1 месяц = 31 марта
--- 31 марта + 1 месяц = 30 апреля
--- 30 апреля + 1 месяц = 31 мая
--- 28 февраля 2001 + 1 месяц = 28 марта 2001
--- 28 февраля 2004 + 1 месяц = 28 марта 2004
-]]
-
-local math = require("math")
-local datetime = require("datetime")
-local luzer = require("luzer")
-
-local MIN_DATE_YEAR = -5879610
-local MAX_DATE_YEAR = 5879611
-
-local function new_dt_fmt(fdp)
-    -- Field descriptors.
-    local desc = {
-                '%a',
-                '%A',
-                '%b',
-                '%B',
-                '%h',
-                '%c',
-                '%C',
-                '%d',
-                '%e',
-                '%D',
-                '%H',
-                '%I',
-                '%j',
-                '%m',
-                '%M',
-                '%n',
-                '%p',
-                '%r',
-                '%R',
-                '%S',
-                '%t',
-                '%T',
-                '%U',
-                '%w',
-                '%W',
-                '%x',
-                '%X',
-                '%y',
-                '%Y',
-    }
-    local n = fdp:consume_integer(1, 5)
-    local fmt = ''
-    for _ = 1, n do
-        local field_idx = fdp:consume_integer(1, #desc)
-        fmt = ("%s%s"):format(fmt, desc[field_idx])
-    end
-
-    return fmt
-end
-
-local function new_dt(fdp)
-    local tz_idx = fdp:consume_integer(1, #datetime.TZ)
-    local d = 0
-    --[[
-    Day number. Value range: 1 - 31. The special value -1 generates the last
-    day of a particular month.
-    ]]
-    while d == 0 do
-        d = fdp:consume_integer(-1, 31)
-    end
-
-    return {
-        -- FIXME: only one of nsec, usec or msecs may be defined simultaneously.
-        -- TODO: usec
-        -- TODO: msec
-        nsec      = fdp:consume_integer(0, 1000000000),
-        sec       = fdp:consume_integer(0, 60),
-        min       = fdp:consume_integer(0, 59),
-        hour      = fdp:consume_integer(0, 23),
-        day       = d,
-        month     = fdp:consume_integer(1, 12),
-        year      = fdp:consume_integer(MIN_DATE_YEAR, MAX_DATE_YEAR),
-        tzoffset  = fdp:consume_integer(-720, 840),
-        tz        = datetime.TZ[tz_idx],
-    }
-end
-
--- Minimum supported date - -5879610-06-22.
--- luacheck: no unused
-local min_dt = {
-    nsec      = 0,
-    sec       = 0,
-    min       = 0,
-    hour      = 0,
-    day       = -1,
-    month     = 1,
-    year      = MIN_DATE_YEAR,
-    tzoffset  = -720,
-}
-
--- Maximum supported date - 5879611-07-11.
--- luacheck: no unused
-local max_dt = {
-    nsec      = 1000000000,
-    sec       = 60,
-    min       = 59,
-    hour      = 23,
-    day       = 31,
-    month     = 12,
-    year      = MAX_DATE_YEAR,
-    tzoffset  = 840,
-}
-
--- https://docs.microsoft.com/en-us/office/troubleshoot/excel/determine-a-leap-year
-local function isLeapYear(year)
-    -- bool leap = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
-    if year%4 ~= 0 then
-        return false
-    elseif year%100 ~= 0 then
-        return true
-    elseif year%400 ~= 0 then
-        return false
-    else
-        return true
-    end
-end
-
-local function getLeapYear(is_leap)
-    while true do
-        local y = math.random(MIN_DATE_YEAR, MAX_DATE_YEAR)
-        if isLeapYear(y) == is_leap then
-            return y
-        end
-    end
-end
-
 local function TestOneInput(buf)
     local fdp = luzer.FuzzedDataProvider(buf)
-    local time_units1 = new_dt(fdp)
-    local time_units2 = new_dt(fdp)
-    local dt1, dt2
 
-    -- Sanity check.
-    if not pcall(datetime.new, time_units1) or
-       not pcall(datetime.new, time_units2) then
-        return
-    end
+    -- PROPERTY: The fields in datetime constructor must be equal
+    -- to the same fields in datetime object and `:totable()`.
 
-    local datetime_fmt = new_dt_fmt(fdp)
-
-    -- Property: datetime.parse(dt:format(random_format)) == dt
-    --[[
+    -- PROPERTY: datetime.parse(tostring(dt)):format() == tostring(dt)
     dt1 = datetime.new(time_units1)
-    local dt1_str = dt1:format(datetime_fmt)
-    local dt_parsed = datetime.parse(dt1_str, { format = datetime_fmt })
-    assert(dt_parsed == dt1)
-    ]]
-
-    -- Property: B - (B - A) == A
-    -- Blocked by: https://github.com/tarantool/tarantool/issues/7145
-    dt1 = datetime.new(time_units1)
     dt2 = datetime.new(time_units2)
-    local sub_dt = dt1 - dt2
-    local add_dt = sub_dt + dt2
-    -- GH-7145 assert(add_dt == dt1, "(A - B) + B != A")
-
-    -- Property: A - A == B - B
-    -- https://github.com/tarantool/tarantool/issues/7144
-    dt1 = datetime.new(time_units1)
-    dt2 = datetime.new(time_units2)
-    assert(dt1 - dt1 == dt2 - dt2, "A - A != B - B")
-
-    -- Property: datetime.new(dt) == datetime.new():set(dt)
-    dt1 = datetime.new(time_units1)
-    assert(dt1 == datetime.new():set(time_units1), "new(dt) != new():set(dt)")
-
-    -- Property: dt == datetime.new(dt):totable()
-    dt1 = datetime.new(time_units1)
-    table.equals(dt1, dt1:totable())
-
-    -- Property: datetime.parse(tostring(dt)):format() == tostring(dt)
-    dt1 = datetime.new(time_units1)
-    dt2 = datetime.new(time_units2)
     local dt_iso8601 = datetime.parse(tostring(dt1), {format = 'iso8601'}):format()
     assert(dt_iso8601 == tostring(dt1), ('Parse roundtrip with iso8601 %s'):format(tostring(dt1)))
     local dt_rfc3339 = datetime.parse(tostring(dt1), {format = 'rfc3339'}):format()
     assert(dt_rfc3339 == tostring(dt1), ('Parse roundtrip with rfc3339 %s'):format(tostring(dt1)))
 
-    -- Property: in leap year last day in February is 29.
+    -- PROPERTY: Formatted datetime is the same as produced by os.date().
     dt1 = datetime.new(time_units1)
-    dt1:set({
-        day = 01,
-        month = 02,
-        year = getLeapYear(true),
-    })
-    assert(dt1:set({ day = -1 }).day == 29, ("Last day in %s (leap year) is not a 29"):format(tostring(dt1)))
+    -- Seems `os.date()` does not support negative epoch.
 
-    -- Property: in non-leap year last day in February is 28.
-    dt1 = datetime.new(time_units1)
-    dt1:set({
-        day = 01,
-        month = 02,
-        year = getLeapYear(false),
-    })
-    assert(dt1:set({ day = -1 }).day == 28, ("Last day in %s (non-leap year) is not a 28"):format(tostring(dt1)))
-
-    -- Property: Formatted datetime is the same as produced by os.date().
-    dt1 = datetime.new(time_units1)
-    -- Seems os.date() does not support negative epoch.
-    if dt1.epoch > 0 then
-        local msg = ('os.date("%s", %d) != dt:format("%s")'):format(datetime_fmt, dt1.epoch, datetime_fmt)
-        --assert(os.date(datetime_fmt, dt1.epoch) == dt1:format(datetime_fmt), msg)
-    end
-
-    -- Property: 28.02.YYYY + 1 year == 28.02.(YYYY + 1), where YYYY is a non-leap year.
+    -- PROPERTY: 28.02.YYYY + 1 year == 28.02.(YYYY + 1), where YYYY is a non-leap year.
     local dt1 = datetime.new(time_units1)
     dt1:set({
-        year = getLeapYear(false),
+        year = random_year(fdp, false),
         month = 02,
         day = 28,
     })
@@ -259,10 +30,11 @@ local function TestOneInput(buf)
         -- TODO: assert(dt_plus_1y.day == 28, msg)
     end
 
-    -- Property: 29.02.YYYY + 1 year == 28.02.(YYYY + 1), where YYYY is a leap year.
+    -- PROPERTY: 29.02.YYYY + 1 year == 28.02.(YYYY + 1),
+    -- where YYYY is a leap year.
     local dt1 = datetime.new(time_units1)
     dt1:set({
-        year = getLeapYear(true),
+        year = random_year(fdp, true),
         month = 02,
         day = 29,
     })
@@ -273,7 +45,7 @@ local function TestOneInput(buf)
         assert(dt_plus_1y.day == 28, msg)
     end
 
-    -- Property: 31.03.YYYY + 1 month == 30.04.YYYY
+    -- PROPERTY: 31.03.YYYY + 1 month == 30.04.YYYY
     local dt1 = datetime.new(time_units1)
     dt1:set({
         month = 03,
@@ -286,35 +58,12 @@ local function TestOneInput(buf)
     msg = ('31.03.YYYY + 1m != 30.04.YYYY: %s + 1m != %s'):format(dt1, dt_plus_1m)
     assert(dt_plus_1m.month == 04, msg)
 
-    -- Property: 31.12.YYYY + 1 day == 01.01.(YYYY + 1)
-    -- "February 29 is not the only day affected by the leap year. Another very
-    -- important date is December 31, because it is the 366th day of the year and
-    -- many applications mistakenly hard-code a year as 365 days."
-    -- Source: https://azure.microsoft.com/en-us/blog/is-your-code-ready-for-the-leap-year/
-    local dt1 = datetime.new(time_units1)
-    dt1:set({
-        day = 01,
-        month = 12,
-    })
-    dt1 = dt1:set({ day = -1 })
-    assert(dt1.day == 31)
-    dt1 = dt1:add({ day = 1})
-    -- TODO: assert(dt.day == 1, ('31 Dec + 1 day != 1 Jan (%s)'):format(dt))
-
-    -- Property: Difference of datetimes with leap and non-leap years is 1 second.
-    local leap_year = getLeapYear(true)
-    local non_leap_year = getLeapYear(false)
+    -- PROPERTY: Difference of datetimes with leap and non-leap
+    -- years is a 1 second.
+    local leap_year = datetime_lib.random_year(fdp, true)
+    local non_leap_year = datetime_lib.random_year(fdp, false)
     dt1 = datetime.new({ year = leap_year })
     dt2 = datetime.new({ year = non_leap_year })
     --local diff = datetime.new():set({ year = leap_year - non_leap_year, sec = 1 })
     -- TODO: assert(dt1 - dt2 == single_sec, ('%s - %s != 1 sec (%s)'):format(dt1, dt2, dt1 - dt2))
 end
-
-local args = {
-    max_len = 2048,
-    print_pcs = 1,
-    detect_leaks = 1,
-    artifact_prefix = "datetime_new_",
-    max_total_time = 60,
-}
-luzer.Fuzz(TestOneInput, nil, args)
blob - /dev/null
blob + 1cfac338648e8dca2c9dbe6de88fef29ad25a226 (mode 644)
--- /dev/null
+++ tests/tarantool_datetime_arith.lua
@@ -0,0 +1,47 @@
+local datetime = require("datetime")
+local datetime_lib = require("tarantool_datetime_lib")
+local luzer = require("luzer")
+
+local function TestOneInput(buf)
+    local fdp = luzer.FuzzedDataProvider(buf)
+    local dt_fields1 = datetime_lib.random_dt_str(fdp)
+    local dt_fields2 = datetime_lib.random_dt_str(fdp)
+    local dt1 = datetime.new(dt_fields1)
+    local dt2 = datetime.new(dt_fields2)
+
+    -- PROPERTY: B - (B - A) == A
+    -- Blocked by https://github.com/tarantool/tarantool/issues/7145
+    local sub_dt = dt1 - dt2
+    local add_dt = sub_dt + dt2
+    assert(add_dt == dt1, "(A - B) + B != A")
+
+    -- PROPERTY: A - A == B - B == +0 seconds
+    local iv1 = datetime.interval.new()
+    assert(dt1 - dt1 == dt2 - dt2, "A - A == B - B")
+    assert(dt1 - dt1 == iv1, "A - A == +0 seconds")
+
+    -- PROPERTY: dt1 + dt2 == dt1:add(dt2)
+    -- assert(dt + dt_fields == dt:add(dt_fields), "dt1 + dt2 == dt1:add(dt2)")
+    -- BROKEN: addition makes date less than minimum allowed -5879610-06-22
+
+    -- PROPERTY: 31.12.YYYY + 1 day == 01.01.(YYYY + 1)
+    -- "February 29 is not the only day affected by the leap year. Another very
+    -- important date is December 31, because it is the 366th day of the year and
+    -- many applications mistakenly hard-code a year as 365 days."
+    -- Source: https://azure.microsoft.com/en-us/blog/is-your-code-ready-for-the-leap-year/
+    dt1 = dt1:add({ day = 1})
+    -- TODO: assert(dt.day == 1, ('31 Dec + 1 day != 1 Jan (%s)'):format(dt))
+
+    -- PROPERTY: dt1.wday == (dt1 + 7 days).wday
+    iv1 = datetime.interval.new({ day = 7 })
+    assert((dt1 + iv1).wday == dt1.wday, "")
+    assert(dt1:add({ day = 7 }).wday == dt1.wday, "")
+end
+
+local args = {
+    print_pcs = 1,
+    artifact_prefix = "datetime_arith_",
+    max_len = 2048,
+    max_total_time = 60,
+}
+luzer.Fuzz(TestOneInput, nil, args)
blob - abadb02226f61ebc601fb2d883f3e5c68e686b4e
blob + 04ff8d36cdb2c8c81557e501e6651ae78c0d491d
--- tests/tarantool_datetime_parse.lua
+++ tests/tarantool_datetime_parse.lua
@@ -1,8 +1,32 @@
 local datetime = require("datetime")
+local datetime_lib = require("tarantool_datetime_lib")
 local luzer = require("luzer")
 
 local function TestOneInput(buf)
-    pcall(datetime.parse, buf)
+    local fdp = luzer.FuzzedDataProvider(buf)
+
+    -- PROPERTY:
+    local str = fdp:consume_string(100)
+    local ok, dt = pcall(datetime.parse, str)
+    if ok then
+        datetime.parse(dt:format())
+    end
+
+    -- PROPERTY:
+    -- datetime.parse(<buf>) == datetime.parse(<buf>, { format = <fmt> })
+    local str, fmt = datetime_lib.random_dt_str(fdp)
+    local ok, res1 = pcall(datetime.parse, str)
+    if not ok then return end
+    local ok, res2 = pcall(datetime.parse, str, { format = fmt })
+    assert(ok == true)
+    assert(res1 == res2)
+
+    -- PROPERTY: datetime.parse(dt:format(random_format), {format = random_format}) == dt
+    -- dt1 = datetime.new(time_units1)
+    -- local dt1_str = dt1:format(datetime_fmt)
+    -- local dt_parsed = datetime.parse(dt1_str, { format = datetime_fmt })
+    -- assert(dt_parsed == dt1, tostring(dt_parsed) .. ' == ' .. dt1_str .. '(' .. datetime_fmt .. ')')
+    -- BROKEN: Wrong assumption.
 end
 
 local args = {
blob - /dev/null
blob + 04ecfb34845a5839a5757619915976536af4972d (mode 644)
--- /dev/null
+++ tests/tarantool_datetime_ctr.lua
@@ -0,0 +1,37 @@
+local datetime = require("datetime")
+local datetime_lib = require("tarantool_datetime_lib")
+local luzer = require("luzer")
+
+local function TestOneInput(buf)
+    local fdp = luzer.FuzzedDataProvider(buf)
+    local dt_fields = datetime_lib.random_dt_str(fdp)
+    local dt = datetime.new(dt_fields)
+
+    -- PROPERTY: dt_fields == datetime.new(dt_fields):totable()
+    table.equals(dt_fields, dt:totable())
+
+    -- PROPERTY: dt == datetime.new({ timestamp = dt.timestamp })
+    -- BROKEN: -4613096-08-31T19:19:36.119810579+0518 == -4613096-08-31T14:01:36.125Z
+    assert(dt == datetime.new({ timestamp = dt.timestamp }))
+
+    -- PROPERTY: datetime.new(dt) == datetime.new():set(dt)
+    assert(dt == datetime.new():set(dt_fields), "new(dt) == new():set(dt)")
+
+    -- PROPERTY: Last day in a February is 28 in non-leap year,
+    --           and 29 in a leap year.
+    local is_leap = datetime_lib.oneof({true, false})
+    dt = datetime.new({
+        year = datetime_lib.random_year(fdp, is_leap),
+        month = 02,
+        day = -1,
+    })
+    assert(dt.day == is_leap and 29 or 28, "last day in February")
+end
+
+local args = {
+    print_pcs = 1,
+    artifact_prefix = "datetime_ctr_",
+    max_len = 2048,
+    max_total_time = 60,
+}
+luzer.Fuzz(TestOneInput, nil, args)
blob - /dev/null
blob + daf60630020be373f22788894958e2ebaaa356e9 (mode 644)
--- /dev/null
+++ tests/tarantool_datetime_lib.lua
@@ -0,0 +1,167 @@
+local MAX_DATE_YEAR = 5879611
+local MIN_DATE_YEAR = -MAX_DATE_YEAR
+
+local fmt_conv_spec = {
+    '%a',
+    '%A',
+    '%b',
+    '%B',
+    '%h',
+    '%c',
+    '%C',
+    '%d',
+    '%e',
+    '%D',
+    '%H',
+    '%I',
+    '%j',
+    '%m',
+    '%M',
+    '%n',
+    '%p',
+    '%r',
+    '%R',
+    '%S',
+    '%t',
+    '%T',
+    '%U',
+    '%w',
+    '%W',
+    '%x',
+    '%X',
+    '%y',
+    '%Y',
+}
+
+local function keys(t)
+    assert(next(t) ~= nil)
+    local table_keys = {}
+    for k, _ in pairs(t) do
+        table.insert(table_keys, k)
+    end
+    return table_keys
+end
+
+local function oneof(tbl, fdp)
+    assert(type(tbl) == 'table')
+    assert(next(tbl) ~= nil)
+    assert(fdp)
+
+    local n = table.getn(tbl)
+    local idx = fdp:consume_integer(1, n)
+
+    return tbl[idx]
+end
+
+-- https://docs.microsoft.com/en-us/office/troubleshoot/excel/determine-a-leap-year
+local function is_leap_year(year)
+    -- bool leap = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
+    if year % 4 ~= 0 then
+        return false
+    elseif year % 100 ~= 0 then
+        return true
+    elseif year % 400 ~= 0 then
+        return false
+    else
+        return true
+    end
+end
+
+local function random_usec(fdp)
+    return fdp:consume_integer(0, 10^3)
+end
+
+local function random_msec(fdp)
+    return fdp:consume_integer(0, 10^6)
+end
+
+local function random_nsec(fdp)
+    return fdp:consume_integer(0, 10^9)
+end
+
+local function random_sec(fdp)
+    return fdp:consume_integer(0, 60)
+end
+
+local function random_min(fdp)
+    return fdp:consume_integer(0, 59)
+end
+
+local function random_hour(fdp)
+    return fdp:consume_integer(0, 23)
+end
+
+local function random_day(fdp)
+    return fdp:consume_integer(1, 31)
+end
+
+local function random_month(fdp)
+    return fdp:consume_integer(1, 12)
+end
+
+local function random_year(fdp, is_leap)
+    local y = fdp:consume_integer(MIN_DATE_YEAR, MAX_DATE_YEAR)
+    if is_leap and not is_leap_year(y) then
+        return random_year(fdp, is_leap)
+    end
+    return y
+end
+
+local function random_tzoffset(fdp)
+    -- TODO: '+02:00'
+    return fdp:consume_integer(-720, 840)
+end
+
+local function random_tz(fdp)
+    return oneof(datetime.TZ)
+end
+
+-- Maximum supported date - 5879611-07-11.
+-- Minimum supported date - -5879610-06-22.
+-- TODO: Only one of nsec, usec or msecs may be defined
+--       simultaneously.
+-- TODO: Day equal to -1 is a special.
+local function random_dt_fields(fdp)
+    return {
+        sec       = random_sec(fdp),
+        min       = random_min(fdp),
+        hour      = random_hour(fdp),
+        day       = random_day(fdp),
+        month     = random_month(fdp),
+        year      = random_year(fdp),
+        tzoffset  = random_tzoffset(fdp),
+        tz        = random_tz(fdp),
+    }
+end
+
+local function random_dt_str(fdp)
+    -- Field descriptors.
+    local n = fdp:consume_integer(1, 5)
+    local fmt = ''
+    for _ = 1, n do
+        local field_idx = fdp:consume_integer(1, #fmt_conv_spec)
+        fmt = ("%s%s"):format(fmt, fmt_conv_spec[field_idx])
+    end
+
+    return fmt
+end
+
+return {
+    random_dt_str = random_dt_str,
+    random_dt_fields = random_dt_fields,
+
+    random_usec = random_usec,
+    random_msec = random_msec,
+    random_nsec = random_nsec,
+    random_sec = random_sec,
+    random_min = random_min,
+    random_hour = random_hour,
+    random_day = random_day,
+    random_month = random_month,
+    random_year = random_year,
+    random_tzoffset = random_tzoffset,
+    random_tz = random_tz,
+
+    keys = keys,
+    oneof = oneof,
+}