Skip to content

Commit c538237

Browse files
committed
Deal with strptime() on OS X and *BSD (fix jqlang#1415)
strptime() on OS X and *BSDs (reputedly) does not set tm_wday and tm_yday unless corresponding %U and %j format specifiers were used. That can be... surprising when one parsed year, month, and day anyways. Glibc's strptime() conveniently sets tm_wday and tm_yday in those cases, but OS X's does not, ignoring them completely. This commit makes jq compute those where possible, though the day of week computation may be wrong for dates before 1900-03-01 or after 2099-12-31.
1 parent 4a6241b commit c538237

File tree

4 files changed

+83
-10
lines changed

4 files changed

+83
-10
lines changed

docs/content/3.manual/manual.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,9 +1908,11 @@ sections:
19081908
Unix epoch and outputs a "broken down time" representation of
19091909
Greenwhich Meridian time as an array of numbers representing
19101910
(in this order): the year, the month (zero-based), the day of
1911-
the month, the hour of the day, the minute of the hour, the
1912-
second of the minute, the day of the week, and the day of the
1913-
year -- all one-based unless otherwise stated.
1911+
the month (one-based), the hour of the day, the minute of the
1912+
hour, the second of the minute, the day of the week, and the
1913+
day of the year -- all one-based unless otherwise stated. The
1914+
day of the week number may be wrong on some systems for dates
1915+
before March 1st 1900, or after December 31 2099.
19141916
19151917
The `localtime` builtin works like the `gmtime` builtin, but
19161918
using the local timezone setting.

jq.1.prebuilt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.\" generated with Ronn/v0.7.3
22
.\" http://github.com/rtomayko/ronn/tree/0.7.3
33
.
4-
.TH "JQ" "1" "April 2017" "" ""
4+
.TH "JQ" "1" "May 2017" "" ""
55
.
66
.SH "NAME"
77
\fBjq\fR \- Command\-line JSON processor
@@ -2082,7 +2082,7 @@ The \fBnow\fR builtin outputs the current time, in seconds since the Unix epoch\
20822082
Low\-level jq interfaces to the C\-library time functions are also provided: \fBstrptime\fR, \fBstrftime\fR, \fBstrflocaltime\fR, \fBmktime\fR, \fBgmtime\fR, and \fBlocaltime\fR\. Refer to your host operating system\'s documentation for the format strings used by \fBstrptime\fR and \fBstrftime\fR\. Note: these are not necessarily stable interfaces in jq, particularly as to their localization functionality\.
20832083
.
20842084
.P
2085-
The \fBgmtime\fR builtin consumes a number of seconds since the Unix epoch and outputs a "broken down time" representation of Greenwhich Meridian time as an array of numbers representing (in this order): the year, the month (zero\-based), the day of the month, 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\.
2085+
The \fBgmtime\fR builtin consumes a number of seconds since the Unix epoch and outputs a "broken down time" representation of Greenwhich Meridian time as an array of numbers representing (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\.
20862086
.
20872087
.P
20882088
The \fBlocaltime\fR builtin works like the \fBgmtime\fR builtin, but using the local timezone setting\.

src/builtin.c

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,63 @@ static time_t my_mktime(struct tm *tm) {
12211221
#endif
12221222
}
12231223

1224+
/* Compute and set tm_wday */
1225+
static void set_tm_wday(struct tm *tm) {
1226+
/*
1227+
* https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Gauss.27s_algorithm
1228+
* https://cs.uwaterloo.ca/~alopez-o/math-faq/node73.html
1229+
*
1230+
* Tested with dates from 1900-01-01 through 2100-01-01. This
1231+
* algorithm produces the wrong day-of-the-week number for dates in
1232+
* the range 1900-01-01..1900-02-28, and for 2100-01-01..2100-02-28.
1233+
* Since this is only needed on OS X and *BSD, we might just document
1234+
* this.
1235+
*/
1236+
int century = (1900 + tm->tm_year) / 100;
1237+
int year = (1900 + tm->tm_year) % 100;
1238+
if (tm->tm_mon < 2)
1239+
year--;
1240+
/*
1241+
* The month value in the wday computation below is shifted so that
1242+
* March is 1, April is 2, .., January is 11, and February is 12.
1243+
*/
1244+
int mon = tm->tm_mon - 1;
1245+
if (mon < 1)
1246+
mon += 12;
1247+
int wday =
1248+
(tm->tm_mday + (int)floor((2.6 * mon - 0.2)) + year + (int)floor(year / 4.0) + (int)floor(century / 4.0) - 2 * century) % 7;
1249+
if (wday < 0)
1250+
wday += 7;
1251+
#if 0
1252+
/* See commentary above */
1253+
assert(wday == tm->tm_wday || tm->tm_wday == 8);
1254+
#endif
1255+
tm->tm_wday = wday;
1256+
}
1257+
/*
1258+
* Compute and set tm_yday.
1259+
*
1260+
*/
1261+
static void set_tm_yday(struct tm *tm) {
1262+
static const int d[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
1263+
int mon = tm->tm_mon;
1264+
int year = 1900 + tm->tm_year;
1265+
int leap_day = 0;
1266+
if (tm->tm_mon > 1 &&
1267+
((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
1268+
leap_day = 1;
1269+
1270+
/* Bound check index into d[] */
1271+
if (mon < 0)
1272+
mon = -mon;
1273+
if (mon > 11)
1274+
mon %= 12;
1275+
1276+
int yday = d[mon] + leap_day + tm->tm_mday - 1;
1277+
assert(yday == tm->tm_yday || tm->tm_yday == 367);
1278+
tm->tm_yday = yday;
1279+
}
1280+
12241281
#ifdef HAVE_STRPTIME
12251282
static jv f_strptime(jq_state *jq, jv a, jv b) {
12261283
if (jv_get_kind(a) != JV_KIND_STRING || jv_get_kind(b) != JV_KIND_STRING)
@@ -1241,10 +1298,19 @@ static jv f_strptime(jq_state *jq, jv a, jv b) {
12411298
return e;
12421299
}
12431300
jv_free(b);
1244-
if ((tm.tm_wday == 8 || tm.tm_yday == 367) && my_timegm(&tm) == (time_t)-2) {
1245-
jv_free(a);
1246-
return jv_invalid_with_msg(jv_string("strptime/1 not supported on this platform"));
1247-
}
1301+
/*
1302+
* This is OS X or some *BSD whose strptime() is just not that
1303+
* helpful!
1304+
*
1305+
* We don't know that the format string did involve parsing a
1306+
* year, or a month (if tm->tm_mon == 0). But with our invalid
1307+
* day-of-week and day-of-year sentinel checks above, the worst
1308+
* this can do is produce garbage.
1309+
*/
1310+
if (tm.tm_wday == 8 && tm.tm_mday != 0 && tm.tm_mon >= 0 && tm.tm_mon <= 11)
1311+
set_tm_wday(&tm);
1312+
if (tm.tm_yday == 367 && tm.tm_mday != 0 && tm.tm_mon >= 0 && tm.tm_mon <= 11)
1313+
set_tm_yday(&tm);
12481314
jv r = tm2jv(&tm);
12491315
if (*end != '\0')
12501316
r = jv_array_append(r, jv_string(end));

tests/optional.test

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

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

7+
# Check day-of-week and day of year computations
8+
# (should trip an assert if this fails)
9+
last(range(365 * 199)|("1900-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"))
10+
null
11+
[2099,0,10,1,2,3,6,9]
12+
813
# %e is not available on mingw/WIN32
914
strftime("%A, %B %e, %Y")
1015
1435677542.822351

0 commit comments

Comments
 (0)