如何计算两个日期之间的天数

计算两个日期之间的天数很实用,我一般用sq

SELECT DATEDIFF("2089-10-01","2008-08-08") AS "北京奥运会开幕式天数"

如果用Go计算两个日期之间的天数,可以使用time 包。以下是步骤和相应的代码示例:

  1. 解析日期:需要先将输入的日期字符串转换为 time.Time 类型。可以通过 time.Parse 函数来实现,它接受日期格式和日期字符串作为参数。
  2. 计算时间差:使用两个 time.Time 对象,可以通过调用它们之间的 Sub 方法来计算它们的时间差。这将返回一个 time.Duration 类型的值。
  3. 转换为天数time.Duration 类型可以被转换为天数。由于 time.Duration 的基本单位是纳秒,因此需要通过将其除以每天的纳秒数(24小时 * 60分钟 * 60秒 * 1000000000纳秒)来转换为天数。

相应的 Go 代码示例:

代码语言:javascript
复制
package main

import (
"fmt"
"time"
)

// 计算两个日期之间的天数差
func daysBetweenDates(date1, date2 string) (int, error) {
// 定义日期格式
const layout = "2006-01-02"

// 解析第一个日期
t1, err := time.Parse(layout, date1)
if err != nil {
return 0, err
}

// 解析第二个日期
t2, err := time.Parse(layout, date2)
if err != nil {
return 0, err
}

// 计算日期差
duration := t2.Sub(t1)

// 转换为天数
days := int(duration.Hours() / 24)

return days, nil
}

func main() {
date1 := "2008-08-08"
date2 := "2089-10-01"

days, err := daysBetweenDates(date1, date2)
if err != nil {
fmt.Println("Error:", err)
return
}

fmt.Printf("Days between %s and %s: %d\n", date1, date2, days)
}

在线执行[1]

输出:

Days between 2008-08-08 and 2089-10-01: 29639

代码中daysBetweenDates 函数接受两个日期字符串,将它们解析为 time.Time 对象,然后计算它们之间的差异,并将这个差异转换为天数。

如何实现的呢...

src/time/time.go:453[2]

调试以上代码:

在sub中的d := Duration(t.sec()-u.sec())*Second + Duration(t.nsec()-u.nsec()) 计算出来两个日期之间的差值

代码语言:javascript
复制
// sec returns the time's seconds since Jan 1 year 1.
func (t Time) sec() int64 {
if t.wall&hasMonotonic != 0 {
return wallToInternal + int64(t.wall<<1>>(nsecShift+1))
}
return t.ext
}

因为t.wall为0, hasMonotonic常量为1 << 63,故而 0&1 << 63值为0

在计算机中,"&" 是位运算符,表示按位与操作。"<<" 是位运算符,表示左移操作。在表达式 "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。

所以不会走到 return wallToInternal + int64(t.wall<<1>>(nsecShift+1))的逻辑,而是返回t.ext

