time.Since()函数如何使用月份和年份进行计算?

26

我正在尝试转换这样的时间戳:

2015-06-27T09:34:22+00:00

我希望将时间格式转换为类似“9个月前1天2小时30分钟2秒”的形式。

大概就是这样。

我使用了time.Parsetime.Since得到了这个结果:

6915h7m47.6901559s

但是我该如何进行转换呢?类似这样的方式是我所想到的:

for hours > 24 {
        days++
        hours -= 24
}

但是这种方法有问题,因为一个月可能有28、30或31天,所以几个月后这种方法就不再准确。

有没有更好的方法来实现我想要的效果呢?


你需要多精确呢?不太精确的相对时间戳会使它变得更简单。 - T0xicCode
我希望它能精确到秒。 - gempir
@T0xicCode能否给我一个不太精确的解决方案?也许我可以在某些方面进行改进。 - gempir
5个回答

59

前言:我在github.com/icza/gox发布了这个实用程序,查看timex.Diff()


一个月中的天数取决于日期,就像闰年一样。

如果您使用time.Since()来获取自time.Time值以来经过的时间,或者当您计算两个time.Time值之间的差异时使用Time.Sub()方法,结果是一个time.Duration,它失去了时间上下文(因为 Duration 只是纳秒级的时间差异)。这意味着无法从Duration值准确且明确地计算出年、月等的差异。

正确的解决方案必须在时间的上下文中计算差异。您可以为每个字段(年、月、日、小时、分钟、秒)计算差异,然后将结果归一化以不具有任何负值。建议如果它们之间的关系不是预期的,则交换Time值。

规范化意味着如果一个值为负,就加上该字段的最大值,并将下一个字段减1。例如,如果seconds为负,则将其加上60,并将minutes减去1。需要注意的一点是,在规范化差异的天数(月中的天数)时,必须应用适当月份的天数。可以使用此小技巧轻松计算:

// Max days in year y1, month M1
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
daysInMonth := 32 - t.Day()

这背后的逻辑是,天数32比任何一个月份的最大天数都要大。它会自动归一化(额外的天数滚到下个月,天数适当地减少)。当我们从32中减去归一化后的天数时,我们恰好得到该月的最后一天。 时区处理: 只有当我们传递的两个时间值在同一个时区(time.Location)中时,差异计算才会给出正确的结果。我们在函数中加入了一个检查:如果不是这种情况,我们使用Time.In()方法将其中一个时间值“转换”为与另一个时间值在相同的位置。
if a.Location() != b.Location() {
    b = b.In(a.Location())
}

以下是一个解决方案,可以计算年份、月份、日期、小时、分钟和秒钟之间的差异:

func diff(a, b time.Time) (year, month, day, hour, min, sec int) {
    if a.Location() != b.Location() {
        b = b.In(a.Location())
    }
    if a.After(b) {
        a, b = b, a
    }
    y1, M1, d1 := a.Date()
    y2, M2, d2 := b.Date()

    h1, m1, s1 := a.Clock()
    h2, m2, s2 := b.Clock()

    year = int(y2 - y1)
    month = int(M2 - M1)
    day = int(d2 - d1)
    hour = int(h2 - h1)
    min = int(m2 - m1)
    sec = int(s2 - s1)

    // Normalize negative values
    if sec < 0 {
        sec += 60
        min--
    }
    if min < 0 {
        min += 60
        hour--
    }
    if hour < 0 {
        hour += 24
        day--
    }
    if day < 0 {
        // days in month:
        t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
        day += 32 - t.Day()
        month--
    }
    if month < 0 {
        month += 12
        year--
    }

    return
}

一些测试:

var a, b time.Time
a = time.Date(2015, 5, 1, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 6, 2, 1, 1, 1, 1, time.UTC)
fmt.Println(diff(a, b)) // Expected: 1 1 1 1 1 1

a = time.Date(2016, 1, 2, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b)) // Expected: 0 0 30 0 0 0

a = time.Date(2016, 2, 2, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b)) // Expected: 0 0 28 0 0 0

a = time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b)) // Expected: 0 11 1 0 0 0

输出结果与预期一致:

1 1 1 1 1 1
0 0 30 0 0 0
0 0 28 0 0 0
0 11 1 0 0 0

Go Playground上尝试一下。

