Skip to content

Commit

Permalink
Add timelocal and timegm
Browse files Browse the repository at this point in the history
  • Loading branch information
nicowilliams committed Aug 29, 2023
1 parent 3d3fef0 commit 0daf027
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 61 deletions.
4 changes: 4 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,14 @@ AC_FIND_FUNC([strptime], [c], [#include <time.h>], [0, 0, 0])
AC_FIND_FUNC([strftime], [c], [#include <time.h>], [0, 0, 0, 0])
AC_FIND_FUNC([setenv], [c], [#include <stdlib.h>], [0, 0, 0])
AC_FIND_FUNC([timegm], [c], [#include <time.h>], [0])
AC_FIND_FUNC([timelocal], [c], [#include <time.h>], [0])
AC_FIND_FUNC([gmtime_r], [c], [#include <time.h>], [0, 0])
AC_FIND_FUNC([gmtime], [c], [#include <time.h>], [0])
AC_FIND_FUNC([localtime_r], [c], [#include <time.h>], [0, 0])
AC_FIND_FUNC([localtime], [c], [#include <time.h>], [0])
AC_FIND_FUNC([mktime], [c], [#include <time.h>], [0])
AC_FIND_FUNC([_mkgmtime], [c], [#include <time.h>], [0])
AC_FIND_FUNC([difftime], [c], [#include <time.h>], [0, 0])
AC_FIND_FUNC([gettimeofday], [c], [#include <sys/time.h>], [0, 0])
AC_STRUCT_TIMEZONE
AC_CHECK_MEMBER([struct tm.tm_gmtoff], [AC_DEFINE([HAVE_TM_TM_GMTOFF],1,[Define to 1 if the system has the tm_gmtoff field in struct tm])],
Expand Down
23 changes: 16 additions & 7 deletions docs/content/manual/manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2187,10 +2187,11 @@ sections:
Low-level jq interfaces to the C-library time functions are
also provided: `strptime`, `strftime`, `strflocaltime`,
`mktime`, `gmtime`, and `localtime`. Refer to your host
operating system's documentation for the format strings used
by `strptime` and `strftime`. Note: these are not necessarily
stable interfaces in jq, particularly as to their localization
`mktime`, `gmtime`, `localtime`, `timegm`, and
`timelocal`. Refer to your host operating system's
documentation for the format strings used by `strptime`
and `strftime`. Note: these are not necessarily stable
interfaces in jq, particularly as to their localization
functionality.
The `gmtime` builtin consumes a number of seconds since the
Expand All @@ -2210,8 +2211,16 @@ sections:
The `localtime` builtin works like the `gmtime` builtin, but
using the local timezone setting.
The `mktime` builtin consumes "broken down time"
representations of time output by `gmtime` and `strptime`.
The `mktime`, `timegm` and `timelocal` builtins do the
opposite of `gmtime` and `localtime`, respectively: they
consume "broken down time" representations of time
output by `gmtime`, `localtime`, and `strptime`, and
output a number of seconds since the Unix epoch.
Note that the C `mktime()` function interprets its broken
down time input as local time, while the jq `mktime`
function interpretes its broken down time as UTC. It is
better to use `timelocal` and `timegm` to avoid confusion.
The `strptime(fmt)` builtin parses input strings matching the
`fmt` argument. The output is in the "broken down time"
Expand All @@ -2238,7 +2247,7 @@ sections:
input: '"2015-03-05T23:51:47Z"'
output: ['[2015,2,5,23,51,47,4,63]']

- program: 'strptime("%Y-%m-%dT%H:%M:%SZ")|mktime'
- program: 'strptime("%Y-%m-%dT%H:%M:%SZ")|timegm'
input: '"2015-03-05T23:51:47Z"'
output: ['1425599507']

Expand Down
9 changes: 6 additions & 3 deletions jq.1.prebuilt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 62 additions & 47 deletions src/builtin.c
Original file line number Diff line number Diff line change
Expand Up @@ -1332,50 +1332,6 @@ static int setenv(const char *var, const char *val, int ovr)
}
#endif

/*
* mktime() has side-effects and anyways, returns time in the local
* timezone, not UTC. We want timegm(), which isn't standard.
*
* To make things worse, mktime() tells you what the timezone
* adjustment is, but you have to #define _BSD_SOURCE to get this
* field of struct tm on some systems.
*
* This is all to blame on POSIX, of course.
*
* Our wrapper tries to use timegm() if available, or mktime() and
* correct for its side-effects if possible.
*
* Returns (time_t)-2 if mktime()'s side-effects cannot be corrected.
*/
static time_t my_mktime(struct tm *tm) {
#ifdef HAVE_TIMEGM
return timegm(tm);
#elif HAVE_TM_TM_GMT_OFF

time_t t = mktime(tm);
if (t == (time_t)-1)
return t;
return t + tm->tm_gmtoff;
#elif HAVE_TM___TM_GMT_OFF
time_t t = mktime(tm);
if (t == (time_t)-1)
return t;
return t + tm->__tm_gmtoff;
#elif WIN32
return _mkgmtime(tm);
#else
char *tz;

tz = (tz = getenv("TZ")) != NULL ? strdup(tz) : NULL;
if (tz != NULL)
setenv("TZ", "", 1);
time_t t = mktime(tm);
if (tz != NULL)
setenv("TZ", tz, 1);
return t;
#endif
}

/* Compute and set tm_wday */
static void set_tm_wday(struct tm *tm) {
/*
Expand Down Expand Up @@ -1571,14 +1527,29 @@ static int jv2tm(jv a, struct tm *tm, double *frac, char **freeme) {
static jv f_mktime(jq_state *jq, jv a) {
if (jv_get_kind(a) != JV_KIND_ARRAY)
return ret_error(a, jv_string("mktime requires array inputs"));
if (jv_array_length(jv_copy(a)) < 6)
return ret_error(a, jv_string("mktime requires parsed datetime inputs"));
double frac;
struct tm tm;
char *freeme;
if (!jv2tm(a, &tm, &frac, &freeme))
return jv_invalid_with_msg(jv_string("mktime requires parsed datetime inputs"));
time_t t = my_mktime(&tm);
/*
* mktime() has side-effects and anyways, returns time in the local
* timezone, not UTC. Does timelocal() have side-effects?
*
* We try to use timegm() if available, else mktime().
*/
#if HAVE_TIMEGM
time_t t = timegm(&tm);
#elif defined(WIN32)
time_t t = _mkgmtime(&tm);
#else
time_t t = mktime(&tm);
#ifdef HAVE_TM_TM_GMTOFF
t += tm.tm_gmtoff;
#else
/* Bah, what can we do? */
#endif
#endif
free(freeme);
if (t == (time_t)-1)
return jv_invalid_with_msg(jv_string("invalid gmtime representation"));
Expand All @@ -1587,6 +1558,48 @@ static jv f_mktime(jq_state *jq, jv a) {
return jv_number(t + frac);
}

static jv f_timelocal(jq_state *jq, jv a) {
if (jv_get_kind(a) != JV_KIND_ARRAY)
return ret_error(a, jv_string("timelocal requires array inputs"));
double frac;
struct tm tm;
char *freeme;
if (!jv2tm(a, &tm, &frac, &freeme))
return jv_invalid_with_msg(jv_string("timelocal requires parsed datetime inputs"));
#ifdef HAVE_TIMELOCAL
time_t t = timelocal(&tm);
#else
time_t t = mktime(&tm);
#endif
free(freeme);
if (t == (time_t)-1)
return jv_invalid_with_msg(jv_string("invalid gmtime representation"));
return jv_number(t + frac);
}

static jv f_timegm(jq_state *jq, jv a) {
if (jv_get_kind(a) != JV_KIND_ARRAY)
return ret_error(a, jv_string("timegm requires array inputs"));
double frac;
struct tm tm;
char *freeme;
if (!jv2tm(a, &tm, &frac, &freeme))
return jv_invalid_with_msg(jv_string("timegm requires parsed datetime inputs"));
#ifdef HAVE_TIMEGM
time_t t = timegm(&tm);
free(freeme);
#elif defined(HAVE__MKGMTIME)
time_t t = _mkgmtime(&tm); // Windows has _mkgmtime()
free(freeme);
#else
free(freeme);
return jv_invalid_with_msg(jv_string("timegm is not supported"));
#endif
if (t == (time_t)-1)
return jv_invalid_with_msg(jv_string("invalid gmtime representation"));
return jv_number(t + frac);
}

#ifdef HAVE_GMTIME_R
static jv f_gmtime(jq_state *jq, jv a) {
if (jv_get_kind(a) != JV_KIND_NUMBER)
Expand Down Expand Up @@ -1843,6 +1856,8 @@ BINOPS
{f_mktime, "mktime", 1},
{f_gmtime, "gmtime", 1},
{f_localtime, "localtime", 1},
{f_timegm, "timegm", 1},
{f_timelocal, "timelocal", 1},
{f_now, "now", 1},
{f_current_filename, "input_filename", 1},
{f_current_line, "input_line_number", 1},
Expand Down
2 changes: 1 addition & 1 deletion src/builtin.jq
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def _flatten($x): reduce .[] as $i ([]; if $i | type == "array" and $x != 0 then
def flatten($x): if $x < 0 then error("flatten depth must not be negative") else _flatten($x) end;
def flatten: _flatten(-1);
def range($x): range(0;$x);
def fromdateiso8601: strptime("%Y-%m-%dT%H:%M:%SZ")|mktime;
def fromdateiso8601: strptime("%Y-%m-%dT%H:%M:%SZ")|timegm;
def todateiso8601: strftime("%Y-%m-%dT%H:%M:%SZ");
def fromdate: fromdateiso8601;
def todate: todateiso8601;
Expand Down
2 changes: 1 addition & 1 deletion tests/man.test

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions tests/optional.test
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# See tests/jq.test and the jq manual for more information.

# strptime() is not available on mingw/WIN32
[strptime("%Y-%m-%dT%H:%M:%SZ")|(.,mktime)]
[strptime("%Y-%m-%dT%H:%M:%SZ")|(.,timegm)]
"2015-03-05T23:51:47Z"
[[2015,2,5,23,51,47,4,63,false,0],1425599507]

# Check day-of-week and day of year computations
# (should trip an assert if this fails)
# This date range
last(range(365 * 67)|("1970-03-01T01:02:03Z"|strptime("%Y-%m-%dT%H:%M:%SZ")|mktime) + (86400 * .)|strftime("%Y-%m-%dT%H:%M:%SZ")|strptime("%Y-%m-%dT%H:%M:%SZ"))
last(range(365 * 67)|("1970-03-01T01:02:03Z"|strptime("%Y-%m-%dT%H:%M:%SZ")|timegm) + (86400 * .)|strftime("%Y-%m-%dT%H:%M:%SZ")|strptime("%Y-%m-%dT%H:%M:%SZ"))
null
[2037,1,11,1,2,3,3,41,false,0]

Expand Down
15 changes: 15 additions & 0 deletions tests/shtest
Original file line number Diff line number Diff line change
Expand Up @@ -561,4 +561,19 @@ if ! $VALGRIND $Q $JQ -n -f "$JQTESTDIR/yes-main-program.jq" > $d/out 2>&1; then
exit 1
fi

# #2863
if ! TZ=CST+6CDT $VALGRIND $Q $JQ -cne '
1693243637
| localtime
| (.[9] |= if .==null then -18000 else . end)
| (.[10] |= if .==null then "CDT" else . end)
| [.,mktime,timegm,timelocal,timegm - timelocal]
| debug
| (.[1] == .[2])
and
(. == [[2023,7,28,12,27,17,1,239,true,-18000,"CDT"],1693225637,1693225637,1693243637,-18000])'; then
echo "Time functions not working correctly"
exit 1
fi

exit 0

0 comments on commit 0daf027

Please sign in to comment.