DSL datetime/timezone functions¶
Dates/times are not a separate data type; Miller uses ints for seconds since the epoch and strings for formatted date/times. In this page we take a look at what some of the various options are for processing datetimes and timezones in your data.
See also the section on time-related functions for information auto-generated from Miller's online-help strings.
Epoch seconds¶
Seconds since the epoch, or Unix Time, is seconds (positive, zero, or negative) since midnight January 1 1970 UTC. This representation has several advantages, and is quite common in the computing world.
Since this is a number in Miller -- 64-bit signed integer or double-precision floating-point -- it can represent dates billions of years into the past or future without worry of overflow. (There is no year-2038 problem here.) Being numbers, epoch-seconds are easy to store in databases, communicate over networks in binary format, etc. Another benefit of epoch-seconds is that they're independent of timezone or daylight-savings time.
One minus is that, being just numbers, they're not particularly human-readable -- hence the to-string and from-string functions described below. Another caveat (not really a minus) is that epoch milliseconds, rather than epoch seconds, are common in some contexts, particularly JavaScript. If you ever (anywhere) see a timestamp for the year 49,000-something -- probably someone is treating epoch-milliseconds as epoch-seconds.
mlr -n put 'end {
print sec2gmt(1500000000);
print sec2gmt(1500000000000);
}'
2017-07-14T02:40:00Z 49503-02-10T02:40:00Z
You can get the current system time, as epoch-seconds, using the systime DSL function:
mlr --c2p --from example.csv put '$t = systime()'
color shape flag k index quantity rate t yellow triangle true 1 11 43.6498 9.8870 1634784588.045347 red square true 2 15 79.2778 0.0130 1634784588.045385 red circle true 3 16 13.8103 2.9010 1634784588.045386 red square false 4 48 77.5542 7.4670 1634784588.045393 purple triangle false 5 51 81.2290 8.5910 1634784588.045394 red square false 6 64 77.1991 9.5310 1634784588.045417 purple triangle false 7 65 80.1405 5.8240 1634784588.045418 yellow circle true 8 73 63.9785 4.2370 1634784588.045419 yellow circle true 9 87 63.5058 8.3350 1634784588.045421 purple square false 10 91 72.3735 8.2430 1634784588.045422
The systimeint DSL function
is nothing more than a keystroke-saver for int(systime()).
UTC times with standard format¶
One way to make epoch-seconds human-readable, while maintaining some of their benefits such as being independent of timezone and daylight savings, is to use the ISO8601 format. This was the first (and initially only) human-readable date/time format supported by Miller going all the way back to Miller 1.0.0.
You can get these from epoch-seconds using the sec2gmt DSL function. (Note that the terms UTC and GMT are used interchangeably in Miller.) We also have sec2gmtdate DSL function.
mlr -n put 'end {
print sec2gmt(0);
print sec2gmt(1234567890.123);
print sec2gmt(-1234567890.123);
print;
print sec2gmtdate(0);
print sec2gmtdate(1234567890.123);
print sec2gmtdate(-1234567890.123);
}'
1970-01-01T00:00:00Z 2009-02-13T23:31:30Z 1930-11-18T00:28:29Z 1970-01-01 2009-02-13 1930-11-18
Local times with standard format; specifying timezones¶
You can use similar formatting for dates in your preferred timezone, not just UTC/GMT. We have the sec2localtime, sec2localdate, and localtime2sec DSL functions.
You can specify the timezone using any of the following:
- An environment variable, e.g.
export TZ=Asia/Istanbulat your system prompt (set TZ=Asia/Istanbulin Windows). - Using the
--tzflag. This sets theTZenvironment variable, but only internally to themlrprocess. - Within a DSL expression, you can assign to
ENV["TZ"]. - By supplying an additional argument to any of the functions with
localin their names.
Regardless, if you specify an invalid timezone, you'll be clearly notified:
mlr --from example.csv --tz This/Is/A/Typo cat
mlr: TZ environment variable appears malformed: "This/Is/A/Typo"
export TZ=Asia/Istanbul
mlr -n put 'end { print sec2localtime(0) }'
1970-01-01 02:00:00
mlr --tz America/Sao_Paulo -n put 'end { print sec2localtime(0) }'
1969-12-31 21:00:00
mlr -n put 'end {
ENV["TZ"] = "Asia/Istanbul";
print sec2localtime(0);
print sec2localdate(0);
print localtime2sec("2000-01-02 03:04:05");
print;
ENV["TZ"] = "America/Sao_Paulo";
print sec2localtime(0);
print sec2localdate(0);
print localtime2sec("2000-01-02 03:04:05");
}'
1970-01-01 02:00:00 1970-01-01 946775045 1969-12-31 21:00:00 1969-12-31 946789445
mlr -n put 'end {
print sec2localtime(0, 0, "Asia/Istanbul");
print sec2localdate(0, "Asia/Istanbul");
print localtime2sec("2000-01-02 03:04:05", "Asia/Istanbul");
print;
print sec2localtime(0, 0, "America/Sao_Paulo");
print sec2localdate(0, "America/Sao_Paulo");
print localtime2sec("2000-01-02 03:04:05", "America/Sao_Paulo");
}'
1970-01-01 02:00:00 1970-01-01 946775045 1969-12-31 21:00:00 1969-12-31 946789445
Note that for local times, Miller omits the T and the Z you see in GMT times.
We also have the gmt2localtime and localtime2gmt convenience functions:
mlr -n put 'end {
ENV["TZ"] = "Asia/Istanbul";
print gmt2localtime("1970-01-01T00:00:00Z");
print localtime2gmt("1970-01-01 00:00:00");
}'
1970-01-01 02:00:00 1969-12-31T22:00:00Z
mlr -n put 'end {
print gmt2localtime("1970-01-01T00:00:00Z", "America/Sao_Paulo");
print gmt2localtime("1970-01-01T00:00:00Z", "Asia/Istanbul");
print localtime2gmt("1970-01-01 00:00:00", "America/Sao_Paulo");
print localtime2gmt("1970-01-01 00:00:00", "Asia/Istanbul");
}'
1969-12-31 21:00:00 1970-01-01 02:00:00 1970-01-01T03:00:00Z 1969-12-31T22:00:00Z
Custom formats: strptime and strftime¶
The to-string and from-string functions we've seen so far are low-keystroking: with a little bit of typing you can convert datetimes to/from epoch seconds. The minus, however, is flexibility. This is where the strftime and strptime functions come into play.
Notes:
- The names
strftimeandstrptimefar predate Miller; they were chosen for familiarity. Thefis for format: from epoch-seconds to human-readable string. Thepis for parse: for doing the reverse. - Even though Miller is written in Go as of Miller 6, it still largely preserves C-like
strftimeandstrptimesemantics. As noted below, not all format strings used by the C library are recognized. - For
strftime, this is thanks to https://github.com/lestrrat-go/strftime, with a Miller-specific modification for fractional seconds. - For
strftime, this is thanks to https://github.com/pbnjay/strptime, with Miller-specific modifications.
Available format strings for strftime, taken directly from https://github.com/lestrrat-go/strftime except for %1..%9, %s, %N, and %O which are Miller-specific additions:
| Pattern | Description |
|---|---|
%A |
national representation of the full weekday name |
%a |
national representation of the abbreviated weekday |
%B |
national representation of the full month name |
%b |
national representation of the abbreviated month name |
%C |
(year / 100) as decimal number; single digits are preceded by a zero |
%c |
national representation of time and date |
%D |
equivalent to %m/%d/%y |
%d |
day of the month as a decimal number (01-31) |
%e |
the day of the month as a decimal number (1-31); single digits are preceded by a blank |
%F |
equivalent to %Y-%m-%d |
%H |
the hour (24-hour clock) as a decimal number (00-23) |
%h |
same as %b |
%I |
the hour (12-hour clock) as a decimal number (01-12) |
%j |
the day of the year as a decimal number (001-366) |
%k |
the hour (24-hour clock) as a decimal number (0-23); single digits are preceded by a blank |
%l |
the hour (12-hour clock) as a decimal number (1-12); single digits are preceded by a blank |
%M |
the minute as a decimal number (00-59) |
%m |
the month as a decimal number (01-12) |
%n |
a newline |
%N |
zero-padded nanoseconds |
%O |
non-zero-padded nanoseconds |
%p |
national representation of either "ante meridiem" (a.m.) or "post meridiem" (p.m.) as appropriate. |
%R |
equivalent to %H:%M |
%r |
equivalent to %I:%M:%S %p |
%s |
integer seconds since the epoch |
%S |
the second as a decimal number (00-60) |
%1S, ..., %9S |
the second as a decimal number (00-60) with 1..9 decimal places, respectively |
%T |
equivalent to %H:%M:%S |
%t |
a tab |
%U |
the week number of the year (Sunday as the first day of the week) as a decimal number (00-53) |
%u |
the weekday (Monday as the first day of the week) as a decimal number (1-7) |
%V |
the week number of the year (Monday as the first day of the week) as a decimal number (01-53) |
%v |
equivalent to %e-%b-%Y |
%W |
the week number of the year (Monday as the first day of the week) as a decimal number (00-53) |
%w |
the weekday (Sunday as the first day of the week) as a decimal number (0-6) |
%X |
national representation of the time |
%x |
national representation of the date |
%Y |
the year with century as a decimal number |
%y |
the year without century as a decimal number (00-99) |
%Z |
the time zone name |
%z |
the time zone offset from UTC |
%% |
a % |
Available format strings for strptime:
| Pattern | Description |
|---|---|
%% |
A literal '%' character. |
%a |
Weekday as locale’s abbreviated name. |
%A |
Weekday as locale’s full name. |
%b |
Month as locale’s abbreviated name. |
%B |
Month as locale’s full name. |
%c |
Locale’s date and time representation. Equivalent to %a %b %e %H:%M:%S %Y. |
%d |
Day of the month as a zero-padded decimal number. |
%D |
Equivalent to %m/%d/%y. |
%e |
Day of the month as a space-padded decimal number. |
%f |
Microsecond as a decimal number, zero-padded on the left. |
%F |
Equivalent to %Y-%m-%d. |
%h |
Same as %b. |
%H |
Hour (24-hour clock) as a zero-padded decimal number. |
%I |
Hour (12-hour clock) as a zero-padded decimal number. |
%j |
Three-digit day of year, like 004 or 363. |
%m |
Month as a zero-padded decimal number. |
%M |
Minute as a zero-padded decimal number. |
%p |
Locale’s equivalent of either AM or PM. |
%r |
Equivalent to %I:%M:%S %p. |
%R |
Equivalent to %H:%M. |
%S |
Second as a zero-padded decimal number. |
%T |
Equivalent to %H:%M:%S. |
%x |
Locale’s date representation. Equivalent to %m/%d/%y. |
%X |
Locale’s time representation. Equivalent to %H:%M:%S. |
%y |
Year without century as a zero-padded decimal number. |
%Y |
Year with century as a decimal number. |
%z |
UTC offset in the form +HHMM or -HHMM. |
%Z |
Time zone name. UTC, EST, CST -- only if you're in that timezone. |
Examples:
mlr -n put 'end {
print strftime(0, "%Y-%m-%dT%H:%M:%SZ");
print strftime(0, "%FT%TZ");
print strfntime(123, "%N");
print strfntime(123, "%O");
}'
1970-01-01T00:00:00Z 1970-01-01T00:00:00Z 000000123 123
mlr -n put 'end {
ENV["TZ"] = "Asia/Istanbul";
print strftime(0, "%Y-%m-%d %H:%M:%S");
print strftime(0, "%Y-%m-%d %H:%M:%S %Z");
print strftime(0, "%Y-%m-%d %H:%M:%S %z");
print strftime(0, "%A, %B %e, %Y");
print strftime(123456789, "%I:%M %p");
}'
1970-01-01 00:00:00 1970-01-01 00:00:00 UTC 1970-01-01 00:00:00 +0000 Thursday, January 1, 1970 09:33 PM
Unfortunately, names from %A and %B are only available in English, as an artifact of a design
choice in the Go time library which Miller (and its strftime / strptime supporting packages as
noted above) rely on.
A note on timezones¶
A note on timezones for strptime:
- Three-letter timezone names such as
CSTare recognized only if you're in them. (UTCis an exception.) This is because these aren't globally unique:CSTcan stand forCentral Standard Time,_Cuba Standard Time,_China Standard Time, etc. - Timezone specifiers which are globally unique are of the form
-0400and+0500. - Specifiers like
-04:30,UTC-8, andAsia/Istanbulwere not supported in Miller 5 (which used the Cstrptimelibrary), and are likewise not supported in Miller 6. See however theTZenvironment-variable examples below. - If you wish to match a final
Zin the input, use a finalZin the format string. For example (see ISO8601) you can match the timestamp1970-01-01T00:00:00Zusing the format string%FT%TZ.
Fractional seconds¶
For historical reasons, Miller's strftime and strptime use different format specifications for fractional seconds. Examples:
mlr -n put 'end {
print strftime(123456.789, "%Y-%m-%d %H:%M:%S");
print strftime(123456.789, "%Y-%m-%d %H:%M:%1S");
print strftime(123456.789, "%Y-%m-%d %H:%M:%3S");
print strftime(123456.789, "%Y-%m-%d %H:%M:%6S");
print strptime("1970-01-02 10:17:36.789000", "%Y-%m-%d %H:%M:%S");
print strptime("1970-01-02 10:17:36.789000", "%Y-%m-%d %H:%M:%S.%f");
}'
1970-01-02 10:17:36 1970-01-02 10:17:36.7 1970-01-02 10:17:36.789 1970-01-02 10:17:36.789000 (error) 123456.789
strptime_local and strftime_local¶
We also have strftimelocal and strptimelocal:
mlr -n put 'end {
ENV["TZ"] = "America/Anchorage";
print strftime_local(0, "%Y-%m-%d %H:%M:%S %Z");
print strftime_local(0, "%Y-%m-%d %H:%M:%S %z");
print strftime_local(0, "%A, %B %e, %Y");
print strptime_local("2020-03-01 00:00:00", "%Y-%m-%d %H:%M:%S");
print;
ENV["TZ"] = "Asia/Hong_Kong";
print strftime_local(0, "%Y-%m-%d %H:%M:%S %Z");
print strftime_local(0, "%Y-%m-%d %H:%M:%S %z");
print strftime_local(0, "%A, %B %e, %Y");
print strptime_local("2020-03-01 00:00:00", "%Y-%m-%d %H:%M:%S");
}'
1969-12-31 14:00:00 AHST 1969-12-31 14:00:00 -1000 Wednesday, December 31, 1969 1583053200 1970-01-01 08:00:00 HKT 1970-01-01 08:00:00 +0800 Thursday, January 1, 1970 1582992000
mlr -n put 'end {
print strftime_local(0, "%Y-%m-%d %H:%M:%S %Z", "America/Anchorage");
print strftime_local(0, "%Y-%m-%d %H:%M:%S %z", "America/Anchorage");
print strftime_local(0, "%A, %B %e, %Y", "America/Anchorage");
print strptime_local("2020-03-01 00:00:00", "%Y-%m-%d %H:%M:%S", "America/Anchorage");
print;
print strftime_local(0, "%Y-%m-%d %H:%M:%S %Z", "Asia/Hong_Kong");
print strftime_local(0, "%Y-%m-%d %H:%M:%S %z", "Asia/Hong_Kong");
print strftime_local(0, "%A, %B %e, %Y", "Asia/Hong_Kong");
print strptime_local("2020-03-01 00:00:00", "%Y-%m-%d %H:%M:%S", "Asia/Hong_Kong");
}'
1969-12-31 14:00:00 AHST 1969-12-31 14:00:00 -1000 Wednesday, December 31, 1969 1583053200 1970-01-01 08:00:00 HKT 1970-01-01 08:00:00 +0800 Thursday, January 1, 1970 1582992000
Relative times¶
You can get the seconds since the Miller process start using uptime:
mlr --c2p --from example.csv put '$u=uptime()'
color shape flag k index quantity rate u yellow triangle true 1 11 43.6498 9.8870 0.0011110305786132812 red square true 2 15 79.2778 0.0130 0.0011241436004638672 red circle true 3 16 13.8103 2.9010 0.0011250972747802734 red square false 4 48 77.5542 7.4670 0.0011301040649414062 purple triangle false 5 51 81.2290 8.5910 0.0011301040649414062 red square false 6 64 77.1991 9.5310 0.002481222152709961 purple triangle false 7 65 80.1405 5.8240 0.0024831295013427734 yellow circle true 8 73 63.9785 4.2370 0.0024831295013427734 yellow circle true 9 87 63.5058 8.3350 0.0024852752685546875 purple square false 10 91 72.3735 8.2430 0.002485990524291992
Time-differences can be done in seconds, of course; you can also use the following if you like:
mlr -F | grep hms
dhms2fsec (class=time #args=1) Recovers floating-point seconds as in dhms2fsec("5d18h53m20.250000s") = 500000.250000
dhms2sec (class=time #args=1) Recovers integer seconds as in dhms2sec("5d18h53m20s") = 500000
fsec2dhms (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s"
fsec2hms (class=time #args=1) Formats floating-point seconds as in fsec2hms(5000.25) = "01:23:20.250000"
hms2fsec (class=time #args=1) Recovers floating-point seconds as in hms2fsec("01:23:20.250000") = 5000.250000
hms2sec (class=time #args=1) Recovers integer seconds as in hms2sec("01:23:20") = 5000
sec2dhms (class=time #args=1) Formats integer seconds as in sec2dhms(500000) = "5d18h53m20s"
sec2hms (class=time #args=1) Formats integer seconds as in sec2hms(5000) = "01:23:20"
Calendar differences between dates¶
The datediff function computes the difference
between two dates in calendar units, in the manner of the DATEDIF function found in spreadsheet
programs. Its first two arguments are seconds since the epoch, as produced by
strptime; the third is one of "y", "m", or "d"
for complete years, complete months, or days between the two dates, or "ym", "yd", or "md"
for months ignoring years, days ignoring years, or days ignoring months and years, respectively:
mlr -n put 'end {
t1 = strptime("2001-06-01", "%Y-%m-%d");
t2 = strptime("2002-08-15", "%Y-%m-%d");
print datediff(t1, t2, "y");
print datediff(t1, t2, "m");
print datediff(t1, t2, "d");
print datediff(t1, t2, "ym");
print datediff(t1, t2, "yd");
print datediff(t1, t2, "md");
}'
1 14 440 2 75 14
Differences are computed on calendar dates in GMT, ignoring any time-of-day parts. Unlike the
spreadsheet versions, which error when the first date is after the second, datediff negates the
result in that case.
Timezone troubleshooting¶
Since Miller applies your TZ environment variable at startup, a timezone problem can make any
Miller command fail immediately -- even one that does nothing with times, such as mlr --csv cat
example.csv. The symptom is an error message like
mlr: TZ environment variable appears malformed: "..."
or, from Miller 6.6.0 and earlier, the underlying error from the Go time library:
mlr : time: invalid location name
There are two distinct causes.
Cause 1: TZ is set to a file path. The Go time library, which Miller uses, accepts only
IANA timezone names such as
America/New_York (plus the special values UTC and Local). Unlike the C library, it does not
accept file paths such as /usr/share/zoneinfo/US/Eastern, nor the POSIX form with a leading colon
such as :/etc/localtime -- even though those work fine with other tools on your system (and worked
with Miller 5, which used the C library):
export TZ=/usr/share/zoneinfo/US/Eastern
mlr -n put 'end { print strftime_local(0, "%Y-%m-%d %H:%M:%S %Z") }'
mlr: TZ environment variable appears malformed: "/usr/share/zoneinfo/US/Eastern"
The remedy is to set TZ to the zone's name rather than its file path -- here, export
TZ=US/Eastern -- or to unset it (unset TZ, or export TZ=""), in which case Miller uses the
system's local-time setting.
Cause 2: the timezone database is missing. Go programs such as Miller normally read timezone definitions from a database on the host system, and (unlike C programs) don't fall back to any built-in copy. If that database is absent, even a perfectly valid name fails:
mlr --csv cat example.csv
mlr: TZ environment variable appears malformed: "Asia/Taipei"
The database is normally present on Linux and macOS (at /usr/share/zoneinfo), but is often missing on:
- Windows -- unless Go itself is installed, since the Go toolchain ships a copy of the database (you don't need to use Go; merely installing it suffices).
- Cygwin, unless a package providing a full timezone database is installed.
- Minimal Linux environments such as Alpine-based or
scratchDocker containers, where the database is an optional package.
Remedies, in decreasing order of thoroughness:
- Install the timezone database:
apk add tzdataon Alpine,apt-get install tzdataon Debian/Ubuntu, and so on; on Windows, install Go as noted above. - Point the
ZONEINFOenvironment variable at a zoneinfo directory or zip file -- the Gotimelibrary checks it before the default system locations. For example, a Go installation provides one at$GOROOT/lib/time/zoneinfo.zip. - Avoid timezone-name lookups altogether: leave
TZunset (or set to""orUTC, which need no database), use the UTC-oriented functions such as sec2gmt and strftime, and use numeric fixed offsets such as+0200(matched by%zin strptime format strings) rather than timezone names.
References¶
- Non-Miller-specific list of formatting characters for
strftimeandstrptime: https://devhints.io/strftime - List of valid timezone names: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones