]> granicus.if.org Git - jq/commitdiff
Deal with strptime() on OS X and *BSD (fix #1415)
authorNicolas Williams <nico@cryptonector.com>
Sun, 21 May 2017 21:24:48 +0000 (16:24 -0500)
committerNicolas Williams <nico@cryptonector.com>
Sun, 21 May 2017 21:24:48 +0000 (16:24 -0500)
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.

docs/content/3.manual/manual.yml
jq.1.prebuilt
src/builtin.c
tests/optional.test

index fcca8412fb76729ae874569f6c2afbf28bf2f41c..119d42bf48f065a0efe24012dfff757e6cabed96 100644 (file)
@@ -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.
index 21a0fcd0ae9bb15028f2a909dc5b98c449403a4f..03672cc2ce43ff43b24945c82fd4a54a8349f245 100644 (file)
@@ -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\.
index 4ad6e1731b55fa829ffbe2ef096c8f5433a1a5fc..23b91546b622a11975a5acdae172d8a7b3e9df7f 100644 (file)
@@ -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));
index 0ee1fb279231096e2a5f2857932f3adaea3bc680..fc37e60705b618a566c6135ca96ba9501f4b4da7 100644 (file)
@@ -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