其中常量 wallToInternal[3] int64 = (1884365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay

secondsPerDay为86400

那接下来需要深入领会一下Time结构体的ext字段的意义:

go/src/time/time.go time结构体的ext字段[4]

Go语言time包中,Time结构体用于表示一个时间点,具有纳秒精度。Time结构体中的wallext字段共同编码了时间的信息,其中ext字段具有特定的含义和作用:

  1. ext字段含义ext字段是一个64位的有符号整数(int64),它的作用依赖于wall字段中的hasMonotonic位的状态:
    • 如果hasMonotonic位为0(表示没有单调时钟读数),ext字段存储的是自公元1年1月1日起的完整的墙上时钟(wall clock)秒数。这意味着,当没有单调时钟读数时,ext用于表示时间点的秒数。
    • 如果hasMonotonic位为1(表示存在单调时钟读数),ext字段则存储自进程启动以来的单调时钟读数,单位为纳秒。这种情况下,ext提供了用于比较或减法运算的额外精度,因为单调时钟保证了时间的前后顺序,即使系统时间被修改。
  2. 如何得到ext
    • 当创建一个time.Time实例时,如果包含了单调时钟的读数,ext字段会被自动设置为自进程启动以来的单调时钟读数。这通常在内部通过调用某些time包的函数来实现,如time.Now(),它会捕获当前的墙上时钟时间和单调时钟时间。
    • 如果单调时钟读数不被包含,ext字段则表示自公元1年1月1日起至该时间点的总秒数,这通常在需要将时间转换为UTC或其他没有单调时间参考的操作中显式设置。

ext字段的设计目的是为了在Time值中提供足够的信息来支持不同的时间操作,包括时间点的比较、持续时间的计算以及时间的序列化与反序列化。单调时钟读数的引入是为了在一些特定的场景下提供更可靠的时间比较方法,避免系统时间的调整对时间逻辑产生影响。

此时d也就是(65914560000-63353750400)=2560809600秒,

其中这两个数是各自日期距离公元1年1月1日0点0分0秒的秒数

(其实会精确到纳秒,此处省略了后面的9个0)

也就是711336h0m0s,再除以24,就得到了天数

此处需要看下,ext如何得到的~

打断点如下:

走到了很长的parse函数,继续追加断点:

代码语言:javascript
复制
func parse(layout, value string, defaultLocation, local *Location) (Time, error) {
alayout, avalue := layout, value
rangeErrString := "" // set if a value is out of range
amSet := false // do we need to subtract 12 from the hour for midnight?
pmSet := false // do we need to add 12 to the hour?

// Time being constructed.
var (
year int
month int = -1
day int = -1
yday int = -1
hour int
min int
sec int
nsec int
z *Location
zoneOffset int = -1
zoneName string
)

// Each iteration processes one std value.
for {
var err error
prefix, std, suffix := nextStdChunk(layout)
stdstr := layout[len(prefix) : len(layout)-len(suffix)]
value, err = skip(value, prefix)
if err != nil {
return Time{}, newParseError(alayout, avalue, prefix, value, "")
}
if std == 0 {
if len(value) != 0 {
return Time{}, newParseError(alayout, avalue, "", value, ": extra text: "+quote(value))
}
break
}
layout = suffix
var p string
hold := value
switch std & stdMask {
case stdYear:
if len(value) < 2 {
err = errBad
break
}
p, value = value[0:2], value[2:]
year, err = atoi(p)
if err != nil {
break
}
if year >= 69 { // Unix time starts Dec 31 1969 in some time zones
year += 1900
} else {
year += 2000
}
case stdLongYear:
if len(value) < 4 || !isDigit(value, 0) {
err = errBad
break
}
p, value = value[0:4], value[4:]
year, err = atoi(p)
case stdMonth:
month, value, err = lookup(shortMonthNames, value)
month++
case stdLongMonth:
month, value, err = lookup(longMonthNames, value)
month++
case stdNumMonth, stdZeroMonth:
month, value, err = getnum(value, std == stdZeroMonth)
if err == nil && (month <= 0 || 12 < month) {
rangeErrString = "month"
}
case stdWeekDay:
// Ignore weekday except for error checking.
_, value, err = lookup(shortDayNames, value)
case stdLongWeekDay:
_, value, err = lookup(longDayNames, value)
case stdDay, stdUnderDay, stdZeroDay:
if std == stdUnderDay && len(value) > 0 && value[0] == ' ' {
value = value[1:]
}
day, value, err = getnum(value, std == stdZeroDay)
// Note that we allow any one- or two-digit day here.
// The month, day, year combination is validated after we've completed parsing.
case stdUnderYearDay, stdZeroYearDay:
for i := 0; i < 2; i++ {
if std == stdUnderYearDay && len(value) > 0 && value[0] == ' ' {
value = value[1:]
}
}
yday, value, err = getnum3(value, std == stdZeroYearDay)
// Note that we allow any one-, two-, or three-digit year-day here.
// The year-day, year combination is validated after we've completed parsing.
case stdHour:
hour, value, err = getnum(value, false)
if hour < 0 || 24 <= hour {
rangeErrString = "hour"
}
case stdHour12, stdZeroHour12:
hour, value, err = getnum(value, std == stdZeroHour12)
if hour < 0 || 12 < hour {
rangeErrString = "hour"
}
case stdMinute, stdZeroMinute:
min, value, err = getnum(value, std == stdZeroMinute)
if min < 0 || 60 <= min {
rangeErrString = "minute"
}
case stdSecond, stdZeroSecond:
sec, value, err = getnum(value, std == stdZeroSecond)
if err != nil {
break
}
if sec < 0 || 60 <= sec {
rangeErrString = "second"
break
}
// Special case: do we have a fractional second but no
// fractional second in the format?
if len(value) >= 2 && commaOrPeriod(value[0]) && isDigit(value, 1) {
_, std, _ = nextStdChunk(layout)
std &= stdMask
if std == stdFracSecond0 || std == stdFracSecond9 {
// Fractional second in the layout; proceed normally
break
}
// No fractional second in the layout but we have one in the input.
n := 2
for ; n < len(value) && isDigit(value, n); n++ {
}
nsec, rangeErrString, err = parseNanoseconds(value, n)
value = value[n:]
}
case stdPM:
if len(value) < 2 {
err = errBad
break
}
p, value = value[0:2], value[2:]
switch p {
case "PM":
pmSet = true
case "AM":
amSet = true
default:
err = errBad
}
case stdpm:
if len(value) < 2 {
err = errBad
break
}
p, value = value[0:2], value[2:]
switch p {
case "pm":
pmSet = true
case "am":
amSet = true
default:
err = errBad
}
case stdISO8601TZ, stdISO8601ColonTZ, stdISO8601SecondsTZ, stdISO8601ShortTZ, stdISO8601ColonSecondsTZ, stdNumTZ, stdNumShortTZ, stdNumColonTZ, stdNumSecondsTz, stdNumColonSecondsTZ:
if (std == stdISO8601TZ || std == stdISO8601ShortTZ || std == stdISO8601ColonTZ) && len(value) >= 1 && value[0] == 'Z' {
value = value[1:]
z = UTC
break
}
var sign, hour, min, seconds string
if std == stdISO8601ColonTZ || std == stdNumColonTZ {
if len(value) < 6 {
err = errBad
break
}
if value[3] != ':' {
err = errBad
break
}
sign, hour, min, seconds, value = value[0:1], value[1:3], value[4:6], "00", value[6:]
} else if std == stdNumShortTZ || std == stdISO8601ShortTZ {
if len(value) < 3 {
err = errBad
break
}
sign, hour, min, seconds, value = value[0:1], value[1:3], "00", "00", value[3:]
} else if std == stdISO8601ColonSecondsTZ || std == stdNumColonSecondsTZ {
if len(value) < 9 {
err = errBad
break
}
if value[3] != ':' || value[6] != ':' {
err = errBad
break
}
sign, hour, min, seconds, value = value[0:1], value[1:3], value[4:6], value[7:9], value[9:]
} else if std == stdISO8601SecondsTZ || std == stdNumSecondsTz {
if len(value) < 7 {
err = errBad
break
}
sign, hour, min, seconds, value = value[0:1], value[1:3], value[3:5], value[5:7], value[7:]
} else {
if len(value) < 5 {
err = errBad
break
}
sign, hour, min, seconds, value = value[0:1], value[1:3], value[3:5], "00", value[5:]
}
var hr, mm, ss int
hr, _, err = getnum(hour, true)
if err == nil {
mm, _, err = getnum(min, true)
}
if err == nil {
ss, _, err = getnum(seconds, true)
}
zoneOffset = (hr*60+mm)*60 + ss // offset is in seconds
switch sign[0] {
case '+':
case '-':
zoneOffset = -zoneOffset
default:
err = errBad
}
case stdTZ:
// Does it look like a time zone?
if len(value) >= 3 && value[0:3] == "UTC" {
z = UTC
value = value[3:]
break
}
n, ok := parseTimeZone(value)
if !ok {
err = errBad
break
}
zoneName, value = value[:n], value[n:]

case stdFracSecond0:
// stdFracSecond0 requires the exact number of digits as specified in
// the layout.
ndigit := 1 + digitsLen(std)
if len(value) < ndigit {
err = errBad
break
}
nsec, rangeErrString, err = parseNanoseconds(value, ndigit)
value = value[ndigit:]

case stdFracSecond9:
if len(value) < 2 || !commaOrPeriod(value[0]) || value[1] < '0' || '9' < value[1] {
// Fractional second omitted.
break
}
// Take any number of digits, even more than asked for,
// because it is what the stdSecond case would do.
i := 0
for i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' {
i++
}
nsec, rangeErrString, err = parseNanoseconds(value, 1+i)
value = value[1+i:]
}
if rangeErrString != "" {
return Time{}, newParseError(alayout, avalue, stdstr, value, ": "+rangeErrString+" out of range")
}
if err != nil {
return Time{}, newParseError(alayout, avalue, stdstr, hold, "")
}
}
if pmSet && hour < 12 {
hour += 12
} else if amSet && hour == 12 {
hour = 0
}

// Convert yday to day, month.
if yday >= 0 {
var d int
var m int
if isLeap(year) {
if yday == 31+29 {
m = int(February)
d = 29
} else if yday > 31+29 {
yday--
}
}
if yday < 1 || yday > 365 {
return Time{}, newParseError(alayout, avalue, "", value, ": day-of-year out of range")
}
if m == 0 {
m = (yday-1)/31 + 1
if int(daysBefore[m]) < yday {
m++
}
d = yday - int(daysBefore[m-1])
}
// If month, day already seen, yday's m, d must match.
// Otherwise, set them from m, d.
if month >= 0 && month != m {
return Time{}, newParseError(alayout, avalue, "", value, ": day-of-year does not match month")
}
month = m
if day >= 0 && day != d {
return Time{}, newParseError(alayout, avalue, "", value, ": day-of-year does not match day")
}
day = d
} else {
if month < 0 {
month = int(January)
}
if day < 0 {
day = 1
}
}

// Validate the day of the month.
if day < 1 || day > daysIn(Month(month), year) {
return Time{}, newParseError(alayout, avalue, "", value, ": day out of range")
}

if z != nil {
return Date(year, Month(month), day, hour, min, sec, nsec, z), nil
}

if zoneOffset != -1 {
t := Date(year, Month(month), day, hour, min, sec, nsec, UTC)
t.addSec(-int64(zoneOffset))

// Look for local zone with the given offset.
// If that zone was in effect at the given time, use it.
name, offset, _, _, _ := local.lookup(t.unixSec())
if offset == zoneOffset && (zoneName == "" || name == zoneName) {
t.setLoc(local)
return t, nil
}

// Otherwise create fake zone to record offset.
zoneNameCopy := cloneString(zoneName) // avoid leaking the input value
t.setLoc(FixedZone(zoneNameCopy, zoneOffset))
return t, nil
}

if zoneName != "" {
t := Date(year, Month(month), day, hour, min, sec, nsec, UTC)
// Look for local zone with the given offset.
// If that zone was in effect at the given time, use it.
offset, ok := local.lookupName(zoneName, t.unixSec())
if ok {
t.addSec(-int64(offset))
t.setLoc(local)
return t, nil
}

// Otherwise, create fake zone with unknown offset.
if len(zoneName) > 3 && zoneName[:3] == "GMT" {
offset, _ = atoi(zoneName[3:]) // Guaranteed OK by parseGMT.
offset *= 3600
}
zoneNameCopy := cloneString(zoneName) // avoid leaking the input value
t.setLoc(FixedZone(zoneNameCopy, offset))
return t, nil
}

// Otherwise, fall back to default.
return Date(year, Month(month), day, hour, min, sec, nsec, defaultLocation), nil
}

