Computing the number of days between two dates...

8,758 views
Skip to first unread message

Vincent Vanackere

unread,
Feb 23, 2013, 5:04:46 PM2/23/13
to golang-nuts
Hi,

 I recently needed to compute the difference between to dates, expressed in days. I assumed this was a common operation - it comes often enough when manipulating dates - but I couldn't find any trivial go code for this.
I came up with the following solution (the only other alternative I considered so far was to compute the duration between the two dates, divide the result by 24*time.Hour and then round the result... but somehow I gave up because it felt somehow ugly - and also because AFAICT there is no ready-to-use "round a float to the nearest integer" function in the std library).

I'd be curious to know if people here can suggest a better way of doing this...

Best regards,

Vincent

--------------------------------
package main

import "fmt"
import "time"

func diffDays(year2, month2, day2, year1, month1, day1 int) int {
    if year2 < year1 {
        return -diffDays(year1, month1, day1, year2, month2, day2)
    }
    d2 := time.Date(year2, time.Month(month2), day2, 0, 0, 0, 0, time.UTC)
    d1 := time.Date(year1, time.Month(month1), day1, 0, 0, 0, 0, time.UTC)
    diff := d2.YearDay() - d1.YearDay()

    for y := year1; y < year2; y++ {
        diff += time.Date(y, time.December, 31, 0, 0, 0, 0, time.UTC).YearDay()
    }
    /* if debug && !d1.AddDate(0, 0, diff).Equal(d2) {
        panic("invalid diff")
    } */
    return diff
}

func main() {
    d := diffDays(2012, 9, 31, 2011, 10, 1)
    fmt.Println(d, " days")

}

Dave Cheney

unread,
Feb 23, 2013, 5:21:11 PM2/23/13
to Vincent Vanackere, golang-nuts
This is what I came up with

http://play.golang.org/p/qbx_J1jVhV

I don't know if a general purpose solution exists. Not all minutes
have 60 seconds, so not all days have 86400 seconds, then you have to
consider leap days, etc.
> --
> You received this message because you are subscribed to the Google Groups
> "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to golang-nuts...@googlegroups.com.
> For more options, visit https://groups.google.com/groups/opt_out.
>
>
Message has been deleted

peterGo

unread,
Feb 23, 2013, 9:36:06 PM2/23/13
to golan...@googlegroups.com
Vincent,

A simple solution:

package main

import (
    "fmt"
    "time"
)

func diffDays(
    year2 int, month2 time.Month, day2 int,
    year1 int, month1 time.Month, day1 int,
) int {
    d2 := time.Date(
        year2, month2, day2,
        0, 0, 0, 0, time.UTC,
    )
    d1 := time.Date(
        year1, month1, day1,
        0, 0, 0, 0, time.UTC,
    )
    return int(d2.Sub(d1) / (24 * time.Hour))
}

func main() {
    fmt.Println(diffDays(2012, 9, 31, 2011, 10, 1))
    fmt.Println(diffDays(2012, 1, 1, 2012, 1, 1))
    fmt.Println(diffDays(2012, 12, 31, 2012, 1, 1))
    fmt.Println(diffDays(2013, 1, 1, 2013, 12, 31))
}

Output:

366
0
365
-364

Peter

Daniel Bryan

unread,
Feb 24, 2013, 2:41:06 AM2/24/13
to golan...@googlegroups.com
Echoing Peter's suggestion; I don't see why this is so complex. Also, if you want to know the absolute difference and you're not sure which date comes first, you can use time.Before or time.After.

peterGo

unread,
Feb 24, 2013, 10:39:23 AM2/24/13
to golan...@googlegroups.com, itmi...@gmail.com
Mitică,

It is that easy. Simply write an IsDate function in Go.


package main

import (
    "fmt"
    "time"
)

func daysIn(m time.Month, year int) int {
    return time.Date(
        year, m+1, 0,

        0, 0, 0, 0,
        time.UTC,
    ).Day()
}

