summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNicolas Williams <nico@cryptonector.com>2017-05-21 16:24:48 -0500
committerNicolas Williams <nico@cryptonector.com>2017-05-21 16:24:48 -0500
commitc538237f4e4c381d35f1c15497c95f659fd55850 (patch)
tree86ae2d701edc08d00507c600bdff879f0f10916e
parent4a6241be0697bbe4ef420c43689c34af59e50330 (diff)
Deal with strptime() on OS X and *BSD (fix #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.
-rw-r--r--docs/content/3.manual/manual.yml8
-rw-r--r--jq.1.prebuilt4
-rw-r--r--src/builtin.c74
-rw-r--r--tests/optional.test7
4 files changed, 83 insertions, 10 deletions
diff --git a/docs/content/3.manual/manual.yml b/docs/content/3.manual/manual.yml
index fcca8412..119d42bf 100644
--- a/docs/content/3.manual/manual.yml
+++ b/docs/content/3.manual/manual.yml
@@ -1908,9 +1908,11 @@ sections:
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.
+ 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.
The `localtime` builtin works like the `gmtime` builtin, but
using the local timezone setting.
diff --git a/jq.1.prebuilt b/jq.1.prebuilt
index 21a0fcd0..03672cc2 100644
--- a/jq.1.prebuilt
+++ b/jq.1.prebuilt
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
-.TH "JQ" "1" "April 2017" "" ""
+.TH "JQ" "1" "May 2017" "" ""
.
.SH "NAME"
\fBjq\fR \- Command\-line JSON processor
@@ -2082,7 +2082,7 @@ The \fBnow\fR builtin outputs the current time, in seconds since the Unix epoch\
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\.
.
.P
-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\.
+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\.
.
.P
The \fBlocaltime\fR builtin works like the \fBgmtime\fR builtin, but using the local timezone setting\.
diff --git a/src/builtin.c b/src/builtin.c
index 4ad6e173..23b91546 100644
--- a/src/builtin.c
+++ b/src/builtin.c
@@ -1221,6 +1221,63 @@ static time_t my_mktime(struct tm *tm) {
#endif
}
+/* Compute and set tm_wday */
+static void set_tm_wday(struct tm *tm) {
+ /*
+ * https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Gauss.27s_algorithm
+ * https://cs.uwaterloo.ca/~alopez-o/math-faq/node73.html
+ *
+ * Tested with dates from 1900-01-01 through 2100-01-01. This
+ * algorithm produces the wrong day-of-the-week number for dates in
+ * the range 1900-01-01..1900-02-28, and for 2100-01-01..2100-02-28.
+ * Since this is only needed on OS X and *BSD, we might just document
+ * this.
+ */
+ int century = (1900 + tm->tm_year) / 100;
+ int year = (1900 + tm->tm_year) % 100;
+ if (tm->tm_mon < 2)
+ year--;
+ /*
+ * The month value in the wday computation below is shifted so that
+ * March is 1, April is 2, .., January is 11, and February is 12.
+ */
+ int mon = tm->tm_mon - 1;
+ if (mon < 1)
+ mon += 12;
+ int wday =
+ (tm->tm_mday + (int)floor((2.6 * mon - 0.2)) + year + (int)floor(year / 4.0) + (int)floor(century / 4.0) - 2 * century) % 7;
+ if (wday < 0)
+ wday += 7;
+#if 0
+ /* See commentary above */
+ assert(wday == tm->tm_wday || tm->tm_wday == 8);
+#endif
+ tm->tm_wday = wday;
+}
+/*
+ * Compute and set tm_yday.
+ *
+ */
+static void set_tm_yday(struct tm *tm) {
+ static const int d[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
+ int mon = tm->tm_mon;
+ int year = 1900 + tm->tm_year;
+ int leap_day = 0;
+ if (tm->tm_mon > 1 &&
+ ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
+ leap_day = 1;
+
+ /* Bound check index into d[] */
+ if (mon < 0)
+ mon = -mon;
+ if (mon > 11)
+ mon %= 12;
+
+ int yday = d[mon] + leap_day + tm->tm_mday - 1;
+ assert(yday == tm->tm_yday || tm->tm_yday == 367);
+ tm->tm_yday = yday;
+}
+
#ifdef HAVE_STRPTIME
static jv f_strptime(jq_state *jq, jv a, jv b) {
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) {
return e;
}
jv_free(b);
- if ((tm.tm_wday == 8 || tm.tm_yday == 367) && my_timegm(&tm) == (time_t)-2) {
- jv_free(a);
- return jv_invalid_with_msg(jv_string("strptime/1 not supported on this platform"));
- }
+ /*
+ * This is OS X or some *BSD whose strptime() is just not that
+ * helpful!
+ *
+ * We don't know that the format string did involve parsing a
+ * year, or a month (if tm->tm_mon == 0). But with our invalid
+ * day-of-week and day-of-year sentinel checks above, the worst
+ * this can do is produce garbage.
+ */
+ if (tm.tm_wday == 8 && tm.tm_mday != 0 && tm.tm_mon >= 0 && tm.tm_mon <= 11)
+ set_tm_wday(&tm);
+ if (tm.tm_yday == 367 && tm.tm_mday != 0 && tm.tm_mon >= 0 && tm.tm_mon <= 11)
+ set_tm_yday(&tm);
jv r = tm2jv(&tm);
if (*end != '\0')
r = jv_array_append(r, jv_string(end));
diff --git a/tests/optional.test b/tests/optional.test
index 0ee1fb27..fc37e607 100644
--- a/tests/optional.test
+++ b/tests/optional.test
@@ -2,9 +2,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]
+# Check day-of-week and day of year computations
+# (should trip an assert if this fails)
+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"))
+null
+[2099,0,10,1,2,3,6,9]
+
# %e is not available on mingw/WIN32
strftime("%A, %B %e, %Y")
1435677542.822351