From ac35c7dcbb991fa0f3c3196dd00ca479211e9847 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:10:52 +0530 Subject: [PATCH] Default the day of the month to 1 in `strptime/1` (#3679) A format without %d left the day of the month at 0, which mktime and todate read as the last day of the previous month, and the weekday was wrong for dates in January and February of 1900 and 2100. Start parsing from the 1st, so a format without a day now gives the first of the month, and a time-only format no longer outputs the 8 and 367 placeholders for the weekday and day of the year. This also fixes the weekday of dates in January and February of 1900 and 2100. --- src/builtin.c | 21 +++++++++++++-------- tests/jq.test | 9 +++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/builtin.c b/src/builtin.c index 7d6f775d..96e5a222 100644 --- a/src/builtin.c +++ b/src/builtin.c @@ -1608,16 +1608,15 @@ 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. + * Tested against gmtime() for every date from 1600-01-01 through + * 2400-12-31. */ int century = (1900 + tm->tm_year) / 100; int year = (1900 + tm->tm_year) % 100; - if (tm->tm_mon < 2) - year--; + if (tm->tm_mon < 2 && --year < 0) { + year += 100; + century--; + } /* * 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. @@ -1630,7 +1629,10 @@ static void set_tm_wday(struct tm *tm) { if (wday < 0) wday += 7; #if 0 - /* See commentary above */ + /* + * Off: the bundled strptime() can leave a wrong weekday here, and %a + * or %u can name one that does not match the date. + */ assert(wday == tm->tm_wday || tm->tm_wday == 8); #endif tm->tm_wday = wday; @@ -1666,6 +1668,7 @@ static jv f_strptime(jq_state *jq, jv a, jv b) { struct tm tm; memset(&tm, 0, sizeof(tm)); + tm.tm_mday = 1; // a format without a day of the month means the 1st tm.tm_wday = 8; // sentinel tm.tm_yday = 367; // sentinel @@ -1698,6 +1701,8 @@ static jv f_strptime(jq_state *jq, jv a, jv b) { set_tm_yday(&tm); #elif defined(WIN32) || !defined(HAVE_STRPTIME) set_tm_wday(&tm); + if (tm.tm_yday == 367) + set_tm_yday(&tm); #else if (tm.tm_wday == 8 && tm.tm_mday != 0 && tm.tm_mon >= 0 && tm.tm_mon <= 11) set_tm_wday(&tm); diff --git a/tests/jq.test b/tests/jq.test index 6086f376..b0db09fd 100644 --- a/tests/jq.test +++ b/tests/jq.test @@ -1988,6 +1988,15 @@ try strflocaltime("%c" * 10) catch . "2015-03-05T23:51:47Z" [[2015,2,5,23,51,47,4,63],1425599507] +# a format without a day of the month gives the first of the month +[("2024-06" | strptime("%Y-%m")), ("2024" | strptime("%Y")) | ., todate] +null +[[2024,5,1,0,0,0,6,152],"2024-06-01T00:00:00Z",[2024,0,1,0,0,0,1,0],"2024-01-01T00:00:00Z"] + +"10:15" | strptime("%H:%M") +null +[1900,0,1,10,15,0,1,0] + # Check day-of-week and day of year computations # (should trip an assert if this fails) 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"))