写点什么

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

作者:fliter
  • 2024-02-03
    上海
  • 本文字数:10052 字

    阅读完需:约 33 分钟

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


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 代码示例:


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)}
复制代码


在线执行


输出:


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


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


如何实现的呢...


src/time/time.go:453



调试以上代码:


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



// 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


<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结构体中的wallext字段共同编码了时间的信息,其中ext字段具有特定的含义和作用:


  1. ext字段含义ext字段是一个 64 位的有符号整数(int64),它的作用依赖于wall字段中的hasMonotonic位的状态:

  2. 如果hasMonotonic位为 0(表示没有单调时钟读数),ext字段存储的是自公元 1 年 1 月 1 日起的完整的墙上时钟(wall clock)秒数。这意味着,当没有单调时钟读数时,ext用于表示时间点的秒数。

  3. 如果hasMonotonic位为 1(表示存在单调时钟读数),ext字段则存储自进程启动以来的单调时钟读数,单位为纳秒。这种情况下,ext提供了用于比较或减法运算的额外精度,因为单调时钟保证了时间的前后顺序,即使系统时间被修改。

  4. 如何得到ext

  5. 当创建一个time.Time实例时,如果包含了单调时钟的读数,ext字段会被自动设置为自进程启动以来的单调时钟读数。这通常在内部通过调用某些time包的函数来实现,如time.Now(),它会捕获当前的墙上时钟时间和单调时钟时间。

  6. 如果单调时钟读数不被包含,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 函数,继续追加断点:


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}
复制代码


<br>


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



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(hour*secondsPerHour + min*secondsPerMinute + 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 打断点,


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 时间戳



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


<font size=1 color=blue>


  1. 计算自绝对纪元以来的天数 (d): 首先,代码通过daysSinceEpoch(year)函数计算出给定年份自绝对纪元(公历纪年的开始)以来的天数。然后,根据月份和是否为闰年调整这个天数,包括在月份之前的所有天数和当前月份中的天数(通过day - 1计算,因为天数是从 1 开始的)。

  2. 将天数转换为秒 (abs): 计算出的天数乘以每天的秒数(secondsPerDay),加上当前天中已经过去的小时、分钟和秒数所对应的秒数,得到abs。这个值是自绝对纪元以来的总秒数。

  3. 调整到 Unix 时间戳 (unix): 计算出的秒数需要经过两个步骤的调整才能转换为 Unix 时间戳:

  4. 首先,通过absoluteToInternal + internalToUnix调整。这里的absoluteToInternal是绝对时间到内部时间表示的偏移量,internalToUnix是内部时间表示到 Unix 时间戳的偏移量。这些偏移量是为了在不同的时间表示法之间进行转换。

  5. 然后,需要根据时间所在的时区进行调整。代码首先尝试使用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)的总天数。这个计算基于格里高利历(该历法是当前国际上最广泛使用的日历体系)的规则。公式的组成部分如下:


  1. 1969*365:计算给定年份之前的所有年份中的天数,假设每年都是 365 天。

  2. 1969/4:每四年有一个闰年,闰年有 366 天。这部分计算从 0 年到 1968 年间包含的闰年数量,因为每个闰年会多出一天。

  3. - 1969/100:格里高利历规则中,每 100 年会跳过一个闰年(即那一年不作为闰年),这部分减去这些年份中多计算的天数。

  4. + 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


kakeibo/date/date.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--谷歌图书搜索结果


C语言之本地时间与格林威治时间互相转换(2种相互转换方法


<br>

用户头像

fliter

关注

www.dashen.tech 2018-06-21 加入

Software Engineer. Focus on Micro Service,Containerization

评论

发布
暂无评论
如何计算两个日期之间的天数_fliter_InfoQ写作社区