-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement unix time and rfc3339 parsing #23
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,4 +55,131 @@ local d = import 'doc-util/main.libsonnet'; | |
if month > 2 && self.isLeapYear(year) | ||
then 1 | ||
else 0, | ||
|
||
// yearSeconds returns the number of seconds in the given year. | ||
local yearSeconds(year) = ( | ||
if $.isLeapYear(year) | ||
then 366 * 24 * 3600 | ||
else 365 * 24 * 3600 | ||
), | ||
|
||
// monthSeconds returns the number of seconds in the given month of a given year. | ||
local monthSeconds(year, month) = ( | ||
commonYearMonthLength[month - 1] * 24 * 3600 | ||
+ if month == 2 && $.isLeapYear(year) then 86400 else 0 | ||
), | ||
|
||
// sumYearsSeconds returns the number of seconds in all years since 1970 up to year-1. | ||
local sumYearsSeconds(year) = std.foldl( | ||
function(acc, y) acc + yearSeconds(y), | ||
std.range(1970, year - 1), | ||
0, | ||
), | ||
|
||
// sumMonthsSeconds returns the number of seconds in all months up to month-1 of the given year. | ||
local sumMonthsSeconds(year, month) = std.foldl( | ||
function(acc, m) acc + monthSeconds(year, m), | ||
std.range(1, month - 1), | ||
0, | ||
), | ||
|
||
// sumDaysSeconds returns the number of seconds in all days up to day-1. | ||
local sumDaysSeconds(day) = (day - 1) * 24 * 3600, | ||
|
||
'#toUnixTimestamp': d.fn( | ||
||| | ||
`toUnixTimestamp` calculates the unix timestamp of a given date. | ||
|||, | ||
[ | ||
d.arg('year', d.T.number), | ||
d.arg('month', d.T.number), | ||
d.arg('day', d.T.number), | ||
d.arg('hour', d.T.number), | ||
d.arg('minute', d.T.number), | ||
d.arg('second', d.T.number), | ||
], | ||
), | ||
toUnixTimestamp(year, month, day, hour, minute, second):: | ||
sumYearsSeconds(year) + sumMonthsSeconds(year, month) + sumDaysSeconds(day) + hour * 3600 + minute * 60 + second, | ||
|
||
// isNumeric checks that the input is a non-empty string containing only digit characters. | ||
local isNumeric(input) = | ||
assert std.type(input) == 'string' : 'isNumeric() only operates on string inputs, got %s' % std.type(input); | ||
std.foldl( | ||
function(acc, char) acc && std.codepoint('0') <= std.codepoint(char) && std.codepoint(char) <= std.codepoint('9'), | ||
std.stringChars(input), | ||
std.length(input) > 0, | ||
), | ||
Comment on lines
+105
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jdbaldry I borrowed the implementation from your comment here :) |
||
|
||
// parseSeparatedNumbers parses input which has part `names` separated by `sep`. | ||
// Returns an object which has one field for each name in `names` with its integer value. | ||
local parseSeparatedNumbers(input, sep, names) = ( | ||
assert std.type(input) == 'string' : 'parseSeparatedNumbers() only operates on string inputs, got %s' % std.type(input); | ||
assert std.type(sep) == 'string' : 'parseSeparatedNumbers() only operates on string separators, got %s' % std.type(sep); | ||
assert std.type(names) == 'array' : 'parseSeparatedNumbers() only operates on arrays of names, got input %s' % std.type(names); | ||
|
||
local parts = std.split(input, sep); | ||
assert std.length(parts) == std.length(names) : 'expected %(expected)d parts separated by %(sep)s in %(format)s formatted input "%(input)s", but got %(got)d' % { | ||
expected: std.length(names), | ||
sep: sep, | ||
format: std.join(sep, names), | ||
input: input, | ||
got: std.length(parts), | ||
}; | ||
|
||
{ | ||
[names[i]]: | ||
// Fail with meaningful message if not numeric, otherwise it will be a hell to debug. | ||
assert isNumeric(parts[i]) : '%(name)%s part "%(part)s" of %(format)s of input "%(input)s" is not numeric' % { | ||
name: names[i], | ||
part: parts[i], | ||
format: std.join(sep, names), | ||
input: input, | ||
}; | ||
std.parseInt(parts[i]) | ||
for i in std.range(0, std.length(parts) - 1) | ||
} | ||
), | ||
|
||
// stringContains is a helper function to check whether a string contains a given substring. | ||
local stringContains(haystack, needle) = std.length(std.findSubstr(needle, haystack)) > 0, | ||
|
||
'#parseRFC3339': d.fn( | ||
||| | ||
`parseRFC3339` parses an RFC3339-formatted date & time string into an object containing the 'year', 'month', 'day', 'hour', 'minute' and 'second fields. | ||
This is a limited implementation that does not support timezones (so it requires an UTC input ending in 'Z' or 'z') nor sub-second precision. | ||
The returned object has a `toUnixTimestamp()` method that can be used to obtain the unix timestamp of the parsed date. | ||
|||, | ||
[ | ||
d.arg('input', d.T.string), | ||
], | ||
), | ||
parseRFC3339(input):: | ||
// Basic input type check. | ||
assert std.type(input) == 'string' : 'parseRFC3339() only operates on string inputs, got %s' % std.type(input); | ||
|
||
// Sub-second precision isn't implemented yet, warn the user about that instead of returning wrong results. | ||
assert !stringContains(input, '.') : 'the provided RFC3339 input "%s" has a dot, most likely representing a sub-second precision, but this function does not support that' % input; | ||
|
||
// We don't support timezones, so string should end with 'Z' or 'z'. | ||
assert std.endsWith(input, 'Z') || std.endsWith(input, 'z') : 'the provided RFC3339 "%s" should end with "Z" or "z". This implementation does not currently support timezones' % input; | ||
|
||
// RFC3339 can separate date and time using 'T', 't' or ' '. | ||
// Find out which one it is and use it. | ||
local sep = | ||
if stringContains(input, 'T') then 'T' | ||
else if stringContains(input, 't') then 't' | ||
else if stringContains(input, ' ') then ' ' | ||
else error 'the provided RFC3339 input "%s" should contain either a "T", or a "t" or space " " as a separator for date and time parts' % input; | ||
Comment on lines
+161
to
+173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Duologic as you asked in grafana/jsonnet-libs#1095, I added support for lowercase t/z and also for space separator. |
||
|
||
// Split date and time using the selected separator. | ||
// Remove the last character as we know it's 'Z' or 'z' and it's not useful to us. | ||
local datetime = std.split(std.substr(input, 0, std.length(input) - 1), sep); | ||
assert std.length(datetime) == 2 : 'the provided RFC3339 timestamp "%(input)s" does not have date and time parts separated by the character "%(sep)s"' % { input: input, sep: sep }; | ||
|
||
local date = parseSeparatedNumbers(datetime[0], '-', ['year', 'month', 'day']); | ||
local time = parseSeparatedNumbers(datetime[1], ':', ['hour', 'minute', 'second']); | ||
date + time + { | ||
toUnixTimestamp():: $.toUnixTimestamp(self.year, self.month, self.day, self.hour, self.minute, self.second), | ||
}, | ||
Comment on lines
+182
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't the most functional approach, but it's easy to use. I'm happy to listen for your suggestions. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,8 @@ local date = import "github.com/jsonnet-libs/xtd/date.libsonnet" | |
* [`fn dayOfWeek(year, month, day)`](#fn-dayofweek) | ||
* [`fn dayOfYear(year, month, day)`](#fn-dayofyear) | ||
* [`fn isLeapYear(year)`](#fn-isleapyear) | ||
* [`fn parseRFC3339(input)`](#fn-parserfc3339) | ||
* [`fn toUnixTimestamp(year, month, day, hour, minute, second)`](#fn-tounixtimestamp) | ||
|
||
## Fields | ||
|
||
|
@@ -42,4 +44,23 @@ for common years, and 1-366 for leap years. | |
isLeapYear(year) | ||
``` | ||
|
||
`isLeapYear` returns true if the given year is a leap year. | ||
`isLeapYear` returns true if the given year is a leap year. | ||
|
||
### fn parseRFC3339 | ||
|
||
```ts | ||
parseRFC3339(input) | ||
``` | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be useful to have an example of an RFC3339 date/time for those of us that know the format but not the RFC number. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, followed up in #24 |
||
`parseRFC3339` parses an RFC3339-formatted date & time string into an object containing the 'year', 'month', 'day', 'hour', 'minute' and 'second fields. | ||
This is a limited implementation that does not support timezones (so it requires an UTC input ending in 'Z' or 'z') nor sub-second precision. | ||
The returned object has a `toUnixTimestamp()` method that can be used to obtain the unix timestamp of the parsed date. | ||
|
||
|
||
### fn toUnixTimestamp | ||
|
||
```ts | ||
toUnixTimestamp(year, month, day, hour, minute, second) | ||
``` | ||
|
||
`toUnixTimestamp` calculates the unix timestamp of a given date. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was already in github.com/grafana/jsonnet-libs/date, now I'm bringing here as it's useful once we've parsed the RFC3339.