func IsDate(year int, month time.Month, day int) bool {
    if 1 > year || year > 9999 {
        return false
    }
    if time.January > month || month > time.December {
        return false
    }
    if 1 > day || day > daysIn(month, year) {
        return false
    }
    return true
}

func main() {
    year, month, day := 2012, time.Month(9), 31
    fmt.Println(year, month, day, IsDate(year, month, day))
    year, month, day = 2012, time.Month(10), 1
    fmt.Println(year, month, day, IsDate(year, month, day))
}

Output:

2012 September 31 false
2012 October 1 true

Peter

On Sunday, February 24, 2013 3:21:53 AM UTC-5, itmi...@gmail.com wrote:
On Sunday, February 24, 2013 4:36:06 AM UTC+2, peterGo wrote:
Vincent,

A simple solution:

func main() {
    fmt.Println(diffDays(2012, 9, 31, 2011, 10, 1))
}

It's not that easy.

One of the reasons I suggested a standard compliant lib is because it will warn about 2012, 9, 31 not being a valid date, for example.

It will also account for various calendar quirks that can't be replaced by simple math operations.

Mitică

Vincent Vanackere

unread,
Feb 24, 2013, 3:45:36 PM2/24/13
to peterGo, golang-nuts
Hi Peter,

 this code is fine - this is exactly what my first implementation looked like - as long as you can rely on the fact that every day is exactly 24h long. This is perhaps true with the current time package implementation but I saw nothing in the documentation that allows to make this assumption. The problem would not be Daylight Saving Time - that doesn't occur in UTC - but leap seconds : ISTR from some old CL discussions that these are not considered in the time package but this fact does not seem explicitly documented (I'd be happy to be wrong here).


2013/2/24 peterGo <go.pe...@gmail.com>

Niklas Schnelle

unread,
Feb 25, 2013, 4:33:59 AM2/25/13
to golan...@googlegroups.com, peterGo
No discussion about time quirks without http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
then again I think for differences of time shown to humans it really depends on which time scales your application will deal with. Most of the time Dave Cheney's solution is probably best. Definitely no need to import a database library just to do some computation in a general purpose programming language..

peterGo

unread,
Feb 25, 2013, 11:01:44 AM2/25/13
to golan...@googlegroups.com, peterGo
Vincent,

The package time API is a contract. From the API:

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

Therefore, we know that there are no leap seconds.

Internally, the package time assumes that there are 24*time.Hour nanoseconds per day.

const (
    secondsPerMinute = 60
    secondsPerHour   = 60 * 60
    secondsPerDay    = 24 * secondsPerHour
    secondsPerWeek   = 7 * secondsPerDay
    daysPer400Years  = 365*400 + 97
    daysPer100Years  = 365*100 + 24
    daysPer4Years    = 365*4 + 1
    days1970To2001   = 31*365 + 8
)

But, to be really sure, add a test for the diffDays function.


func diffDays(
    year2 int, month2 time.Month, day2 int,
    year1 int, month1 time.Month, day1 int,
) int {
    d2 := time.Date(
        year2, month2, day2,
        0, 0, 0, 0, time.UTC,
    )
    d1 := time.Date(
        year1, month1, day1,
        0, 0, 0, 0, time.UTC,
    )
    return int(d2.Sub(d1) / (24 * time.Hour))
}

For example,

import (
    "testing"
    "time"
)

type testDiffDay struct {
    y2, m2, d2 int
    y1, m1, d1 int
    days       int
}

var testDiffDays = []testDiffDay{
    // 1 - 1 = 0
    {2000, 1, 1, 2000, 1, 1, 0},
    // 2 - 1 = 1
    {2000, 1, 1 + 1, 2000, 1, 1, 1},
    // 1 - 2 = -1
    {2000, 1, 1, 2000, 1, 1 + 1, -1},
    // (366 - 1) + (365*290 + 290/4 - 290/100) = 106285
    {2000 + 290, 12, 31, 2000, 1, 1, 106285},
    // (366 - 1) + (365*290 + 290/4 - 290/100) = 106285
    {2000, 12, 31, 2000 - 290, 1, 1, 106285},
}

