Skip to content

Commit

Permalink
Broken down time needs tm_{isdst,gmtoff,zone} too (fix #1912)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicowilliams committed Aug 28, 2023
1 parent 91d7257 commit b7df0ea
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 18 deletions.
11 changes: 7 additions & 4 deletions docs/content/manual/manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2199,9 +2199,12 @@ sections:
(in this order): the year, the month (zero-based), the day of
the month (one-based), the hour of the day, the minute of the
hour, the second of the minute, the day of the week, and the
day of the year -- all one-based unless otherwise stated. The
day of the week number may be wrong on some systems for dates
before March 1st 1900, or after December 31 2099.
day of the year -all one-based unless otherwise stated-
followed by a boolean indicating whether the time is in a
daylight savings timezone, followed by the offset to UTC,
followed by the name of the timezone. The day of the week
number may be wrong on some systems for dates before March
1st 1900, or after December 31 2099.
The `localtime` builtin works like the `gmtime` builtin, but
using the local timezone setting.
Expand Down Expand Up @@ -2232,7 +2235,7 @@ sections:

- program: 'strptime("%Y-%m-%dT%H:%M:%SZ")'
input: '"2015-03-05T23:51:47Z"'
output: ['[2015,2,5,23,51,47,4,63]']
output: ['[2015,2,5,23,51,47,4,63,false,0]']

- program: 'strptime("%Y-%m-%dT%H:%M:%SZ")|mktime'
input: '"2015-03-05T23:51:47Z"'
Expand Down
4 changes: 2 additions & 2 deletions jq.1.prebuilt

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

68 changes: 60 additions & 8 deletions src/builtin.c
Original file line number Diff line number Diff line change
Expand Up @@ -1287,14 +1287,20 @@ static jv f_stderr(jq_state *jq, jv input) {
}

static jv tm2jv(struct tm *tm) {
return JV_ARRAY(jv_number(tm->tm_year + 1900),
jv a = JV_ARRAY(jv_number(tm->tm_year + 1900),
jv_number(tm->tm_mon),
jv_number(tm->tm_mday),
jv_number(tm->tm_hour),
jv_number(tm->tm_min),
jv_number(tm->tm_sec),
jv_number(tm->tm_wday),
jv_number(tm->tm_yday));
jv_number(tm->tm_yday),
tm->tm_isdst ? jv_true() : jv_false());
// JV_ARRAY() only handles up to 9 arguments
a = jv_array_append(a, jv_number(tm->tm_gmtoff));
if (tm->tm_zone)
return jv_array_append(a, jv_string(tm->tm_zone));
return a;
}

#if defined(WIN32) && !defined(HAVE_SETENV)
Expand Down Expand Up @@ -1488,7 +1494,10 @@ static jv f_strptime(jq_state *jq, jv a, jv b) {
jv_free(n); \
} while (0)

static int jv2tm(jv a, struct tm *tm) {
static int jv2tm(jv a, struct tm *tm, double *frac, char **freeme) {
int ret = 1;

*freeme = NULL;
memset(tm, 0, sizeof(*tm));
TO_TM_FIELD(tm->tm_year, a, 0);
tm->tm_year -= 1900;
Expand All @@ -1497,8 +1506,36 @@ static int jv2tm(jv a, struct tm *tm) {
TO_TM_FIELD(tm->tm_hour, a, 3);
TO_TM_FIELD(tm->tm_min, a, 4);
TO_TM_FIELD(tm->tm_sec, a, 5);
if (frac) {
double d = jv_number_value(jv_array_get(jv_copy(a), 5));

*frac = d - floor(d);
}
TO_TM_FIELD(tm->tm_wday, a, 6);
TO_TM_FIELD(tm->tm_yday, a, 7);
jv v = jv_array_get(jv_copy(a), 8);
switch (jv_get_kind(v)) {
case JV_KIND_INVALID: break;
case JV_KIND_FALSE: break;
case JV_KIND_TRUE: tm->tm_isdst = 1; break;
case JV_KIND_NUMBER: tm->tm_isdst = !!(int)jv_number_value(v); break;
default: ret = 0; break;
}
jv_free(v);
v = jv_array_get(jv_copy(a), 9);
if (jv_get_kind(v) == JV_KIND_NUMBER)
tm->tm_gmtoff = jv_number_value(v);
else if (jv_is_valid(v))
ret = 0;
jv_free(v);
v = jv_array_get(jv_copy(a), 10);
if (jv_get_kind(v) == JV_KIND_STRING) {
tm->tm_zone = *freeme = strdup(jv_string_value(v));
if (tm->tm_zone == NULL)
ret = 0;
} else if (jv_is_valid(v)) {
ret = 0;
}
jv_free(a);

// We use UTC everywhere (gettimeofday, gmtime) and UTC does not do DST.
Expand All @@ -1509,7 +1546,11 @@ static int jv2tm(jv a, struct tm *tm) {
// hope it is okay to initialize them to zero, because the standard does not
// provide an alternative.

return 1;
if (!ret) {
free(*freeme);
*freeme = NULL;
}
return ret;
}

#undef TO_TM_FIELD
Expand All @@ -1519,15 +1560,18 @@ static jv f_mktime(jq_state *jq, jv a) {
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;
if (!jv2tm(a, &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);
free(freeme);
if (t == (time_t)-1)
return jv_invalid_with_msg(jv_string("invalid gmtime representation"));
if (t == (time_t)-2)
return jv_invalid_with_msg(jv_string("mktime not supported on this platform"));
return jv_number(t);
return jv_number(t + frac);
}

#ifdef HAVE_GMTIME_R
Expand Down Expand Up @@ -1618,14 +1662,16 @@ static jv f_strftime(jq_state *jq, jv a, jv b) {
return ret_error2(a, b, jv_string("strftime/1 requires a string format"));
}
struct tm tm;
if (!jv2tm(a, &tm))
char *freeme;
if (!jv2tm(a, &tm, NULL, &freeme))
return ret_error(b, jv_string("strftime/1 requires parsed datetime inputs"));

const char *fmt = jv_string_value(b);
size_t alloced = strlen(fmt) + 100;
char *buf = alloca(alloced);
size_t n = strftime(buf, alloced, fmt, &tm);
jv_free(b);
free(freeme);
/* POSIX doesn't provide errno values for strftime() failures; weird */
if (n == 0 || n > alloced)
return jv_invalid_with_msg(jv_string("strftime/1: unknown system failure"));
Expand All @@ -1643,19 +1689,25 @@ static jv f_strftime(jq_state *jq, jv a, jv b) {
static jv f_strflocaltime(jq_state *jq, jv a, jv b) {
if (jv_get_kind(a) == JV_KIND_NUMBER) {
a = f_localtime(jq, a);
if (!jv_is_valid(a)) {
jv_free(b);
return a;
}
} else if (jv_get_kind(a) != JV_KIND_ARRAY) {
return ret_error2(a, b, jv_string("strflocaltime/1 requires parsed datetime inputs"));
} else if (jv_get_kind(b) != JV_KIND_STRING) {
return ret_error2(a, b, jv_string("strflocaltime/1 requires a string format"));
}
struct tm tm;
if (!jv2tm(a, &tm))
char *freeme;
if (!jv2tm(a, &tm, NULL, &freeme))
return ret_error(b, jv_string("strflocaltime/1 requires parsed datetime inputs"));
const char *fmt = jv_string_value(b);
size_t alloced = strlen(fmt) + 100;
char *buf = alloca(alloced);
size_t n = strftime(buf, alloced, fmt, &tm);
jv_free(b);
free(freeme);
/* POSIX doesn't provide errno values for strftime() failures; weird */
if (n == 0 || n > alloced)
return jv_invalid_with_msg(jv_string("strflocaltime/1: unknown system failure"));
Expand Down
2 changes: 1 addition & 1 deletion tests/jq.test
Original file line number Diff line number Diff line change
Expand Up @@ -1565,7 +1565,7 @@ strftime("%A, %B %d, %Y")

gmtime
1425599507
[2015,2,5,23,51,47,4,63]
[2015,2,5,23,51,47,4,63,false,0,"GMT"]

# test invalid tm input
try strftime("%Y-%m-%dT%H:%M:%SZ") catch .
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
Expand Up @@ -3,14 +3,14 @@
# strptime() is not available on mingw/WIN32
[strptime("%Y-%m-%dT%H:%M:%SZ")|(.,mktime)]
"2015-03-05T23:51:47Z"
[[2015,2,5,23,51,47,4,63],1425599507]
[[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"))
null
[2037,1,11,1,2,3,3,41]
[2037,1,11,1,2,3,3,41,false,0]

# %e is not available on mingw/WIN32
strftime("%A, %B %e, %Y")
Expand Down

0 comments on commit b7df0ea

Please sign in to comment.