【问题标题】:Parsing a date string reliably in jq在 jq 中可靠地解析日期字符串
【发布时间】:2020-07-04 18:48:09
【问题描述】:

总体目标:使用 jq 将 GMT 中的字符串解析为时间,并将格式化时间和该时间的差异输出到“现在”。但是,jqs(1.6 版,Debian 测试)时区处理对我来说似乎很困惑:

$ jq --version
jq-1.6
$ date
Sa 4. Jul 19:36:08 BST 2020
$ echo '""' | jq 'now | strftime("%H:%M")'
"18:36"        // OK, strftime is supposed to give GMT
$ echo '""' | jq 'now | strflocaltime("%H:%M")'
"19:36"        // also OK, British Summer time is one hour ahead, strflocaltime should give local time
$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | strftime("%H:%M")'
"18:14"        // strptime parses GMT, so this is fine
$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | strflocaltime("%H:%M")'
"18:14"        // but why is this not 19:14?!
$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | mktime | strftime("%H:%M")'
"19:14"        // and why does "mktime" change things around?
$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | mktime | strflocaltime("%H:%M")'
"20:14"       // and why does strflocaltime kick in after, but not before mktime?
$ echo '"2020-07-04T18:14:12Z"' | jq 'fromdate | strftime("%H:%M")'
"19:14"       // I thought fromdate was synonymous to strptime?
$ echo '"2020-07-04T18:14:12Z"' | jq 'fromdate | strflocaltime("%H:%M")'
"20:14"       // I suppose this is the same issue as above with mktime

更长的版本:我正在使用 API 来显示附近火车站的到达时间,特别是我想显示接下来的几列火车以及从现在开始它们将离开的分钟数。我想使用jq 来解析该数据。数据包含格式为"2020-07-04T18:14:12Z" 的时间字符串。我的理解是,jq 中的 fromdatestrptime 都应该将该数据解析为 GMT 时间戳(来自手册页:“在所有情况下,这些内置函数都专门处理 UTC 时间。”,手册页似乎可互换使用 GMT 和 UTC),jq 内的任何操作都使用 UTC,如果使用 strflocaltime,则只有最终输出位于本地时区。

但是,考虑到jq 的输出和上面显示的各种输入,这种理解肯定是错误的。特别是,我不明白如何正确可靠地将时间字符串解析为 GMT 时间戳,b) 一旦完成,fromdatemktimenowstrptime 的输出分别有何不同当传递到strf[local]time 以产生上面看到的输出数组时。

编辑:进一步研究并使用前两个答案中的信息,主要问题似乎是fromdate 的夏令时应用(或不应用)取决于TZ 环境变量的设置:

$ TZ=BST jq -n '"2020-07-05T07:38:57Z" | fromdate'
1593934737
$ TZ=Etc/UTC jq -n '"2020-07-05T07:38:57Z" | fromdate'
1593934737
$ TZ=Europe/London jq -n '"2020-07-05T07:38:57Z" | fromdate'
1593938337
$ TZ=Asia/Tokyo jq -n '"2020-07-05T07:38:57Z" | fromdate'
1593934737
$ TZ=America/Los_Angeles jq -n '"2020-07-05T07:38:57Z" | fromdate'
1593938337
$ TZ=Asia/Kathmandu jq -n '"2020-07-05T07:38:57Z" | fromdate'
1593934737
$ unset TZ; jq -n '"2020-07-05T07:38:57Z" | fromdate'
1593938337

请注意,伦敦、洛杉矶和未设置的 TZ 获得的 Unix 纪元时间戳与东京、加德满都、UTC 和(我认为格式错误?)BST 不同。我相信这不应该发生,因为时间戳应该与时区无关。不幸的是,目前它似乎忽略了永久时区偏移(东京和加德满都给出与 UTC 相同的结果,两者都没有 DST)但它确实考虑了 DST,除非在不遵守 DST 的时区运行。

strflocaltime,当给定时间戳时,似乎根据TZ 的当前值应用永久和 DST 时区更正。

不幸的是,这似乎暗示我首先需要将 TZ 设置为 Etc/Utc 以使 fromdate 正常运行,然后当我想打印本地时间时,我需要将 TZ 重新设置为本地时区。

【问题讨论】:

  • 我无法回答“为什么”部分,但这表明函数已编写,strptime(fmt) 仅用于解析(验证),如果输入符合请求的格式并且如果通过,请使用mktime 进行进一步的自定义。我想,没有它,strptime(fmt) 的输出就无法重用

标签: timezone jq


【解决方案1】:

我想在这里开始构建一个答案,结合不同的块:

首先,mktime 在采用“分解时间结构”时考虑了 DST,但没有考虑其他时区信息:

$ TZ=Etc/Utc jq -n '[2020,6,5,7,38,57,0,186] | mktime'
1593934737
$ TZ=Europe/London jq -n '[2020,6,5,7,38,57,0,186] | mktime'
1593938337
$ TZ=America/Los_Angeles jq -n '[2020,6,5,7,38,57,0,186] | mktime'
1593938337
$ TZ=Asia/Tokyo jq -n '[2020,6,5,7,38,57,0,186] | mktime'
1593934737
$ TZ=Asia/Kathmandu jq -n '[2020,6,5,7,38,57,0,186] | mktime'
1593934737
$ unset TZ; jq -n '[2020,6,5,7,38,57,0,186] | mktime'
1593938337

请注意,仅有的两个输出是 1593934737 或 1593938337,两者之差正好是 3600。

其次,fromdate 等同于strptime() | mktime

第三,strflocaltime 将时区偏移(永久和 DST)应用于 unix 时间戳输入,但不应用于分解时间输入:

$ TZ='Europe/London' jq -n '[2020,6,5,7,38,57,0,186] | strflocaltime("%H:%M")'
"07:38"
$ TZ='Asia/Tokyo' jq -n '[2020,6,5,7,38,57,0,186] | strflocaltime("%H:%M")'
"07:38"
$ TZ='Europe/London' jq -n '1593934737 | strflocaltime("%H:%M")'
"08:38"
$ TZ='Asia/Tokyo' jq -n '1593934737 | strflocaltime("%H:%M")'
"16:38"

第四,now 产生一个 unix-timestamp 输出,它会受到strflocaltime 的调整的影响。

按顺序复习我原来引起混淆的顺序:

$ echo '""' | jq 'now | strftime("%H:%M")'
"18:36"        // OK, strftime is supposed to give GMT
$ echo '""' | jq 'now | strflocaltime("%H:%M")'
"19:36"        // also OK, British Summer time is one hour ahead, strflocaltime should give local time

上面的 (3) 和 (4) 对此进行了解释:now 生成一个 unix 时间戳,strflocaltime 将其调整为本地时间。

$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | strftime("%H:%M")'
"18:14"        // strptime parses GMT, so this is fine
$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | strflocaltime("%H:%M")'
"18:14"        // but why is this not 19:14?!

这里,strptime 产生了一个不被strflocaltime 调整的细分时间,通过上面的(3)。

$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | mktime | strftime("%H:%M")'
"19:14"        // and why does "mktime" change things around?
$ echo '"2020-07-04T18:14:12Z"' | jq 'strptime("%Y-%m-%dT%H:%M:%SZ") | mktime | strflocaltime("%H:%M")'
"20:14"       // and why does strflocaltime kick in after, but not before mktime?

strptime 产生故障时间,mktime 理论上应该将其转换为 unix-timestamp 时间,假设它是 UTC 时间,但mktime 错误地应用了一小时 DST 偏移量(上面的 (1) ),导致strftime 产生(意外正确的)本地时间和strflocaltime - 校正永久和 DST 偏移量(通过上述 (3) ) - 再给出一个(总共两个)小时偏移量。

$ echo '"2020-07-04T18:14:12Z"' | jq 'fromdate | strftime("%H:%M")'
"19:14"       // I thought fromdate was synonymous to strptime?
$ echo '"2020-07-04T18:14:12Z"' | jq 'fromdate | strflocaltime("%H:%M")'
"20:14"       // I suppose this is the same issue as above with mktime

这只是 (2) 的结果,fromdate 在内部使用 mktime

编译master分支上的最新提交(a17dd32),这个问题不再出现,因为mktime不再应用一小时偏移量。这可能是由于提交 3c5b1419

作为一种临时解决方法,我们可以通过jq -n 'now | gmtime | mktime - (now | trunc)' 获得mktime 引入的偏移量。从fromdate 的任何出现中减去此偏移量,将可靠地产生 UTC 时间戳。

【讨论】:

  • 在 1.6 后的世界里,mktime takes into account DST 已经不是这样了。至少不是在 MacOS 上。那是(显然?)一个错误,尽管它可能已经存在了很长时间。
【解决方案2】:

strflocaltime/1 的行为会根据其输入的类型而变化。

如果输入是一个数组(“分解时间”,这是strptime 返回的内容),strflocaltime 不会针对时区和任何季节性时间调整进行更正。

$ TZ=UTC jq -n '[1970,0,1,0,0,1,4,0] | strflocaltime("%H")'
"00"
$ TZ=EST jq -n '[1970,0,1,0,0,1,4,0] | strflocaltime("%H")'
"00"

