time.Since() with months and years

29,995

Solution 1

Foreword: I released this utility in github.com/icza/gox, see timex.Diff().


The days in a month depends on the date, just like the days in a year (leap years).

If you use time.Since() to get the elapsed time since a time.Time value, or when you calculate the difference between 2 time.Time values using the Time.Sub() method, the result is a time.Duration which loses the time context (as Duration is just the time difference in nanoseconds). This means you cannot accurately and unambiguously calculate the difference in years, months, etc. from a Duration value.

The right solution must calculate the difference in the context of the time. You may calculate the difference for each field (year, month, day, hour, minute, second), and then normalize the result to not have any negative values. It is also recommended to swap the Time values if the relation between them is not the expected.

Normalization means if a value is negative, add the maximum value of that field and decrement the next field by 1. For example if seconds is negative, add 60 to it and decrement minutes by 1. One thing to look out for is when normalizing the difference of days (days in month), the number of days in the proper month has to be applied. This can easily be calculated with this little trick:

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

The logic behind this is that the day 32 is bigger than the max day in any month. It will get automatically normalized (extra days rolled to the next month and day decremented properly). And when we subtract day we have after normalization from 32, we get exactly what the last day was in the month.

Time zone handling:

The difference calculation will only give correct result if both of the time values we pass in are in the same time zone (time.Location). We incorporate a check into our function: if this is not the case, we "convert" one of the time value to be in the same location as the other using the Time.In() method:

if a.Location() != b.Location() {
    b = b.In(a.Location())
}

Here's a solution which calculates difference in year, month, day, hour, min, sec:

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
}

Some tests:

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

Output is as expected:

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

Try it on the Go Playground.

To calculate how old you are:

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

Example output:

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

The magic date/time at which the Go playground time starts is: 2009-11-10 23:00:00 UTC
This is the time when Go was first announced. Let's calculate how old Go is:

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)

Output:

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

Solution 2

The solution proposed by izca is great, but it misses one thing. If you add the following example, you can see the effect:

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

The code is calculating the remaining days of the next incomplete month based on the total days of the first month (y1,M1), but it needs to be computed from the previous month of the later date month (y2,M2-1).

The final code is as follows:

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

Solution 3

If you use PostgreSQL, you can easily get the result with age function.

Suppose you have two dates a and b.

Like icza said, be careful, a and b must be in the same time zone.

First, you can invoke age with two parameters, in your case date a and date b. This function return a interval type that contains years, months, weeks, days, hours, minutes, seconds, and milliseconds.

SELECT age('2016-03-31', '2016-06-30'); -- result is: -2 mons -30 days

The second possibilty is to use age function with one parameter. The result is a interval too but in this case, age subtract from current_date (at midnight). Suppose today is 2016/06/16:

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

Note, timestamp keyword is needed to cast the date '2016-06-30'.

For more details, you can use date_part or directly extract function that return one specific field (years, months, days...).

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

Full request:

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

(with CTE - Common Table Expression):

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 documentation (current version): https://www.postgresql.org/docs/current/static/functions-datetime.html

Solution 4

You could try working with my date package, which includes the period package for working with ISO-style periods of time (Wikipedia).

The Period type comes with a formatter that understands plurals, printing readable strings such as "9 years, 2 months" and "3 hours, 4 minutes, 1 second", along with the ISO equivalents ("P9Y2M" and "PT3H4M1S").

Periods are, of course, tricky due to the variable lengths of days (due to DST) and months (due to the Gregorian calendar). The period package tries to help you by providing an API that allows both precise and imprecise calculations. For short periods (up to ±3276 hours) it is able to convert a Duration precisely.

duration := time.Since(...)
p, _ := period.NewOf(duration)
str := p.String()

If you need precise durations over longer spans, you need to use the Between function (which embody icza's excellent answer).

p := period.Between(t1, t2)
str := p.String()

Solution 5

Something like this would work, probably not the most efficient but it is as accurate as you gonna get:

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

Share:
29,995

Related videos on Youtube

gempir
Author by

gempir

I'm a software-developer.

Updated on July 09, 2022

Comments

  • gempir
    gempir almost 2 years

    I am trying to convert a timestamp like this:

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

    to a time since format so it would say like 9 months ago 1 day 2 hours 30 minutes 2 seconds.

    something like that.

    I used time.Parse and time.Since to get to this:

    6915h7m47.6901559s
    

    But how do I convert from there on? Something like this is what I thought:

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

    But the issue with this is that this won't be accurate for months because months can have 28, 30 and 31 days.

    Is there a better way of achieving what I want?

    • T0xicCode
      T0xicCode about 8 years
      How precise do you need it to be? Imprecise relative timestamps would make that a bit simpler
    • gempir
      gempir about 8 years
      I would prefer it to be precise to the second
    • gempir
      gempir about 8 years
      @T0xicCode could give me the solution that is less precise? Maybe I can improve it somehow
  • gempir
    gempir about 8 years
    can't figure out why but 2015-06-27T09:34:22+00:00 time returns 1 year and 10 months which is incorrect.
  • OneOfOne
    OneOfOne about 8 years
    @danielps1 my math was off, updating the post now
  • sbartell
    sbartell over 6 years
    I'm upvoting this because, even though it has nothing to do with golang, it is a well explained approach in sql. ty sir.
  • icza
    icza over 2 years
    This is a nice observation, but this is debatable. If a = "2021-01-31" and b = "2021-03-01", then my algorithm gives 1 month 1 day difference. Your algorithm gives 1 month and -2 days difference (negative days!). Putting the negative aside, which one is correct? There should be at least 1 month difference because the first date is January, the second is March. This is subjective.
  • icza
    icza over 2 years
    The difference is that my algorithm first tries to step the days in month to reach the second days in month, then steps the months. Yours first steps the months to reach the second month, then steps the days. That's why your solution gives a non-user friendly negative days at the end.
  • robermorales
    robermorales over 2 years
    you are right @icza , i will edit my answer to fix it.