diff --git a/src/builtin.c b/src/builtin.c index c0aba960b0..fc06d949a5 100644 --- a/src/builtin.c +++ b/src/builtin.c @@ -1594,7 +1594,7 @@ static jv f_strptime(jq_state *jq, jv a, jv b) { return r; } -static int jv2tm(jv a, struct tm *tm) { +static int jv2tm(jv a, struct tm *tm, int localtime) { memset(tm, 0, sizeof(*tm)); static const size_t offsets[] = { offsetof(struct tm, tm_year), @@ -1624,13 +1624,25 @@ static int jv2tm(jv a, struct tm *tm) { jv_free(n); } - // We use UTC everywhere (gettimeofday, gmtime) and UTC does not do DST. - // Setting tm_isdst to 0 is done by the memset. - // tm->tm_isdst = 0; + if (localtime) { + tm->tm_isdst = -1; + mktime(tm); + } else { +#ifdef HAVE_TIMEGM + timegm(tm); +#elif HAVE_TM_TM_GMT_OFF + // tm->tm_gmtoff = 0; + tm->tm_zone = "GMT"; +#elif HAVE_TM___TM_GMT_OFF + // tm->__tm_gmtoff = 0; + tm->__tm_zone = "GMT"; +#endif + // tm->tm_isdst = 0; - // The standard permits the tm structure to contain additional members. We - // hope it is okay to initialize them to zero, because the standard does not - // provide an alternative. + // The standard permits the tm structure to contain additional members. We + // hope it is okay to initialize them to zero, because the standard does not + // provide an alternative. + } jv_free(a); return 1; @@ -1642,7 +1654,7 @@ static jv f_mktime(jq_state *jq, jv a) { if (jv_get_kind(a) != JV_KIND_ARRAY) return ret_error(a, jv_string("mktime requires array inputs")); struct tm tm; - if (!jv2tm(a, &tm)) + if (!jv2tm(a, &tm, 0)) return jv_invalid_with_msg(jv_string("mktime requires parsed datetime inputs")); time_t t = my_mktime(&tm); if (t == (time_t)-1) @@ -1740,13 +1752,27 @@ static jv f_strftime(jq_state *jq, jv a, jv b) { if (jv_get_kind(b) != JV_KIND_STRING) return ret_error2(a, b, jv_string("strftime/1 requires a string format")); struct tm tm; - if (!jv2tm(a, &tm)) + if (!jv2tm(a, &tm, 0)) 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); +#ifdef __APPLE__ + /* Apple Libc (as of version 1669.40.2) contains a bug which causes it to + * ignore the `tm.tm_gmtoff` in favor of the global timezone. To print the + * proper timezone offset we temporarily switch the TZ to UTC. */ + char *tz = getenv("TZ"); + setenv("TZ", "UTC", 1); +#endif size_t n = strftime(buf, alloced, fmt, &tm); +#ifdef __APPLE__ + if (tz) { + setenv("TZ", tz, 1); + } else { + unsetenv("TZ"); + } +#endif jv_free(b); /* POSIX doesn't provide errno values for strftime() failures; weird */ if (n == 0 || n > alloced) @@ -1771,7 +1797,7 @@ static jv f_strflocaltime(jq_state *jq, jv a, jv b) { 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)) + if (!jv2tm(a, &tm, 1)) 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; diff --git a/tests/shtest b/tests/shtest index 2417c6a64c..7a482e5112 100755 --- a/tests/shtest +++ b/tests/shtest @@ -694,4 +694,26 @@ printf '[\n {\n "a": 1\n }\n]\n' > $d/expected $JQ --indent 6 -n "[{a:1}]" > $d/out cmp $d/out $d/expected +if ! $msys && ! $mingw; then + # Test handling of timezones -- #2429, #2475 + if ! r=$(TZ=Asia/Tokyo $JQ -rn '1731627341 | strflocaltime("%F %T %z %Z")') \ + || [ "$r" != "2024-11-15 08:35:41 +0900 JST" ]; then + echo "Incorrectly formatted local time" + exit 1 + fi + + if ! r=$(TZ=Europe/Paris $JQ -rn '1731627341 | strflocaltime("%F %T %z %Z")') \ + || [ "$r" != "2024-11-15 00:35:41 +0100 CET" ]; then + echo "Incorrectly formatted local time" + exit 1 + fi + + if ! r=$(TZ=Europe/Paris $JQ -rn '1731627341 | strftime("%F %T %z %Z")') \ + || ( [ "$r" != "2024-11-14 23:35:41 +0000 UTC" ] \ + && [ "$r" != "2024-11-14 23:35:41 +0000 GMT" ] ); then + echo "Incorrectly formatted universal time" + exit 1 + fi +fi + exit 0