计算你的年龄:

// Your birthday: let's say it's January 2nd, 1980, 3:30 AM
birthday := time.Date(1980, 1, 2, 3, 30, 0, 0, time.UTC)
year, month, day, hour, min, sec := diff(birthday, time.Now())

fmt.Printf("You are %d years, %d months, %d days, %d hours, %d mins and %d seconds old.",
    year, month, day, hour, min, sec)

示例输出:

You are 36 years, 3 months, 8 days, 11 hours, 57 mins and 41 seconds old.

Go playground的时间开始的魔法日期/时间是:2009年11月10日23:00:00 UTC
这是Go语言首次宣布的时间。让我们计算一下Go已经存在了多久:

goAnnounced := time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC)
year, month, day, hour, min, sec := diff(goAnnounced, time.Now())
fmt.Printf("Go was announced "+
    "%d years, %d months, %d days, %d hours, %d mins and %d seconds ago.",
    year, month, day, hour, min, sec)

输出:

Go was announced 6 years, 4 months, 29 days, 16 hours, 53 mins and 31 seconds ago.

5
izca提出的解决方案很好,但它缺少一件事情。如果您添加以下示例,您就可以看到效果:
a = time.Date(2015, 1, 11, 0, 0, 0, 0, time.UTC)
b = time.Date(2015, 3, 10, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b))
// Expected: 0 1 27 0 0 0
// Actual output: 0 1 30 0 0 0

playground

该代码根据第一个月的总天数 (y1,M1) 计算下一个未完成月份的剩余天数,但需要从后面日期的上个月计算 (y2,M2-1)。

最终代码如下:

package main

import (
    "fmt"
    "time"
)

func DaysIn(year int, month time.Month) int {
    return time.Date(year, month, 0, 0, 0, 0, 0, time.UTC).Day()
}

func Elapsed(from, to time.Time) (inverted bool, years, months, days, hours, minutes, seconds, nanoseconds int) {
    if from.Location() != to.Location() {
        to = to.In(to.Location())
    }

    inverted = false
    if from.After(to) {
        inverted = true
        from, to = to, from
    }

    y1, M1, d1 := from.Date()
    y2, M2, d2 := to.Date()

    h1, m1, s1 := from.Clock()
    h2, m2, s2 := to.Clock()

    ns1, ns2 := from.Nanosecond(), to.Nanosecond()

    years = y2 - y1
    months = int(M2 - M1)
    days = d2 - d1

    hours = h2 - h1
    minutes = m2 - m1
    seconds = s2 - s1
    nanoseconds = ns2 - ns1

    if nanoseconds < 0 {
        nanoseconds += 1e9
        seconds--
    }
    if seconds < 0 {
        seconds += 60
        minutes--
    }
    if minutes < 0 {
        minutes += 60
        hours--
    }
    if hours < 0 {
        hours += 24
        days--
    }
    if days < 0 {
        days += DaysIn(y2, M2-1)
        months--
    }
    if days < 0 {
        days += DaysIn(y2, M2)
        months--
    }
    if months < 0 {
        months += 12
        years--
    }
    return
}