最终到了Date()函数中, 继续追加断点

代码语言:javascript
复制
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {
if loc == nil {
panic("time: missing Location in call to Date")
}

// Normalize month, overflowing into year.
m := int(month) - 1
year, m = norm(year, m, 12)
month = Month(m) + 1

// Normalize nsec, sec, min, hour, overflowing into day.
sec, nsec = norm(sec, nsec, 1e9)
min, sec = norm(min, sec, 60)
hour, min = norm(hour, min, 60)
day, hour = norm(day, hour, 24)

// Compute days since the absolute epoch.
d := daysSinceEpoch(year)

// Add in days before this month.
d += uint64(daysBefore[month-1])
if isLeap(year) && month >= March {
d++ // February 29
}

// Add in days before today.
d += uint64(day - 1)

// Add in time elapsed today.
abs := d * secondsPerDay
abs += uint64(hoursecondsPerHour + minsecondsPerMinute + sec)

unix := int64(abs) + (absoluteToInternal + internalToUnix)

// Look for zone offset for expected time, so we can adjust to UTC.
// The lookup function expects UTC, so first we pass unix in the
// hope that it will not be too close to a zone transition,
// and then adjust if it is.
_, offset, start, end, _ := loc.lookup(unix)
if offset != 0 {
utc := unix - int64(offset)
// If utc is valid for the time zone we found, then we have the right offset.
// If not, we get the correct offset by looking up utc in the location.
if utc < start || utc >= end {
_, offset, _, _, _ = loc.lookup(utc)
}
unix -= int64(offset)
}

t := unixTime(unix, int32(nsec))
t.setLoc(loc)
return t
}

