21 February 2014

关于时间的迷惑

一个简单的“时间”包含很多假设

假设我在早上十点钟对一个同事说“我现在很忙,三点钟我再来找你”,几乎所有人都理 解我的意思。但实际上这个理解基于很多假设:

  1. 我说的是 “ 今天 三点”
  2. 我说的是 “今天 下午 三点” ,因为我们正常情况下不在半夜上班
  3. 我说的是 “ 北京时间 今天下午三点” ,因为我们住在中国

没有这些假设,单纯的“三点钟”是不能准确描述一个时刻的

同一时刻的不同表示

现在是下午四点钟,下面哪个时间戳是对的?

  1. 4:00 PM
  2. 8:00 AM
  3. 3:00 AM

不完整的表述没有意义。加上合适的补充信息就都是 同一时刻不同表 示

  1. 4:00 PM Beijing time
  2. 8:00 AM UTC time
  3. 3:00 AM US/Eastern time

计算机程序也有约定

$ date -d 'Feb 21 08:00:00 2014'
Fri, Feb 21, 2014  8:00:00 AM

如果没有明确指定, date 假定你输入的时间是 当前时区 的时间,而你也假定 date 打印的是当前时区的时间

假定的错位

以下是个常见的时间戳导致问题的场景:

  1. 记录时间戳的设备是北京时间(UTC+0800),时间戳也是北京时间。

    $ date -Iseconds
    2014-02-21T09:16:00+08:00
    
    $ echo "2014-02-21T09:16:18" > timestamp
    
  2. 读取时间戳的设备时区为悉尼时间(UTC+1100),所以它假定时间戳记录的是悉尼时 间。

    $  read t <timestamp; date -Iseconds -d "$t"
    2014-02-21T09:16:00+11:00
    
  3. 表面上看日期、时、分、秒都没错,但因为一个是北京时间、一个是悉尼夏令时,两 者实际相差3小时。都转换到同一个时区就一目了然了:原始时间其实是悉尼时间12 点16分,但读取出来把它当成悉尼时间9点16分了。

    $ date -d "2014-02-21T09:16:00+08:00"
    Fri Feb 21 12:16:00 AEDT 2014
    
    $ date -d "2014-02-21T09:16:00+11:00"
    Fri Feb 21 09:16:00 AEDT 2014
    
  4. 更正错误的假定

    错误的原因是时间戳没有指明时区。所以知道记录时间戳时用的是哪个时区,在读取 时间戳后把时区信息加上就行了。

    $ read t <timestamp; date -d "$t+08:00"
    Fri Feb 21 12:16:00 AEDT 2014
    

避免歧义

从上面讨论可以看出,时间戳的混乱主要来源于时区信息的缺失。所以理论上可以如下 解决:

  1. 显式约定时间戳的时区:理论上可以,但实际难以贯彻执行。这正是大部分时间疑问 的根源。
  2. 用相对 "epoch" 的秒:不容易出错,但可读性不强。

    因为 "epoch" 时刻(1970-01-01 00:00:00+0000)的描述是完整无歧异的(包含时 间、时区),所以“epoch之后N秒”的描述也是无歧义的。

    $ TZ=Asia/Shanghai date -d @1392969600
    Fri, Feb 21, 2014  4:00:00 PM
    
    $ TZ=UTC date -d @1392969600
    Fri, Feb 21, 2014  8:00:00 AM
    
    $ TZ=US/Eastern date -d @1392969600
    Fri, Feb 21, 2014  3:00:00 AM
    

    NOTE: UTC datetime: "UTC datetime" basically refers to a data type. This data type is usually a 64-bit integer that represents the number of milliseconds since Unix epoch ('1970-01-01 00:00:00+0000').

  3. 把时区信息包含在每一个时间戳里,即下面例子中的 +0800AEDT 。这是我 唯一推荐的做法,应该也是最普遍的做法。

    • 2014-02-21 16:00:00+0800:
    • Fri Feb 21 19:00:00 AEDT 2014

    NOTE: Date and Time Formats

Get a relative date

For GNU date

$ date
Sun, Dec 29, 2013 11:57:54 AM

$ date -d 'last fri'
Fri, Dec 27, 2013 12:00:00 AM

$ date -d 'next fri'
Fri, Jan 03, 2014 12:00:00 AM

