Lua ISO 8601 datetime parsing pattern - parsing

I'm trying to parse a full ISO8601 datetime from JSON data in Lua.
I'm having trouble with the match pattern.
So far, this is what I have:
-- Example datetime string 2011-10-25T00:29:55.503-04:00
local datetime = "2011-10-25T00:29:55.503-04:00"
local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)%.(%d+)"
local xyear, xmonth, xday, xhour, xminute,
xseconds, xmillies, xoffset = datetime:match(pattern)
local convertedTimestamp = os.time({year = xyear, month = xmonth,
day = xday, hour = xhour, min = xminute, sec = xseconds})
I'm stuck at how to deal with the timezone on the pattern because there is no logical or that will handle the - or + or none.
Although I know lua doesn't support the timezone in the os.time function, at least I would know how it needed to be adjusted.
I've considered stripping off everything after the "." (milliseconds and timezone), but then i really wouldn't have a valid datetime. Milliseconds is not all that important and i wouldn't mind losing it, but the timezone changes things.
Note: Somebody may have some much better code for doing this and I'm not married to it, I just need to get something useful out of the datetime string :)

The full ISO 8601 format can't be done with a single pattern match. There is too much variation.
Some examples from the wikipedia page:
There is a "compressed" format that doesn't separate numbers: YYYYMMDD vs YYYY-MM-DD
The day can be omited: YYYY-MM-DD and YYYY-MM are both valid dates
The ordinal date is also valid: YYYY-DDD, where DDD is the day of the year (1-365/6)
When representing the time, the minutes and seconds can be ommited: hh:mm:ss, hh:mm and hh are all valid times
Moreover, time also has a compressed version: hhmmss, hhmm
And on top of that, time accepts fractions, using both the dot or the comma to denote fractions of the lower time element in the time section. 14:30,5, 1430,5, 14:30.5, or 1430.5 all represent 14 hours, 30 seconds and a half.
Finally, the timezone section is optional. When present, it can be either the letter Z, ±hh:mm, ±hh or ±hhmm.
So, there are lots of possible exceptions to take into account, if you are going to parse according to the full spec. In that case, your initial code might look like this:
function parseDateTime(str)
local Y,M,D = parseDate(str)
local h,m,s = parseTime(str)
local oh,om = parseOffset(str)
return os.time({year=Y, month=M, day=D, hour=(h+oh), min=(m+om), sec=s})
end
And then you would have to create parseDate, parseTime and parseOffset. The later should return the time offsets from UTC, while the first two would have to take into account things like compressed formats, time fractions, comma or dot separators, and the like.
parseDate will likely use the "^" character at the beginning of its pattern matches, since the date has to be at the beginning of the string. parseTime's patterns will likely start with "T". And parseOffset's will end with "$", since the time offsets, when they exist, are at the end.
A "full ISO" parseOffset function might look similar to this:
function parseOffset(str)
if str:sub(-1)=="Z" then return 0,0 end -- ends with Z, Zulu time
-- matches ±hh:mm, ±hhmm or ±hh; else returns nils
local sign, oh, om = str:match("([-+])(%d%d):?(%d?%d?)$")
sign, oh, om = sign or "+", oh or "00", om or "00"
return tonumber(sign .. oh), tonumber(sign .. om)
end
By the way, I'm assuming that your computer is working in UTC time. If that's not the case, you will have to include an additional offset on your hours/minutes to account for that.
function parseDateTime(str)
local Y,M,D = parseDate(str)
local h,m,s = parseTime(str)
local oh,om = parseOffset(str)
local loh,lom = getLocalUTCOffset()
return os.time({year=Y, month=M, day=D, hour=(h+oh-loh), min=(m+om-lom), sec=s})
end
To get your local offset you might want to look at http://lua-users.org/wiki/TimeZone .
I hope this helps. Regards!

There is also the luadate package, which supports iso8601. (You probably want the patched version)

Here is a simple parseDate function for ISO dates. Note that I'm using "now" as a fallback. This may or may not work for you. YMMV 😉.
--[[
Parse date given in any of supported forms.
Note! For unrecognised format will return now.
#param str ISO date. Formats:
Y-m-d
Y-m -- this will assume January
Y -- this will assume 1st January
]]
function parseDate(str)
local y, m, d = str:match("(%d%d%d%d)-?(%d?%d?)-?(%d?%d?)$")
-- fallback to now
if y == nil then
return os.time()
end
-- defaults
if m == '' then
m = 1
end
if d == '' then
d = 1
end
-- create time
return os.time{year=y, month=m, day=d, hour=0}
end
--[[
--Tests:
print( os.date( "%Y-%m-%d", parseDate("2019-12-28") ) )
print( os.date( "%Y-%m-%d", parseDate("2019-12") ) )
print( os.date( "%Y-%m-%d", parseDate("2019") ) )
]]

