Recently I needed to find the number of days between two dates in Go and kept coming across implementations that didn't quite suit my needs.
So I rolled my own, and I'm pretty proud of how robust it is! :)
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(daysBetween(date("2012-01-01"), date("2012-01-07"))) // 6
fmt.Println(daysBetween(date("2016-01-01"), date("2017-01-01"))) // 366 because leap year
fmt.Println(daysBetween(date("2017-01-01"), date("2018-01-01"))) // 365
fmt.Println(daysBetween(date("2016-01-01"), date("2016-01-01"))) // 0
}
func daysBetween(a, b time.Time) int {
if a.After(b) {
a, b = b, a
}
days := -a.YearDay()
for year := a.Year(); year < b.Year(); year++ {
days += time.Date(year, time.December, 31, 0, 0, 0, 0, time.UTC).YearDay()
}
days += b.YearDay()
return days
}
func date(s string) time.Time {
d, _ := time.Parse("2006-01-02", s)
return d
}
Top comments (8)
Not quite as robust as I'd like. Not all days have 24 hours in them. 😔
This still doesn't work: Not all days have 24 hours.
In most countries in Europe and North America it varies between 23 and 25.
Yes and no I think for different reasons.
The time.Sub here operates on the internal time value golang uses not the "timezone" value. This internal is UTC seconds since jan 01 0001 for most cases.
As such in UTC all days do have 24 hours in them so this maths is sound.
However, the error you are seeing is because the time initialisation applies the DST offset. Take time.Date(2022, 3, 28, 0, 0, 0, 0, centralEuropeTime), when that is initialised it is converted into go's core UTC time which applies the DST offset, so when in DST the UTC time will be 23:00 the day before.
So the maths is still valid in that the 24 hour per day calculation is fine because its all UTC, its just the end or start bound has accidentally been dragged backwards an hour due to its initialisation which can yield an off by 1 error.
(I'm talking about 1 hour DST, of course there could be different values of this)
As my main comment suggests, represent the dates straight up in UTC and that fixes the conversion / initialisation and the maths works.
twentyFourHours := time.Date(2022, 3, 27, 0, 0, 0, 0, time.UTC).Sub(time.Date(2022, 3, 28, 0, 0, 0, 0, time.UTC))
This is good for straight up date maths. If you want to start getting clever with times / clock components, then you need to handle the timezone in different ways, again I talked about that on my root comment.
And as I mentioned in the post as well, duration.Hours() does a float conversion and that is unnecessary and not good. Embrace durations and use integer division i.e. time.Sub...(...) / (24 * time.Hour).
The trick to avoid timezone shenanigans is to just avoid the timezones and represent the dates in UTC (represent + avoid, not convert to UTC as that conversion applies the timezone so will break)
Consider the code:
Couple of thoughts:
If you want to do this via time objects, simply:
t1 := time.Date(myTime.Year(), myTime.Month(), myTime.Day(), 0,0,0,0, time.UTC)
This all relies on the truncating time and doing simple date math. If you want to add a time component in for things like noon to noon is 1 day difference and noon to 11am the next day isnt a day then timezones do become important again.
For that off the top of my head I would calculate the absolute difference of days in the method above and then compare t1 clock component in its tz (hour/min/sec/nsec) against the t2 clock component and -1 if it didn't meet the day definition (>= the same time in the day). This should be basic comparisons or integer maths to calculate the face value of the clock time since midnight ignoring DST leaps. This would assume the same timezone though, doing that sort of time duration + comparisons across different timezones seems fundamentally incorrect.
I think its worth mentioning the time struct / module in golang stores the number of seconds since typically Jan 1 year 0001 UTC (I think on lower bit machines that initial date is brought forward so its not 100% the same starting point on every machine, but the seconds since + UTC is consistent see time.go). Its important as all math operations operate on this UTC time (Sub, Add etc) not the 'timezone' time. For example, on a DST boundary you can add a second the formatted time will jump forward or backward an hour. Timezones in this package are more like a presentational layer so when you ask for day, hour or formatted string the timezone gets applied.
Thanks for sharing this. I like your approach. What are your thoughts on ensuring that "a" and "b" are within the same time.Location (Timezone) as each other?
For my thinking if they are in different time zones they might be viewed as in a different YearDay than accurate for the two.
I passed in time.Location, used that instead of time.UTC (although that seems moot, except for the benefits of the simplicity of consistency) and ensured that "a" and "b" are in the same TZ.
That sounds extremely reasonable. 🙂