但是,如果输入是一个数字(自 Unix 纪元以来的秒数,这是 mktime 返回的值),strflocaltime 首先将其提供给 localtime 以获得细分时间;和localtime 执行此类更正。

$ TZ=UTC jq -n '1 | strflocaltime("%H")'
"00"
$ TZ=EST jq -n '1 | strflocaltime("%H")'
"19"

在这两种情况下,strftime 都使用分解的时间结构调用,并返回结果字符串。

【讨论】:

    【解决方案3】:

    这可能不是您正在寻找的答案,但它可能会解决一些问题。 builtin.jq 定义

    def fromdateiso8601: strptime("%Y-%m-%dT%H:%M:%SZ")|mktime;
    def todateiso8601: strftime("%Y-%m-%dT%H:%M:%SZ");
    def fromdate: fromdateiso8601;
    def todate: todateiso8601;
    

    以下测试脚本

    #!/bin/bash
    echo '"2020-07-04T18:14:12Z"' | jq -cr '
      def strptime_:  strptime("%Y-%m-%dT%H:%M:%SZ") ;
      def hour:       strftime("%H") ;
        ".                                    \(.)"
      , ". | strptime_                        \(strptime_)"
      , ". | fromdate                         \(fromdate)"  
      , ". | fromdate | todate                \(fromdate | todate)" 
      , ". | fromdate | hour                  \(fromdate | hour)"   
    '
    

    显示在我的 mac(运行 jq 1.6)上,%H strftime 说明符似乎对 TZ 的设置很敏感。

    没有明确设置TZ(我系统的时区是太平洋夏令时)我观察

    bash-3.2$ ./test.sh
    .                                    2020-07-04T18:14:12Z
    . | strptime_                        [2020,6,4,18,14,12,6,185]
    . | fromdate                         1593890052
    . | fromdate | todate                2020-07-04T19:14:12Z
    . | fromdate | hour                  19
    

    将 TZ 显式设置为 America/Los_Angeles 会产生相同的输出

    bash-3.2$ env TZ=America/Los_Angeles ./test.sh
    .                                    2020-07-04T18:14:12Z
    . | strptime_                        [2020,6,4,18,14,12,6,185]
    . | fromdate                         1593890052
    . | fromdate | todate                2020-07-04T19:14:12Z
    . | fromdate | hour                  19
    

    但将 TZ 显式设置为 Etc/UTC 会产生不同的小时

    bash-3.2$ env TZ=Etc/UTC ./test.sh
    .                                    2020-07-04T18:14:12Z
    . | strptime_                        [2020,6,4,18,14,12,6,185]
    . | fromdate                         1593886452
    . | fromdate | todate                2020-07-04T18:14:12Z
    . | fromdate | hour                  18
    

    我觉得奇怪的是 strptime 的值与 struct tm 并不完全相同,因此深入研究 builtin.c 会发现一些重要的特定于平台的细节以及 jv2tm,它揭示了来自 @ 的映射987654333@ 到 json 数组 strptime 返回。

    static int jv2tm(jv a, struct tm *tm) {
      memset(tm, 0, sizeof(*tm));
      TO_TM_FIELD(tm->tm_year, a, 0);
      tm->tm_year -= 1900;
      TO_TM_FIELD(tm->tm_mon,  a, 1);
      TO_TM_FIELD(tm->tm_mday, a, 2);
      TO_TM_FIELD(tm->tm_hour, a, 3);
      TO_TM_FIELD(tm->tm_min,  a, 4);
      TO_TM_FIELD(tm->tm_sec,  a, 5);
      TO_TM_FIELD(tm->tm_wday, a, 6);
      TO_TM_FIELD(tm->tm_yday, a, 7);
      jv_free(a);
    
      // 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;
    
      // 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.
    
      return 1;
    }
    

    【讨论】:

    • 谢谢你,背景信息当然很感激!
    【解决方案4】:

    我已将时区设置为欧洲/阿姆斯特丹 (+1)。

    使用 JQ 1.6:

    这是预期的:

    $ echo '"2020-03-28T11:04:04Z"' | jq 'fromdate | strflocaltime("%H:%M (%Z)")'
    $ "12:04 CET"
    

    这不是预期的:

    $ echo '"2020-03-29T11:04:04Z"' | jq 'fromdate | strflocaltime("%H:%M (%Z)")'
    $ "14:04 CET"
    

    人们会认为“2020-03-29 11:04”的时间是“13:04 CET”,夏令时 +1, 但它给了我“14:04 CET”?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-21
      • 1970-01-01
      • 2012-07-21
      • 1970-01-01
      相关资源
      最近更新 更多