One of the things that often comes up is how to figure out how much elapsed (or remaining) time there is from now to a date and time string you get from somewhere, perhaps an RSS feed.
So you might get a string like 2016-06-18T16:04:17Z, and want to know how many weeks, days, hours, minutes and seconds old (or in the future) that date and time is.
This guide assumes that the date and time string will be returned in an ISO-8601 standard date and time format. This are generally going to be one of two things:
2016-06-18T16:04:17Z
The date as yyyy-mm-dd followed by a "T" for "time" and then hh:mm:ss followed by a "Z" to indicate "Zulu" or UTC time.
or:
2016-06-18T12:04:17-04:00
The date as yyyy-mm-dd followed by a "T" for "time" and then hh:mm:ss followed by a positive or negative hh:mm offset from UTC that the string is returning. This will be when the resource you are getting the date from returns some local server time, perhaps on a site like our forums where you set your personal time zone information and the RSS feed returns things in your local time.
Both of these are reasonably common, although you will find that the vast majority of sites will return date strings in UTC.
First let's have a completed .rmskin that you can use to follow along, and can edit as you need for your own uses:
That might display something like: The skin:
Code: Select all
[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1
[Metadata]
Name=DateTimeLua
Author=JSMorley
Version=Jun 18, 2016
License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0
Information=Demonstrates parsing ISO standard date / time strings into elapsed or remaining Weeks/Days/Hours/Minutes/Seconds.
[TimeMeasure]
Measure=String
;String=2016-06-18T12:04:17-04:00
String=2016-06-18T16:04:17Z
[MeasureScript]
Measure=Script
ScriptFile=DateTimeLua.lua
[MeterNowLocal]
Meter=String
FontSize=12
FontColor=255,255,255,255
SolidColor=0,0,0,1
AntiAlias=1
[MeterNowUTC]
Meter=String
Y=5R
FontSize=12
FontColor=255,255,255,255
SolidColor=0,0,0,1
AntiAlias=1
[MeterDate1Original]
Meter=String
Y=10R
FontSize=12
FontColor=255,255,255,255
SolidColor=0,0,0,1
AntiAlias=1
[MeterDate1Formatted]
Meter=String
Y=5R
FontSize=12
FontColor=255,255,255,255
SolidColor=0,0,0,1
AntiAlias=1
[MeterDiffFormatted]
Meter=String
Y=10R
FontSize=12
FontColor=255,255,255,255
SolidColor=0,0,0,1
AntiAlias=1
I set up some meters to hold our information, but I don't have any MeasureName or Text values for those, we are going to let the Lua set them in a bit...
The Lua script
Code: Select all
function Initialize()
standardFormat = '%A, %B %d %Y at %H:%M:%S'
timeMeasure = SKIN:GetMeasure('TimeMeasure')
end
function Update()
local itemDate = timeMeasure:GetStringValue()
if itemDate == '' then return -1 end
local timeLocalNow = os.time(os.date('*t'))
local timeUTCNow = os.time(os.date('!*t'))
if os.date(isdst) then
timeUTCAdjusted = timeUTCNow - 3600
end
local itemTimeStamp = TimeStamp(itemDate)
local diffTotal = timeUTCNow - itemTimeStamp
SKIN:Bang('!SetOption', 'MeterNowLocal', 'Text', 'Now Local: '..os.date(standardFormat, timeLocalNow))
SKIN:Bang('!SetOption', 'MeterNowUTC', 'Text', 'Now UTC: '..os.date(standardFormat, timeUTCAdjusted))
SKIN:Bang('!SetOption', 'MeterDate1Original', 'Text', 'Input String: '..itemDate)
SKIN:Bang('!SetOption', 'MeterDate1Formatted', 'Text', 'Formatted: '..os.date(standardFormat, itemTimeStamp))
if diffTotal >= 0 then
textPrefix = 'Elapsed:'
else
textPrefix = 'Remaining:'
end
diffWeeks, diffDays, diffHours, diffMinutes, diffSeconds = FormatSeconds(math.abs(diffTotal))
if diffWeeks == 1 then
outputWeeks = diffWeeks..' Week'
else
outputWeeks = diffWeeks..' Weeks'
end
if diffDays == 1 then
outputDays = diffDays..' Day'
else
outputDays = diffDays..' Days'
end
if diffHours == 1 then
outputHours = diffHours..' Hour'
else
outputHours = diffHours..' Hours'
end
if diffMinutes == 1 then
outputMinutes = diffMinutes..' Minute'
else
outputMinutes = diffMinutes..' Minutes'
end
if diffSeconds == 1 then
outputSeconds = diffSeconds..' Second'
else
outputSeconds = diffSeconds..' Seconds'
end
if diffWeeks > 0 then
outputString = textPrefix..' '..outputWeeks..' '..outputDays..' '..outputHours..' '..outputMinutes..' '..outputSeconds
elseif diffDays > 0 then
outputString = textPrefix..' '..outputDays..' '..outputHours..' '..outputMinutes..' '..outputSeconds
elseif diffHours > 0 then
outputString = textPrefix..' '..outputHours..' '..outputMinutes..' '..outputSeconds
elseif diffMinutes > 0 then
outputString = textPrefix..' '..outputMinutes..' '..outputSeconds
else
outputString = textPrefix..' '..outputSeconds
end
SKIN:Bang('!SetOption', 'MeterDiffFormatted', 'Text', outputString)
return diffTotal
end
function TimeStamp(dateStringArg)
local inYear, inMonth, inDay, inHour, inMinute, inSecond, inZone =
string.match(dateStringArg, '^(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)(.-)$')
local zHours, zMinutes = string.match(inZone, '^(.-):(%d%d)$')
local returnTime = os.time({year=inYear, month=inMonth, day=inDay, hour=inHour, min=inMinute, sec=inSecond, isdst=false})
if zHours then
returnTime = returnTime - ((tonumber(zHours)*3600) + (tonumber(zMinutes)*60))
end
return returnTime
end
function FormatSeconds(secondsArg)
local weeks = math.floor(secondsArg / 604800)
local remainder = secondsArg % 604800
local days = math.floor(remainder / 86400)
local remainder = remainder % 86400
local hours = math.floor(remainder / 3600)
local remainder = remainder % 3600
local minutes = math.floor(remainder / 60)
local seconds = remainder % 60
return weeks, days, hours, minutes, seconds
end
There are two main time and date functions in Lua, which compliment each other. These are os.time() and os.date(), which are described in some detail here:
https://www.lua.org/pil/22.1.html
os.time returns a timestamp number. By default, with no arguments, it returns the current system date and time as a number of seconds from a standard epoch of 00:00:00 UTC on 1 January 1970.
os.time will also accept a table as an argument, with the following possible fields:
year
month
day
hour
min
sec
isdst
Not all fields are required. So for example:
newYear = os.time{year=2017, month=1, day=1}
Would return a number that is the number of seconds from January 1, 1970 at midnight to January 1, 2017 at midnight. This is how you ask Lua to give you a timestamp number for some specific date and time.
os.date is really just the reverse of os.time. It takes arguments of a format string and a timestamp number, and can return one of two things:
If you specify a format string of '*t' (splat-t) as in nowTable = os.date('*t') it will return a table much the same as the one the os.time function supports above. By default the second argument, the timestamp number, will be the current system date and time.
Note that if you specify '!*t' (bang-splat-t) you will get the current standard time in UTC. If you get the UTC time, daylight savings time is not considered. More on that in a bit.
If you specify any other format string, it will produce a formatted string from either the current time, or a timestamp you give it as the second argument. It will use much the same (although not exactly the same) format codes that the Time measure in Rainmeter uses. See the https://www.lua.org/pil/22.1.html link above for the supported codes.
So os.time and os.date really do compliment each other. In many cases, you will use a table returned by os.date as input to os.time, and in others use a timestamp returned by os.time as input to os.date. Hopefully this will be come clear as we get into our Lua code.
What our Lua code does at a high level
1) Get the current system date and time as a formatted string.
2) Get the current UTC date and time as a formatted string.
3) Get an ISO-8601 formatted date and time string from the skin.
4) Convert the components of that formatted string into table fields that we will use to get a timestamp from os.time.
5) Compare that timestamp to the current time, to determine how many seconds difference there is.
6) Convert that number of seconds difference into numbers of weeks/days/hours/minutes/seconds.
7) Send all this information back to the skin for display in meters. Again, the goal is something like this:
Walking through the Lua code
The Initialize() function will do two things. It will set up a variable to hold a standard format string that we will use with os.date to pretty up the current date and time, and it will get a "handle" to the measure in our skin that will be giving us the ISO-8601 date and time string we want to parse.
The Update() function will do the following:
Get the current value of the measure from the skin
local itemDate = timeMeasure:GetStringValue()
Get the current local system time
local timeLocalNow = os.time(os.date('*t'))
Get the current UTC time
local timeUTCNow = os.time(os.date('!*t'))
The next bit takes just a tad of explaining. Remember that when we get UTC time with '!*t' Daylight Saving Time is not considered. However, if our local time zone is in fact currently in Daylight Saving Time, we do want to consider that when we format and display the current UTC time.
Code: Select all
if os.date(isdst) then
timeUTCAdjusted = timeUTCNow - 3600
end
Now we are passing our ISO-8601 date and time string from the skin to a function called TimeStamp().
local itemTimeStamp = TimeStamp(itemDate)
What we want back from TimeStamp(itemDate) is a timestamp, the number of seconds from from January 1, 1970 at midnight to the date and time we pass it as a formatted string.
We are going to parse that string, and then use os.time to get that timestamp number for us. Let's walk through the function:
Code: Select all
function TimeStamp(dateStringArg)
local inYear, inMonth, inDay, inHour, inMinute, inSecond, inZone =
string.match(dateStringArg, '^(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)(.-)$')
local zHours, zMinutes = string.match(inZone, '^(.-):(%d%d)$')
local returnTime = os.time({year=inYear, month=inMonth, day=inDay, hour=inHour, min=inMinute, sec=inSecond, isdst=false})
if zHours then
returnTime = returnTime - ((tonumber(zHours)*3600) + (tonumber(zMinutes)*60))
end
return returnTime
end
Now, remember that our ISO-8601 formatted string can take one of two forms:
2016-06-18T16:04:17Z
Where the "Z" just means "Zulu" or UTC time
2016-06-18T12:04:17-04:00
Where the "-04:00" is an offset in hours and minutes from UTC that is being provided.
So we are parsing inZone for that "-04:00" mask, and if zHours exists, if the match succeeded, we know we need to account for that time zone offset. We want to "end up" with UTC time, never any local time.
Then we return that number created by os.time (and adjusted for time zone if needed) back to the main part of the Lua.
So now we have a timestamp that reflects the UTC time in our ISO-8601 string from the skin, and we have a timestamp that reflects the current time in UTC, the difference in seconds is simple subtraction.
local diffTotal = timeUTCNow - itemTimeStamp
Note that if the date and time string from the skin is in the "past" the value of diffTotal will be positive, or elapsed time from then. If it is in the future, diffTotal will be negative, or remaining time from now.
We then use SKIN:Bang() to set the meter values for the information we already have. All that's left is to turn diffTotal (the seconds of difference) into the number of Weeks/Days/Hours/Minutes/Seconds.
That is done with a call to the function FormatSeconds(). This is a pretty straightforward function that uses modulo math to set and return our desired values. It's pretty standard, and will always work, no matter how you get some number of seconds to pass it. The function can always be copied from https://docs.rainmeter.net/snippets/format-time/ when you need it.
Code: Select all
function FormatSeconds(secondsArg)
local weeks = math.floor(secondsArg / 604800)
local remainder = secondsArg % 604800
local days = math.floor(remainder / 86400)
local remainder = remainder % 86400
local hours = math.floor(remainder / 3600)
local remainder = remainder % 3600
local minutes = math.floor(remainder / 60)
local seconds = remainder % 60
return weeks, days, hours, minutes, seconds
end
Also note that it is not possible to accurately turn a number of seconds into anything higher than "Weeks". Months, Quarters, Years, Decades, Centuries etc. don't have a fixed number of seconds.
So we have all the values we need. The rest of the Lua is just to pretty things up, use the singular for the components of time if the value is "1", and use SKIN:Bang() to set the meter that will display the Weeks/Days/Hours/Minutes/Seconds of elapsed or remaining time.
Hope this gives you some idea of how you might use Lua with date and time strings. It's a powerful part of the Lua language, and there is a lot you can do with it other than just this example. Feel free to ask any questions you might have.