How to do date/time comparison
Asked Answered
W

9

136

Is there any options in doing date comparison in Go? I have to sort data based on date and time - independently. So I might allow an object that occurs within a range of dates so long as it also occurs within a range of times. In this model, I could not simply just select the oldest date, youngest time/latest date, latest time and Unix() seconds compare them. I'd really appreciate any suggestions.

Ultimately, I wrote a time parsing string compare module to check if a time is within a range. However, this is not faring to well; I've got some gaping issues. I'll post that here just for fun, but I'm hoping there's a better way to time compare.

package main

import (
    "strconv"
    "strings"
)

func tryIndex(arr []string, index int, def string) string {
    if index <= len(arr)-1 {
        return arr[index]
    }
    return def
}

/*
 * Takes two strings of format "hh:mm:ss" and compares them.
 * Takes a function to compare individual sections (split by ":").
 * Note: strings can actually be formatted like "h", "hh", "hh:m",
 * "hh:mm", etc. Any missing parts will be added lazily.
 */
func timeCompare(a, b string, compare func(int, int) (bool, bool)) bool {
    aArr := strings.Split(a, ":")
    bArr := strings.Split(b, ":")
    // Catches margins.
    if (b == a) {
        return true
    }
    for i := range aArr {
        aI, _ := strconv.Atoi(tryIndex(aArr, i, "00"))
        bI, _ := strconv.Atoi(tryIndex(bArr, i, "00"))
        res, flag := compare(aI, bI)
        if res {
            return true
        } else if flag { // Needed to catch case where a > b and a is the lower limit
            return false
        }
    }
    return false
}

func timeGreaterEqual(a, b int) (bool, bool) {return a > b, a < b}
func timeLesserEqual(a, b int) (bool, bool) {return a < b, a > b}

/*
 * Returns true for two strings formmated "hh:mm:ss".
 * Note: strings can actually be formatted like "h", "hh", "hh:m",
 * "hh:mm", etc. Any missing parts will be added lazily.
 */
func withinTime(timeRange, time string) bool {
    rArr := strings.Split(timeRange, "-")
    if timeCompare(rArr[0], rArr[1], timeLesserEqual) {
        afterStart := timeCompare(rArr[0], time, timeLesserEqual)
        beforeEnd := timeCompare(rArr[1], time, timeGreaterEqual)
        return afterStart && beforeEnd
    }
    // Catch things like `timeRange := "22:00:00-04:59:59"` which will happen
    // with UTC conversions from local time.
    // THIS IS THE BROKEN PART I BELIEVE
    afterStart := timeCompare(rArr[0], time, timeLesserEqual)
    beforeEnd := timeCompare(rArr[1], time, timeGreaterEqual)
    return afterStart || beforeEnd
}

So TLDR, I wrote a withinTimeRange(range, time) function but it's not working totally correctly. (In fact, mostly just the second case, where a time range crosses over days is broken. The original part worked, I just realized I'd need to account for that when making conversions to UTC from local.)

If there's a better (preferably built in) way, I'd love to hear about it!

NOTE: Just as an example, I solved this issue in Javascript with this function:

function withinTime(start, end, time) {
    var s = Date.parse("01/01/2011 "+start);
    var e = Date.parse("01/0"+(end=="24:00:00"?"2":"1")+"/2011 "+(end=="24:00:00"?"00:00:00":end));
    var t = Date.parse("01/01/2011 "+time);
    return s <= t && e >= t;
}

However I really want to do this filter server-side.

Whereunto answered 4/1, 2014 at 17:35 Comment(1)
pkg.go.dev/time#Time.Compare added in go 1.20Hook
D
151

Use the time package to work with time information in Go.

Time instants can be compared using the Before, After, and Equal methods. The Sub method subtracts two instants, producing a Duration. The Add method adds a Time and a Duration, producing a Time.

Play example:

package main

import (
    "fmt"
    "time"
)

func inTimeSpan(start, end, check time.Time) bool {
    return check.After(start) && check.Before(end)
}

func main() {
    start, _ := time.Parse(time.RFC822, "01 Jan 15 10:00 UTC")
    end, _ := time.Parse(time.RFC822, "01 Jan 16 10:00 UTC")

    in, _ := time.Parse(time.RFC822, "01 Jan 15 20:00 UTC")
    out, _ := time.Parse(time.RFC822, "01 Jan 17 10:00 UTC")

    if inTimeSpan(start, end, in) {
        fmt.Println(in, "is between", start, "and", end, ".")
    }

    if !inTimeSpan(start, end, out) {
        fmt.Println(out, "is not between", start, "and", end, ".")
    }
}
Disembroil answered 4/1, 2014 at 17:52 Comment(2)
Maybe I can't read, but I didn't see anything in there about time comparisons. If it's there, could you point me to an exact article?Whereunto
Try godoc.org/time#Time.Equal or godoc.org/time#Time.After for simple comparison, or godoc.org/time#Time.Sub to find out the difference between two times.Disembroil
F
37

For comparison between two times use time.Sub()

// utc life
loc, _ := time.LoadLocation("UTC")