Related

Parse time string to hours, minutes and seconds in Lua

I am currently working on a plugin for grandMA2 lighting control using Lua. I need the current time. The only way to get the current time is the following function:
gma.show.getvar('TIME')
which always returns the current system time, which I then store in a variable. An example return value is "12h54m47.517s".
How can I separate the hours, minutes and seconds into 3 variables?
If os.date is available (and matches gma.show.getvar('TIME')), this is trivial:
If format starts with '!', then the date is formatted in Coordinated Universal Time. After this optional character, if format is the string "*t", then date returns a table with the following fields: year, month (1–12), day (1–31), hour (0–23), min (0–59), sec (0–61, due to leap seconds), wday (weekday, 1–7, Sunday is 1), yday (day of the year, 1–366), and isdst (daylight saving flag, a boolean). This last field may be absent if the information is not available.
local time = os.date('*t')
local hour, min, sec = time.hour, time.min, time.sec
This does not provide you with a sub-second precision though.
Otherwise, parsing the time string is a typical task for tostring and string.match:
local hour, min, sec = gma.show.getvar('TIME'):match('^(%d+)h(%d+)m(%d*%.?%d*)s$')
-- This is usually not needed as Lua will just coerce strings to numbers
-- as soon as you start doing arithmetic on them;
-- it still is good practice to convert the variables to the proper type though
-- (and starts being relevant when you compare them, use them as table keys or call strict functions that check their argument types on them)
hour, min, sec = tonumber(hour), tonumber(min), tonumber(sec)
Pattern explanation:
^ and $ pattern anchors: Match the full string (and not just part of it), making the match fail if the string does not have the right format.
(%d)+h: Capture hours: One or more digits followed by a literal h
(%d)+m: Capture minutes: One or more digits followed by a literal m
(%d*%.?%d*)s: Capture seconds: Zero or more digits followed by an optional dot followed by again zero or more digits, finally ending with a literal s. I do not know the specifics of the format and whether something like .1s, 1.s or 1s is occasionally emitted, but Lua's tonumber supports all of these so there should be no issue. Note that this is slightly overly permissive: It will also match . (just a dot) and an s without any leading digits. You might want (%d+%.?%d+)s instead to force digits appearing before & after the dot.
Lets do it with string method gsub()
local ts = gma.show.getvar('TIME')
local hours = ts:gsub('h.*', '')
local mins = ts:gsub('.*%f[^h]', ''):gsub('%f[m].*', '')
local secs = ts:gsub('.*%f[^m]', ''):gsub('%f[s].*', '')
To make a Timestring i suggest string method format()
-- secs as float
timestring = ('[%s:%s:%.3f]'):format(hours, mins, secs)
-- secs not as float
timestring = ('[%s:%s:%.f]'):format(hours, mins, secs)

formatting function output similar os.date() in lua

I have a function that get current time and do some calculation that return a table. Something like this:
functuin newtime()
t1 = os.date("*t")
-- do some calculation that calculate this variable chha_ye , chha_mo , chha_da
t={}
t["ye"] = chha_ye
t["mo"] = chha_mo
t["da"] = chha_da
return t
end
Now I can get newtime().ye.
I need formatting output of this function something similar os.date()
for example, if chhaa_ye = 4 and chha_mo = 9 :
newtime("%ye")
4
newtime("%ye - %mo")
4 - 9
Default os.date do this, for example
os.date("%Y")
22
os.date("%Y - %m")
22 - 10
How should I do this?
I will provide an answer based on the comment from Egor, slightly modified to make the answer directly testable in a Lua interpreter. First, the os.date function from Lua is quite handy, it returns a hashtable with the corresponding field/values:
if format is the string "*t", then date returns a table with the following fields: year, month (1–12), day (1–31), hour (0–23), min (0–59), sec (0–61, due to leap seconds), wday (weekday, 1–7, Sunday is 1), yday (day of the year, 1–366), and isdst (daylight saving flag, a boolean). This last field may be absent if the information is not available.
We could test the function with the following code snippet:
for k,v in pairs(os.date("*t")) do
print(k,v)
end
This will print the following:
month 10
year 2022
yday 290
day 17
wday 2
isdst false
sec 48
min 34
hour 9
The gsub function, intended for string substitutions, is not limited to a single string, but could take a table as argument.
string.gsub (s, pattern, repl [, n])
If repl is a table, then the table is queried for every match, using the first capture as the key.
function my_stringformat (format)
local date_fields = os.date("*t")
local date_string = format:gsub("%%(%a+)", date_fields)
return date_string
end
The function can be used as most would expect:
> my_stringformat("%year - %month")
2022 - 10
Obviously, it's trivial to rename or add the fields names:
function my_stringformat_2 (format)
local date_fields = os.date("*t")
-- Rename the fields with shorter name
date_fields.ye = date_fields.year
date_fields.mo = date_fields.month
-- Delete previous field names
date_fields.year = nil
date_fields.month = nil
-- Interpolate the strings
local date_string = format:gsub("%%(%a+)", date_fields)
-- return the new string
return date_string
end
And the behavior is the same as the previous one:
> my_stringformat_2("%ye - %mo")
2022 - 10

