From 8e7514b9d6f0eaafee82db8efecb41f4a7d6e843 Mon Sep 17 00:00:00 2001
From: Oleg Babin <babinoleg@mail.ru>
Date: Sat, 22 Apr 2023 08:11:27 +0300
Subject: [PATCH] datetime: fix invalid representation of timestamps with
 fraction part

Sometimes we need negative timestamps to work with dates before
1970. But seems such cases were even covered in tests. So there
wasn't any handling of negative timestamps with fraction part.
Such datetime objects had incorrect string representation (e.g.
"1963-11-22T12:30:02.-999"). This patch fixes it.

Closes #8570

NO_DOC=bugfix
---
 .../gh-8570-fix-datetime-str-negative-nsec.md    |  4 ++++
 src/lua/datetime.lua                             | 14 ++++++++++++++
 test/app-tap/datetime.test.lua                   | 16 +++++++++++++---
 3 files changed, 31 insertions(+), 3 deletions(-)
 create mode 100644 changelogs/unreleased/gh-8570-fix-datetime-str-negative-nsec.md

diff --git a/changelogs/unreleased/gh-8570-fix-datetime-str-negative-nsec.md b/changelogs/unreleased/gh-8570-fix-datetime-str-negative-nsec.md
new file mode 100644
index 0000000000..d75101f383
--- /dev/null
+++ b/changelogs/unreleased/gh-8570-fix-datetime-str-negative-nsec.md
@@ -0,0 +1,4 @@
+## bugfix/datetime
+
+* Fixed errors when the string representation of a datetime object had
+a negative nanosecond part (gh-8570).
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index 7bb84badbb..60949a8e6e 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -588,6 +588,13 @@ local function datetime_new(obj)
         end
         local fraction
         s, fraction = math_modf(ts)
+        -- In case of negative fraction part we should
+        -- make it positive at the expense of the integer part.
+        -- Code below expects that "nsec" value is always positive.
+        if fraction < 0 then
+            s = s - 1
+            fraction = fraction + 1
+        end
         -- if there are separate nsec, usec, or msec provided then
         -- timestamp should be integer
         if count_usec == 0 then
@@ -1085,6 +1092,13 @@ local function datetime_set(self, obj)
         end
         local sec_int, fraction
         sec_int, fraction = math_modf(ts)
+        -- In case of negative fraction part we should
+        -- make it positive at the expense of the integer part.
+        -- Code below expects that "nsec" value is always positive.
+        if fraction < 0 then
+            sec_int = sec_int - 1
+            fraction = fraction + 1
+        end
         -- if there is one of nsec, usec, msec provided
         -- then ignore fraction in timestamp
         -- otherwise - use nsec, usec, or msec
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index a5cc8c9446..790ec29ac6 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -192,7 +192,7 @@ test:test("Default date creation and comparison", function(test)
 end)
 
 test:test("Simple date creation by attributes", function(test)
-    test:plan(14)
+    test:plan(15)
     local ts
     local obj = {}
     local attribs = {
@@ -222,6 +222,8 @@ test:test("Simple date creation by attributes", function(test)
             '2021-08-30T21:31:11.000123Z', '{timestamp.usec}')
     test:is(tostring(date.new{timestamp = 1630359071, nsec = 123}),
             '2021-08-30T21:31:11.000000123Z', '{timestamp.nsec}')
+    test:is(tostring(date.new{timestamp = -0.1}),
+            '1969-12-31T23:59:59.900Z', '{negative timestamp}')
 end)
 
 test:test("Simple date creation by attributes - check failed", function(test)
@@ -1779,7 +1781,7 @@ test:test("totable{}", function(test)
 end)
 
 test:test("Time :set{} operations", function(test)
-    test:plan(15)
+    test:plan(16)
 
     local ts = date.new{ year = 2021, month = 8, day = 31,
                   hour = 0, min = 31, sec = 11, tzoffset = '+0300'}
@@ -1813,10 +1815,12 @@ test:test("Time :set{} operations", function(test)
             '2021-08-30T21:31:11.000123+0800', 'timestamp + usec')
     test:is(tostring(ts:set{timestamp = 1630359071, nsec = 123}),
             '2021-08-30T21:31:11.000000123+0800', 'timestamp + nsec')
+    test:is(tostring(ts:set{timestamp = -0.1}),
+            '1969-12-31T23:59:59.900+0800', 'negative timestamp')
 end)
 
 test:test("Check :set{} and .new{} equal for all attributes", function(test)
-    test:plan(11)
+    test:plan(12)
     local ts, ts2
     local obj = {}
     local attribs = {
@@ -1845,6 +1849,12 @@ test:test("Check :set{} and .new{} equal for all attributes", function(test)
     ts2 = date.new():set(obj)
     test:is(ts, ts2, ('timestamp+tzoffset (%s = %s)'):
             format(tostring(ts), tostring(ts2)))
+
+    obj = {timestamp = -0.1, tzoffset = '+0800'}
+    ts = date.new(obj)
+    ts2 = date.new():set(obj)
+    test:is(ts, ts2, ('negative timestamp+tzoffset (%s = %s)'):
+            format(tostring(ts), tostring(ts2)))
 end)
 
 
-- 
GitLab