func Test24HoursPerDay(t *testing.T) {
    for _, test := range testDiffDays {
        days := diffDays(
            test.y2, time.Month(test.m2), test.d2,
            test.y1, time.Month(test.m1), test.d1,
        )
        if days != test.days {
            t.Errorf("diffDays: found %d expected %d", days, test.days)
        }
    }
}

Peter

peterGo

unread,
Feb 25, 2013, 11:57:10 AM2/25/13
to golan...@googlegroups.com, itmi...@gmail.com
Mitică,

I read the documentation.

func Date

The month, day, hour, min, sec, and nsec values may be outside their usual ranges and will be normalized during the conversion. For example, October 32 converts to November 1.

http://golang.org/pkg/time/#Date

Then I read the code and comments. I probably ran a test too.

Peter

On Monday, February 25, 2013 7:41:25 AM UTC-5, itmi...@gmail.com wrote:
On Sunday, February 24, 2013 5:39:23 PM UTC+2, peterGo wrote:
Mitică,

It is that easy. Simply write an IsDate function in Go.

func daysIn(m time.Month, year int) int {
    return time.Date(
        year, m+1, 0,
        0, 0, 0, 0,
        time.UTC,
    ).Day()
}

 Hi Peter.

Nice trick! 
How did you know that the zero day for the next month will be normalized as the last day of the previous month?

Mitică

Kyle Lemons

unread,
Feb 25, 2013, 12:30:31 PM2/25/13
to peterGo, golang-nuts
On Mon, Feb 25, 2013 at 8:01 AM, peterGo <go.pe...@gmail.com> wrote:
Vincent,

The package time API is a contract. From the API:

The Time API mimics Unix time.  In unix time, precisely 86400 seconds are counted per day.  Most of the poor assumptions involving time revolve around the relationship of these days to a calendar and the relationship of these days to human-perceived time.

peterGo

unread,
Feb 26, 2013, 9:22:23 AM2/26/13
to golan...@googlegroups.com, itmi...@gmail.com
Mitică,

"If the day changes due to normalization, it means the date was not a valid one."

What if the day doesn't change? For example, myday{2012, 24, 31} is not a valid Gregorian date.

Peter

On Monday, February 25, 2013 1:16:18 PM UTC-5, itmi...@gmail.com wrote:
On Monday, February 25, 2013 6:57:10 PM UTC+2, peterGo wrote:
Mitică,

I read the documentation.

func Date

The month, day, hour, min, sec, and nsec values may be outside their usual ranges and will be normalized during the conversion. For example, October 32 converts to November 1.

Well Peter,

I read it too. I got the (overday m) to (firstday m+) part.
But it never occurred to me the normalization would go like that: (0 m+) to (lastday m).
Very interesting, to say the least.
You made the time package look interesting.

Anyway, the code I posted last needed some type conversion around time.Month so I decided to test against the day instead.

If the day changes due to normalization, it means the date was not a valid one. Pretty easy test for a must validation:


package main
import (
    "fmt"
    "time"
)
type myday struct {
    year  int
    month time.Month
    day   int
}
func main() {
    d := myday{2012, 9, 31}
    if d.day == time.Date(d.year, d.month, d.day, 0, 0, 0, 0, time.UTC).Day() {
        fmt.Println("valid date")
    } else {
        fmt.Println("invalid date", d)
    }
}

Mitică

Karthik Krishnamurthy

unread,
Oct 21, 2015, 8:34:51 AM10/21/15
to golang-nuts
This is an old post, but responding to it in case someone else needs this.

http://play.golang.org/p/nTcjGZQKAa

olehg...@gmail.com

unread,
Sep 19, 2017, 8:43:03 AM9/19/17
to golang-nuts
The solution has a bug when b has the year higher than a. In my project I use a solution based on the Julian Day. More about this you can read here: http://en.wikipedia.org/wiki/Julian_day
Reply all
Reply to author
Forward
0 new messages