在最后t := unixTime(unix, int32(nsec))中ext字段被赋值

继续对unixTime打断点,

代码语言:javascript
复制
func unixTime(sec int64, nsec int32) Time {
return Time{uint64(nsec), sec + unixToInternal, Local}
}

其中,第一个字段sec,即Date()函数中的unix,代表的是自1970年1月1日00:00:00 UTC以来的秒数,也就是第一个日期,2008-08-08 00:00:00的Unix时间戳

其计算过程如下, 可以略过:

  1. 计算自绝对纪元以来的天数 (d): 首先,代码通过daysSinceEpoch(year)函数计算出给定年份自绝对纪元(公历纪年的开始)以来的天数。然后,根据月份和是否为闰年调整这个天数,包括在月份之前的所有天数和当前月份中的天数(通过day - 1计算,因为天数是从1开始的)。
  2. 将天数转换为秒 (abs): 计算出的天数乘以每天的秒数(secondsPerDay),加上当前天中已经过去的小时、分钟和秒数所对应的秒数,得到abs。这个值是自绝对纪元以来的总秒数。
  3. 调整到Unix时间戳 (unix): 计算出的秒数需要经过两个步骤的调整才能转换为Unix时间戳:
    • 首先,通过absoluteToInternal + internalToUnix调整。这里的absoluteToInternal是绝对时间到内部时间表示的偏移量,internalToUnix是内部时间表示到Unix时间戳的偏移量。这些偏移量是为了在不同的时间表示法之间进行转换。
    • 然后,需要根据时间所在的时区进行调整。代码首先尝试使用unix时间戳来查找时区偏移量(offset),如果这个时间戳正好在时区变更的边缘,那么它会根据UTC时间(unix - offset)再次查找正确的偏移量,并使用这个偏移量来更新unix时间戳,确保unix变量代表的是UTC时间。