$ date -d 'Dec 15 07:35:00+0800 2011 1 month ago'
Tue, Nov 15, 2011  7:35:00 AM

$ date -d '2 week 1 day ago '
Sat, Jan 11, 2014 12:08:08 PM

$ date -d '2 month' # <-- 2 months later, NOTE how leap month is dealt with
Sat, Mar 01, 2014 12:02:51 PM

Calculate days between two dates

$ echo $(( ($(date '+%s') - $(date '+%s' -d '2011-12-15'))/3600/24 ))
1757

$ date -I -d "@$(($(date '+%s') - 1757*3600*24))"
2011-12-15

$ date -d '1757 days ago'
Thu Dec 15 08:49:34 AEDT 2011

Convert between seconds from epoch and date

$ date '+%s' -d 'Dec 15 07:35:00 2011'
1323905700

$ date -d @1323905700
Thu Dec 15 07:35:00 CST 2011

Timezone, Daylight Saving Time, and offset

$ date -R
Sun, 29 Dec 2013 14:31:47 +0800     <=== +0800 : offet

$ date -Iseconds
2022-02-24T13:04:41+11:00           <=== +11:00 : offset

$ TZ=US/Eastern date +%Z:%z
EDT:-0400           <=== EDT: Daylight Saving in effect; -0400: offset

Note: due to daylight saving, a timezone (zoneinfo) may have different offset at different times:

$ TZ=Australia/Sydney date -d 2020-04-05T01:00:00 -R
Sun, 05 Apr 2020 01:00:00 +1100     <=== Timezone offset: 11 hours

$ TZ=Australia/Sydney date -d 2020-04-05T02:00:00 -R
Sun, 05 Apr 2020 02:00:00 +1000     <=== Timezone offset: becomes 10 hours

If you convert both time into UTC, you'll see that the difference between the two points in time is not 1 hour but 2 hours:

$ date -u -d "Sun, 05 Apr 2020 01:00:00 +1100"
Sat Apr  4 14:00:00 UTC 2020

$ date -u -d "Sun, 05 Apr 2020 02:00:00 +1000"
Sat Apr  4 16:00:00 UTC 2020

Therefore, given a timezone, when trying to find out the timezone offset, ensure it is the offset is calculated based on the exact point in time of concern.

Fun Fact While normally "Sydney time xxxx" defines a single point in time without any issue, "Sydney time Apr 5 02:00:00 2020" is ambiguous because it happens to fall in the one hour "overlap" of AEDT and AEST caused by the end of daylight saving time. You have to explicitly specify if it is AEDT or not.

Show the same time in different timezones

$ ls -l /etc/localtime
lrwxr-xr-x 1 root wheel 42 Jan 27 13:17 /etc/localtime -> /var/db/timezone/zoneinfo/Australia/Sydney

$ date -d '2014-02-21 19:00:00+1100'
Fri Feb 21 19:00:00 AEDT 2014

$ TZ=Asia/Shanghai date -d '2014-02-21 19:00:00+1100'
Fri Feb 21 16:00:00 CST 2014

$ TZ=UTC date -d '2014-02-21 19:00:00+1100'
Fri Feb 21 08:00:00 UTC 2014

$ TZ=-08 date -R -d '2014-02-21 19:00:00+1100'          # Note: the Posix "-08" equals to the ISO "+0800"
Fri, 21 Feb 2014 16:00:00 +0800

$ TZ=MY_TZ_NAME-08 date -d '2014-02-21 19:00:00+1100'   # Same as the above, but with customized timezone name
Fri Feb 21 16:00:00 MY_TZ_NAME 2014

Set timezone

  • set timezone in Linux:

    ls -l /etc/localtime
    

    It should link to (or a copy of) a file in /usr/share/zoneinfo

  • Or

    cat /etc/zoneinfo
    

Hardware clock

hwclock

This is the command we use to operate Hardware clock most of the time.

/sbin/hwclock --utc --show
/sbin/hwclock --localtime --show
/sbin/hwclock --utc --systohc --noadjfile
/sbin/hwclock --utc --hctosys --noadjfile

Hardware clock set to local/UTC time

sed -i 's/UTC=no/UTC=yes/' /etc/default/rcS

Or

echo "UTC=true" > /etc/sysconfig/clock


blog comments powered by Disqus