如何计算两个日期之间的天数
计算两个日期之间的天数很实用,我一般用 sql:
SELECT DATEDIFF("2089-10-01","2008-08-08") AS "北京奥运会开幕式天数"
如果用 Go 计算两个日期之间的天数,可以使用time
包。以下是步骤和相应的代码示例:
解析日期:需要先将输入的日期字符串转换为
time.Time
类型。可以通过time.Parse
函数来实现,它接受日期格式和日期字符串作为参数。计算时间差:使用两个
time.Time
对象,可以通过调用它们之间的Sub
方法来计算它们的时间差。这将返回一个time.Duration
类型的值。转换为天数:
time.Duration
类型可以被转换为天数。由于time.Duration
的基本单位是纳秒,因此需要通过将其除以每天的纳秒数(24 小时 * 60 分钟 * 60 秒 * 1000000000 纳秒)来转换为天数。
相应的 Go 代码示例:
输出:
Days between 2008-08-08 and 2089-10-01: 29639
代码中daysBetweenDates
函数接受两个日期字符串,将它们解析为 time.Time
对象,然后计算它们之间的差异,并将这个差异转换为天数。
如何实现的呢...
调试以上代码:
在 sub 中的d := Duration(t.sec()-u.sec())*Second + Duration(t.nsec()-u.nsec())
计算出来两个日期之间的差值
因为 t.wall 为 0, hasMonotonic 常量为1 << 63
,故而 0&1 << 63
值为 0
<font size=1 color="orange">
在计算机中,"&" 是位运算符,表示按位与操作。"<<" 是位运算符,表示左移操作。在表达式 "0 & 1 << 63" 中,数字 0 表示二进制的"00000000",数字 1 表示二进制的"00000001"。
首先进行左移操作,将数字 1 向左移动 63 位得到结果:
1 << 63 = 2^63 = 9,223,372,036,854,775,808
然后进行按位与操作,将左移的结果与数字 0 进行按位与运算:
9,223,372,036,854,775,808 & 0 = 0
故而,"0 & 1 << 63" 的值为 0。
</font>
所以不会走到 return wallToInternal + int64(t.wall<<1>>(nsecShift+1))
的逻辑,而是返回t.ext
<font size=1 color="orange">
其中常量 wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
secondsPerDay 为 86400
</font>
<br>
那接下来需要深入领会一下 Time 结构体的 ext 字段的意义:
go/src/time/time.go time结构体的ext字段
Go 语言time
包中,Time
结构体用于表示一个时间点,具有纳秒精度。Time
结构体中的wall
和ext
字段共同编码了时间的信息,其中ext
字段具有特定的含义和作用:
ext
字段含义:ext
字段是一个 64 位的有符号整数(int64
),它的作用依赖于wall
字段中的hasMonotonic
位的状态:如果
hasMonotonic
位为 0(表示没有单调时钟读数),ext
字段存储的是自公元 1 年 1 月 1 日起的完整的墙上时钟(wall clock)秒数。这意味着,当没有单调时钟读数时,ext
用于表示时间点的秒数。如果
hasMonotonic
位为 1(表示存在单调时钟读数),ext
字段则存储自进程启动以来的单调时钟读数,单位为纳秒。这种情况下,ext
提供了用于比较或减法运算的额外精度,因为单调时钟保证了时间的前后顺序,即使系统时间被修改。如何得到
ext
:当创建一个
time.Time
实例时,如果包含了单调时钟的读数,ext
字段会被自动设置为自进程启动以来的单调时钟读数。这通常在内部通过调用某些time
包的函数来实现,如time.Now()
,它会捕获当前的墙上时钟时间和单调时钟时间。如果单调时钟读数不被包含,
ext
字段则表示自公元 1 年 1 月 1 日起至该时间点的总秒数,这通常在需要将时间转换为 UTC 或其他没有单调时间参考的操作中显式设置。
ext
字段的设计目的是为了在Time
值中提供足够的信息来支持不同的时间操作,包括时间点的比较、持续时间的计算以及时间的序列化与反序列化。单调时钟读数的引入是为了在一些特定的场景下提供更可靠的时间比较方法,避免系统时间的调整对时间逻辑产生影响。
<br>
此时 d 也就是(65914560000-63353750400)=2560809600 秒,
其中这两个数是各自日期距离公元 1 年 1 月 1 日 0 点 0 分 0 秒的秒数
(其实会精确到纳秒,此处省略了后面的 9 个 0)
也就是 711336h0m0s,再除以 24,就得到了天数
<br>
此处需要看下,ext 如何得到的~
打断点如下:
走到了很长的 parse 函数,继续追加断点:
<br>
最终到了 Date()函数中, 继续追加断点
在最后t := unixTime(unix, int32(nsec))
中 ext 字段被赋值
继续对 unixTime 打断点,
其中,第一个字段 sec,即 Date()函数中的 unix,代表的是自 1970 年 1 月 1 日 00:00:00 UTC 以来的秒数,也就是第一个日期,2008-08-08 00:00:00 的 Unix 时间戳
其计算过程如下, 可以略过:
<font size=1 color=blue>
计算自绝对纪元以来的天数 (
d
): 首先,代码通过daysSinceEpoch(year)
函数计算出给定年份自绝对纪元(公历纪年的开始)以来的天数。然后,根据月份和是否为闰年调整这个天数,包括在月份之前的所有天数和当前月份中的天数(通过day - 1
计算,因为天数是从 1 开始的)。将天数转换为秒 (
abs
): 计算出的天数乘以每天的秒数(secondsPerDay
),加上当前天中已经过去的小时、分钟和秒数所对应的秒数,得到abs
。这个值是自绝对纪元以来的总秒数。调整到 Unix 时间戳 (
unix
): 计算出的秒数需要经过两个步骤的调整才能转换为 Unix 时间戳:首先,通过
absoluteToInternal + internalToUnix
调整。这里的absoluteToInternal
是绝对时间到内部时间表示的偏移量,internalToUnix
是内部时间表示到 Unix 时间戳的偏移量。这些偏移量是为了在不同的时间表示法之间进行转换。然后,需要根据时间所在的时区进行调整。代码首先尝试使用
unix
时间戳来查找时区偏移量(offset
),如果这个时间戳正好在时区变更的边缘,那么它会根据 UTC 时间(unix - offset
)再次查找正确的偏移量,并使用这个偏移量来更新unix
时间戳,确保unix
变量代表的是 UTC 时间。
通过这些步骤,unix
变量最终得到的是一个表示指定日期和时间(考虑了时区偏移)的 Unix 时间戳。
</font>
<br>
而
unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
<br>
关于 1969*365 + 1969/4 - 1969/100 + 1969/400
, 是用来计算格里高利历(Gregorian calendar)下,从 0 年 1 月 1 日到给定年份(此处应该是到 1970 年,因为公元前 1 年的话是 0)的总天数。这个计算基于格里高利历(该历法是当前国际上最广泛使用的日历体系)的规则。公式的组成部分如下:
1969*365
:计算给定年份之前的所有年份中的天数,假设每年都是 365 天。1969/4
:每四年有一个闰年,闰年有 366 天。这部分计算从 0 年到 1968 年间包含的闰年数量,因为每个闰年会多出一天。- 1969/100
:格里高利历规则中,每 100 年会跳过一个闰年(即那一年不作为闰年),这部分减去这些年份中多计算的天数。+ 1969/400
:然而,每 400 年会将本该跳过的闰年加回来(即那一年作为闰年),这部分加上这些年份中应该加回的天数。
<br>
即 (1969*365 + 1969/4 - 1969/100 + 1969/400)
这个公式用于计算从公元 0 年 1 月 1 日到给定年份(公元前 1 年算作年份 0,公元 1 年为年份 1,以此类推)的累计天数,考虑了闰年的影响
再乘以 86400 秒,即从公元 0 年 1 月 1 日 00:00:00 到 1970-01-01 00:00:00 的秒数
所以sec + unixToInternal
,即 2008-08-08 00:00:00 到 1970-01-01 00:00:00 的秒数,再加上 1970-01-01 00:00:00 到公元 0 年 1 月 1 日 00:00:00 的秒数,也就是 2008-08-08 00:00:00 到公元 0 年 1 月 1 日 00:00:00 的秒数
<br>
很多项目中都有对该公式的直接使用
例如:
google/cel-go/common/types/timestamp.go
<br><br>
其中常量 wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
而至于上面提到的其中常量
wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
,
为什么选 1885 年,可参考前作 [Wall Clock 与 Monotonic Clock]( "Wall Clock 与 Monotonic Clock")
<br>
更多参考:
1969*365 + 1969/4 - 1969/100 + 1969/400--谷歌搜索结果
1969*365 + 1969/4 - 1969/100 + 1969/400--谷歌图书搜索结果
<br>
评论