Substitute character for integer

I am working on a project where I need to find the integer value substituted from a character which is either 'K' 'M' or 'B'.
I am trying to find the best way for a user to input a string such as "1k" "12k" "100k" and to receive the value back in the appropriate way. Such as a user entering "12k" and I would receive "12000".
I am new to Lua, and I am not that great at string patterns.
if (string.match(text, 'k')) then
print(text)
local test = string.match(text, '%d+')
print(test)
end
local text = "1k"
print(tonumber((text:lower():gsub("[kmb]", {k="e3"}))))
I don't know what factors you use for M and B. What is that supposed to be? Million and billion?
I suggest you use the international standard. kilo (k), mega (M), giga (G) instead.
Then it would look like so:
local text = "1k"
print(tonumber((text:gsub("[mkMG]", {m = "e-3", k="e3", M="e6", G="e9"})))) --and so on
You can match the pattern as something like (%d+)([kmb]) to get the number and the suffix separately. Then just tonumber the first part and map the latter to a factor (using a table, for example) and multiply it with your result.
local factors = { k=1e3, m=1e6, --[and so on]] }
local num, suffix = string.match(text, '(%d+)([kmb])')
local result = tonumber(num) * factors[suffix]

Datetime and iso format

sometimes toString(datetime()) return the milliseconds without the leading zeros to reach the length of 3 (yyyy-MM-dd'T'HH:mm:ss.SSSXXX). Is it a bug or normal behavior?
For example:
2019-11-21T15:59:22.53Z -> it should be 2019-11-21T15:59:22.053Z
2019-11-21T15:59:21.216Z -> OK
2019-11-21T15:30:09.042Z -> OK
This behavior causes an issue when I try to convert the string into a date.
Thank you
Try using the apoc.temporal.format function, specifying the iso_instant type conversion.
For example:
RETURN apoc.temporal.format(datetime("2019-11-21T22:04:19.13Z"), 'iso_instant');
will return:
"2019-11-21T22:04:19.130Z"
[UPDATE]
Since the TOSTRING() function is not documented to return any particular ISO 8601 string format for a datetime, one should not depend on it returning a specific format -- or even returning the same string for the same datetime across versions.
However, if you want a non-APOC approach that works with recent versions of neo4j (like, 3.5.12, on which this was tested), here is an example of one way to modify the current TOSTRING() output string to always have a 3-digit millisecond value:
// Generate a string.
// You can play with the number of digits after ".", and
// even eliminate the "." and any following digits.
WITH TOSTRING(datetime("2019-11-21T22:04:10.1Z")) AS d
// Always return a 3-digit millisecond time in result
WITH d, LENGTH(d) AS lth
RETURN d, CASE WHEN lth < 24
THEN SUBSTRING(d, 0, lth-1) + SUBSTRING('.000Z', lth - 20)
ELSE d END AS result

OLE Automation date in lua

Ok, I really need OLE Automation date in lua.
From here:
public double ToOADate()
Return Value Type: System.Double A double-precision floating-point
number that contains an OLE Automation date equivalent to the value of
this instance.
So in C# this:
Console.Write("DateTime.Now.ToOADate() = " + DateTime.Now.ToOADate());
gives me this:
DateTime.Now.ToOADate() = 42146,4748270602
What is the best way to get simular value in Lua?
Some more details, based on EgorSkriptunoff answer.
So, that Lua code works just fine for me to get OLE Automation date in lua:
-- number of days between December, 30 1899 and January, 1 1970
local magicnumber = 25569
-- don't forget about time zone (UTC+3 for my case)
local utcshift = 3*3600
-- calc and print for test
local oleadate = magicnumber + ((os.time()+utcshift)/(3600*24))
print(oleadate)
Output:
42146.575740741

Resources