time.Since() with months and years
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.
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