通过这些步骤,unix变量最终得到的是一个表示指定日期和时间(考虑了时区偏移)的Unix时间戳。

unixToInternal int64 = (1969365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay

关于 1969365 + 1969/4 - 1969/100 + 1969/400, 是用来计算格里高利历(Gregorian calendar)下,从1年1月1日到给定年份(此处应该是到1970年,因为公元前1年的话是0)的总天数。这个计算基于格里高利历(该历法是当前国际上最广泛使用的日历体系)的规则。公式的组成部分如下:

  1. 1969365:计算给定年份之前的所有年份中的天数,假设每年都是365天。
  2. 1969/4:每四年有一个闰年,闰年有366天。这部分计算从1年到1969年间包含的闰年数量,因为每个闰年会多出一天。
  3. - 1969/100:格里高利历规则中,每100年会跳过一个闰年(即那一年不作为闰年),这部分减去这些年份中多计算的天数。
  4. + 1969/400:然而,每400年会将本该跳过的闰年加回来(即那一年作为闰年),这部分加上这些年份中应该加回的天数。

(1969365 + 1969/4 - 1969/100 + 1969/400)这个公式用于计算从公元1年1月1日到给定年份(公元前1年算作年份0,公元1年为年份1,以此类推)的累计天数,考虑了闰年的影响

再乘以 86400秒,即从公元1年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到公元1年1月1日 00:00:00的秒数,也就是2008-08-08 00:00:00到公元1年1月1日 00:00:00的秒数

很多项目中都有对该公式的直接使用

例如:

google/cel-go/common/types/timestamp.go[5]

kakeibo/date/date.go[6]

其中常量 wallToInternal[7] int64 = (1884365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay

而至于上面提到的其中常量

wallToInternal int64 = (1884365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay,

为什么选1885年,可参考前作 [Wall Clock与Monotonic Clock]( "Wall Clock与Monotonic Clock")

参考资料:
[1]

在线执行: https://go.dev/play/p/4qne90uxRD4

[2]

src/time/time.go:453: https://github.com/golang/go/blob/master/src/time/time.go#L453

[3]

wallToInternal: https://github.com/golang/go/blob/master/src/time/time.go#L456

[4]

go/src/time/time.go time结构体的ext字段: https://github.com/golang/go/blob/master/src/time/time.go#L148

[5]

google/cel-go/common/types/timestamp.go: https://github.com/google/cel-go/blob/master/common/types/timestamp.go#L47

[6]

kakeibo/date/date.go: https://github.com/hajimehoshi/kakeibo/blob/master/date/date.go#L10

[7]

wallToInternal: https://github.com/golang/go/blob/master/src/time/time.go#L456

[8]

1969365 + 1969/4 - 1969/100 + 1969/400--谷歌搜索结果: https://www.google.com/search?q=1969365+%2B+1969/4+-+1969/100+%2B+1969/400

[9]

1969365 + 1969/4 - 1969/100 + 1969/400--谷歌图书搜索结果: https://www.google.com.ph/search?tbm=bks&hl=zh-CN&q=1969365+%2B+1969%2F4+-+1969%2F100+%2B+1969%2F400

[10]

C语言之本地时间与格林威治时间互相转换(2种相互转换方法: https://its401.com/article/qq_34885669/89215858