util.datetime: Add support for sub-second precision timestamps
authorKim Alvefur <zash@zash.se>
Sun, 14 Aug 2022 16:57:31 +0200
changeset 12633 4c1d3f817063
parent 12632 b95da9a593be
child 12634 781772c8b6d9
util.datetime: Add support for sub-second precision timestamps Lua since 5.3 raises a fuss when time functions are handed a number with a fractional part and the underlying C functions are all based on integer seconds without support for more precision.
CHANGES
spec/util_datetime_spec.lua
util/datetime.lua
--- a/CHANGES	Sun Aug 14 16:51:10 2022 +0200
+++ b/CHANGES	Sun Aug 14 16:57:31 2022 +0200
@@ -19,6 +19,10 @@
 - Advertise supported SASL Channel-Binding types (XEP-0440)
 - Implement RFC 9266 'tls-exporter' channel binding with TLS 1.3
 
+## Changes
+
+- Support sub-second precision timestamps
+
 ## Removed
 
 - Lua 5.1 support
--- a/spec/util_datetime_spec.lua	Sun Aug 14 16:51:10 2022 +0200
+++ b/spec/util_datetime_spec.lua	Sun Aug 14 16:57:31 2022 +0200
@@ -18,6 +18,9 @@
 		it("should work", function ()
 			assert.equals("2006-01-02", date(1136239445));
 		end);
+		it("should ignore fractional parts", function ()
+			assert.equals("2006-01-02", date(1136239445.5));
+		end);
 	end);
 	describe("#time", function ()
 		local time = util_datetime.time;
@@ -34,6 +37,9 @@
 		it("should work", function ()
 			assert.equals("22:04:05", time(1136239445));
 		end);
+		it("should handle precision", function ()
+			assert.equal("14:46:32.158200", time(1660488392.1582))
+		end)
 	end);
 	describe("#datetime", function ()
 		local datetime = util_datetime.datetime;
@@ -50,6 +56,9 @@
 		it("should work", function ()
 			assert.equals("2006-01-02T22:04:05Z", datetime(1136239445));
 		end);
+		it("should handle precision", function ()
+			assert.equal("2022-08-14T14:46:32.158200Z", datetime(1660488392.1582))
+		end)
 	end);
 	describe("#legacy", function ()
 		local legacy = util_datetime.legacy;
@@ -72,5 +81,9 @@
 			-- https://xmpp.org/extensions/xep-0082.html#example-2 and 3
 			assert.equals(parse("1969-07-21T02:56:15Z"), parse("1969-07-20T21:56:15-05:00"));
 		end);
+		it("should handle precision", function ()
+			-- floating point comparison is not an exact science
+			assert.truthy(math.abs(1660488392.1582 - parse("2022-08-14T14:46:32.158200Z")) < 0.001)
+		end)
 	end);
 end);
--- a/util/datetime.lua	Sun Aug 14 16:51:10 2022 +0200
+++ b/util/datetime.lua	Sun Aug 14 16:57:31 2022 +0200
@@ -12,31 +12,42 @@
 local os_date = os.date;
 local os_time = os.time;
 local os_difftime = os.difftime;
+local floor = math.floor;
 local tonumber = tonumber;
 
 local _ENV = nil;
 -- luacheck: std none
 
 local function date(t)
-	return os_date("!%Y-%m-%d", t);
+	return os_date("!%Y-%m-%d", t and floor(t) or nil);
 end
 
 local function datetime(t)
-	return os_date("!%Y-%m-%dT%H:%M:%SZ", t);
+	if t == nil or t % 1 == 0 then
+		return os_date("!%Y-%m-%dT%H:%M:%SZ", t);
+	end
+	local m = t % 1;
+	local s = floor(t);
+	return os_date("!%Y-%m-%dT%H:%M:%S.%%06dZ", s):format(floor(m * 1000000));
 end
 
 local function time(t)
-	return os_date("!%H:%M:%S", t);
+	if t == nil or t % 1 == 0 then
+		return os_date("!%H:%M:%S", t);
+	end
+	local m = t % 1;
+	local s = floor(t);
+	return os_date("!%H:%M:%S.%%06d", s):format(floor(m * 1000000));
 end
 
 local function legacy(t)
-	return os_date("!%Y%m%dT%H:%M:%S", t);
+	return os_date("!%Y%m%dT%H:%M:%S", t and floor(t) or nil);
 end
 
 local function parse(s)
 	if s then
 		local year, month, day, hour, min, sec, tzd;
-		year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$");
+		year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d%.?%d*)([Z+%-]?.*)$");
 		if year then
 			local now = os_time();
 			local time_offset = os_difftime(os_time(os_date("*t", now)), os_time(os_date("!*t", now))); -- to deal with local timezone
@@ -49,8 +60,9 @@
 				tzd_offset = h * 60 * 60 + m * 60;
 				if sign == "-" then tzd_offset = -tzd_offset; end
 			end
-			sec = (sec + time_offset) - tzd_offset;
-			return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false});
+			local prec = sec%1;
+			sec = floor(sec + time_offset) - tzd_offset;
+			return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false})+prec;
 		end
 	end
 end