// setup a start and end time
createdAt := time.Now().In(loc).Add(1 * time.Hour)
expiresAt := time.Now().In(loc).Add(4 * time.Hour)

// get the diff
diff := expiresAt.Sub(createdAt)
fmt.Printf("Lifespan is %+v", diff)

The program outputs:

Lifespan is 3h0m0s

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

Fenian answered 5/12, 2015 at 2:25 Comment(1)
This is the best answer.Nystrom
K
21

For case when your interval's end date doesn't contains hours like "from 2017-01-01 to whole day of 2017-01-16" it's better to adjust interval's end to midnight of the next day to include all milliseconds like this:

if now.After(start) && now.Before(end.Add(24 * time.Hour).Truncate(24 * time.Hour)) {
    ...
}
Killigrew answered 16/1, 2017 at 4:30 Comment(3)
Exactly what I needed to compare a stored TimeStamp with the Current Time.Goodwin
Using 23:59:59 as the time could be a bit risky as it wouldn't handle events that occur a few milliseconds after 23:59:59 but before 00:00:00. To avoid this, I would recommend setting end to midnight of the next day using end = end.Add(24 * time.Hour).Truncate(24 * time.Hour). Then the check if now.After(start) && now.Before(end) would work exactly as expected.Assort
@Assort thanks for this comment, really important point and code looks much better!Killigrew
F
9

It's possible to compare date using int64 of Unix epoch with seconds granularity. If you need more exact comparison like milisecons or microseconds etc. I guess that @Oleg Neumyvakin's answer is perfect.

if expirationDate.Unix() > time.Now().Unix() {
...
}
Foulk answered 29/11, 2021 at 13:11 Comment(1)
this is better, but are there any performance considerations?Alate
D
4

If you're interested in comparing whether a time is close to another for test purposes, you can use testify assert.WithinDuration for this. For example:

expectedTime := time.Now()
actualTime := expectedTime.Add(100*time.Millisecond)
assert.WithinDuration(t, expectedTime, actualTime, 1*time.Second) // pass
assert.WithinDuration(t, expectedTime, actualTime, 1*time.Millisecond) // fail

Otherwise the implementation of assert.WithinDuration can be re-used in your code to determine how close two times are (subtracting one date from the other gives the time difference):

func WithinDuration(expected, actual time.Time, delta time.Duration) bool {
   dt := expected.Sub(actual)
   return dt >= -delta && dt <= delta
}
Dryclean answered 25/5, 2021 at 23:58 Comment(0)
B
3

Per proposal time: add Time.Compare and related commit, time.Compare will be added in the new release (Go 1.20)

// Compare compares the time instant t with u. If t is before u, it returns -1;
// if t is after u, it returns +1; if they're the same, it returns 0.
func (t Time) Compare(u Time) int {

Sample

var t1, t2 Time

result := t1.Compare(t2)
Biota answered 8/11, 2022 at 9:35 Comment(0)
D
1

Recent protocols prefer usage of RFC3339 per golang time package documentation.

In general RFC1123Z should be used instead of RFC1123 for servers that insist on that format, and RFC3339 should be preferred for new protocols. RFC822, RFC822Z, RFC1123, and RFC1123Z are useful for formatting; when used with time.Parse they do not accept all the time formats permitted by the RFCs.

cutOffTime, _ := time.Parse(time.RFC3339, "2017-08-30T13:35:00Z")
// POSTDATE is a date time field in DB (datastore)
query := datastore.NewQuery("db").Filter("POSTDATE >=", cutOffTime).
Diamagnet answered 30/8, 2017 at 8:18 Comment(0)
F
0

As explained in the theread we could use github.com/google/go-cmp/cmp package for dates comparison in tests.

func TestDates(t *testing.T) {
    date, _ := time.Parse(time.RFC3339, "2021-11-05T12:00:00+02:00")
    dateEqual, _ := time.Parse(time.RFC3339, "2021-11-05T11:00:00+01:00")
    dateNotEqual, _ := time.Parse(time.RFC3339, "2021-11-05T12:00:01+02:00")

    assertDates(t, date, dateEqual)    //pass
    assertDates(t, date, dateNotEqual) //fail
}

func assertDates(t *testing.T, expected, actual time.Time) {
    t.Helper()

    if diff := cmp.Diff(expected, actual); diff != "" {
        t.Errorf("mismatch (-expected +actual):\n%s", diff)
    }
}
Family answered 5/11, 2021 at 11:39 Comment(0)
V
0
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Hello World")
    maxRep := 5
    repPeroid := 6
    expiry := maxRep * repPeroid
    fmt.Println("Expiry: ", expiry)
    fmt.Println(time.Now())
    CorrIdtime := time.Now().Add(time.Second * time.Duration(expiry)).Format(time.RFC3339)
    Notifytime := time.Now().Add(2 * time.Second * time.Duration(expiry)).Format(time.RFC3339)
    
    fmt.Println(CorrIdtime)
    fmt.Println(Notifytime)

    if CorrIdtime < Notifytime {
        fmt.Println("Discarded")
    } else {
        fmt.Println("Accepted")
        
    }
}
Venlo answered 3/10, 2022 at 16:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.