func main() {
    var a, b time.Time
    a = time.Date(2015, 5, 1, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 6, 2, 1, 1, 1, 1, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 1 1 1 1 1 1

    a = time.Date(2016, 1, 2, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 0 0 30 0 0 0

    a = time.Date(2016, 2, 2, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 0 0 28 0 0 0

    a = time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 0 11 1 0 0 0

    a = time.Date(2015, 1, 11, 0, 0, 0, 0, time.UTC)
    b = time.Date(2015, 3, 10, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 0 1 27 0 0 0

    a = time.Date(2015, 12, 31, 0, 0, 0, 0, time.UTC)
    b = time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 0 9 30 0 0 0

    a = time.Date(2015, 12, 31, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 0 2 1 0 0 0

    a = time.Date(2015, 12, 31, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 2, 28, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: 0 2 1 0 0 0
}

playground


这是一个不错的观察,但这是有争议的。如果 a = "2021-01-31"b = "2021-03-01",那么我的算法给出了1个月1天的差异。你的算法给出了1个月和-2天的差异(负数天数!)。抛开负面因素,哪一个是正确的?至少应该有1个月的差异,因为第一个日期是一月,第二个日期是三月。这是主观的。 - icza
区别在于我的算法首先尝试步进月份以达到月份中的第二天,然后再步进月份。而你的算法首先步进月份以达到第二个月,然后再步进天数。这就是为什么你的解决方案最终会给出一个不友好的负数天数。 - icza
你是对的 @icza,我会编辑我的答案来修复它。 - robermorales

3
如果你使用PostgreSQL,你可以很容易地使用age函数获得结果。
假设你有两个日期ab
像icza所说,要小心,ab必须在同一个时区。
首先,你可以使用两个参数调用age,在你的情况下是日期a和日期b。这个函数返回一个间隔类型,包含年、月、周、日、小时、分钟、秒和毫秒。
SELECT age('2016-03-31', '2016-06-30'); -- result is: -2 mons -30 days

第二种可能性是使用一个参数的age函数。结果也是一个时间区间,但在这种情况下,age从当前日期(午夜时刻)开始减去。假设今天是2016/06/16:

SELECT age(timestamp '2016-06-30'); -- result is: -14 days

注意,需要使用timestamp关键词来转换日期“2016-06-30”。

如果需要更多细节,可以使用date_part或直接使用返回一个特定字段(年、月、日等)的extract函数。

SELECT date_part('month', age('2016-03-31', '2016-06-30')); --result is: -2
SELECT date_part('day',   age('2016-03-31', '2016-06-30')); --result is: -30

完整请求:

SELECT  
    date_part('year', diff) as year
  , date_part('month', diff) as month
  , date_part('day', diff) as day
FROM (
  SELECT age(timestamp '2016-06-30') AS diff
) as qdiff;

-- result is: 
-- year month day
-- 0    0     -14

(使用公共表达式 - CTE):

WITH qdiff AS (
  SELECT age(timestamp '2016-06-30') AS diff
)
SELECT  
    date_part('year', diff) as year
  , date_part('month', diff) as month
  , date_part('day', diff) as day
FROM qdiff

-- result is: 
-- year month day
-- 0    0     -14

PostgreSQL文档(当前版本):https://www.postgresql.org/docs/current/static/functions-datetime.html

这是关于PostgreSQL日期时间函数的官方文档链接。


我点赞这个帖子,因为虽然它与golang无关,但是它对SQL的解释很好。谢谢您。 - sbartell

2
您可以尝试使用我的日期包,其中包括时间段包,用于处理ISO风格的时间段 (维基百科)。
时间段类型带有一个格式化程序,它理解复数形式,打印可读的字符串,例如“9年2个月”和“3小时4分钟1秒”,以及其ISO等效项(“P9Y2M”和“PT3H4M1S”)。
当然,由于夏令时和公历日历的月份长度不同,因此时间段可能很棘手。 period 包通过提供允许精确和不精确计算的 API 来帮助您。对于短期时间段(最多±3276小时),它能够精确地将持续时间转换。
duration := time.Since(...)
p, _ := period.NewOf(duration)
str := p.String()

如果您需要在较长时间跨度内获得精确的持续时间,您需要使用Between函数(这体现了icza的出色答案)。
p := period.Between(t1, t2)
str := p.String()

嗨@Rick-777,但我理解这还没有准备好投入生产,对吧? - Manuelarte
现有的 period 已经发布供公众使用。它并没有完全表达周数(就像 time.Time 一样)。一个更新的变体 https://github.com/rickb777/period 仍在开发中;这将明确地表达周数。 - Rick-777

2

这样做可能不是最高效的,但它是你能得到的最准确的:

func main() {
    a := time.Date(2015, 10, 15, 0, 0, 0, 0, time.UTC)
    b := time.Date(2016, 11, 15, 0, 0, 0, 0, time.UTC)
    fmt.Println(monthYearDiff(a, b))
}

func monthYearDiff(a, b time.Time) (years, months int) {
    m := a.Month()
    for a.Before(b) {
        a = a.Add(time.Hour * 24)
        m2 := a.Month()
        if m2 != m {
            months++
        }
        m = m2
    }
    years = months / 12
    months = months % 12
    return
}

playground


无法弄清原因,但是2015-06-27T09:34:22+00:00时间返回1年10个月,这是不正确的。 - gempir
@danielps1,我的数学有误,现在正在更新帖子。 - OneOfOne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接