DEV Community

Sam Rose
Sam Rose

Posted on

Get the number of days between two dates in Go

#go

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)

Collapse
 
sergivb01 profile image
sergi • Edited
func daysBetween(a, b time.Time) int {
    if a.After(b) {
        a, b = b, a
    }
    return int(b.Sub(a).Hours() / 24)
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
samwho profile image
Sam Rose

Not quite as robust as I'd like. Not all days have 24 hours in them. πŸ˜”

Collapse
 
alexyslozada profile image
Alexys Lozada
func daysBetween(a, b time.Time) float64 {
    if a.After(b) {
        a, b = b, a
    }
    return math.Ceil(b.Sub(a).Hours() / 24.0)
}
Thread Thread
 
jonstodle profile image
Jon StΓΈdle

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.

centralEuropeTime, _ := time.LoadLocation("Europe/Oslo")
twentyThreeHours := time.Date(2022, 3, 27, 0, 0, 0, 0, centralEuropeTime).Sub(time.Date(2022, 3, 28, 0, 0, 0, 0, centralEuropeTime))
twentyFiveHours := time.Date(2022, 10, 30, 0, 0, 0, 0, centralEuropeTime).Sub(time.Date(2022, 10, 31, 0, 0, 0, 0, centralEuropeTime))
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
jonmithe profile image
jonmithe • Edited

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

Collapse
 
jonmithe profile image
jonmithe • Edited

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:

d1 := "2022-03-27"
d2 := "2022-04-28"

t1, _ := time.ParseInLocation("2006-01-02", d1, time.UTC)
t2, _ := time.ParseInLocation("2006-01-02", d2, time.UTC)

days := t2.Sub(t1) / (24 * time.Hour)
Enter fullscreen mode Exit fullscreen mode

Couple of thoughts:

  • UTC does not have DST / offsets, its consistently 24 hours in a day so the 24 assumption is now safe
  • Leap seconds can be ignored too, go uses the linux / POSIX definition where they are adsorbed into an adjacent timestamp so seconds in a minute is always constant.
  • This uses integer division + durations. Using duration.Hours() as others have suggested converts into a float (e.g. 1.5 hrs) and opens up possibilities of floating point errors / need to round etc, its nasty. Given the times are truncated to midnight this division is always perfect + integer so the using float is unnecessary. Even if there was a time component you'd want to floor / truncate the decimals anyway to ignore "partial" days so integer division would still be a better fit (although a time component here could break the maths if wanted to handle a tz).
  • days is actually a time.Duration, you probably want to wrap that whole line in an int64(....) or return int64(days) to get it in an actual int

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.

Collapse
 
adamjack profile image
Adam Jack

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.

Collapse
 
samwho profile image
Sam Rose

That sounds extremely reasonable. πŸ™‚