diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 41bbddc..fea57bc 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -8,6 +8,7 @@ package cmpopts import ( "math" "reflect" + "time" "github.com/google/go-cmp/cmp" ) @@ -87,3 +88,38 @@ func areNaNsF64s(x, y float64) bool { func areNaNsF32s(x, y float32) bool { return areNaNsF64s(float64(x), float64(y)) } + +// EquateApproxTime returns a Comparer options that +// determine two time.Time values to be equal if they +// are within the given time interval of one another. +// Note that if both times have a monotonic clock reading, +// the monotonic time difference will be used. +// +// The zero time is treated specially: it is only considered +// equal to another zero time value. +// +// It will panic if margin is negative. +func EquateApproxTime(margin time.Duration) cmp.Option { + if margin < 0 { + panic("negative duration in EquateApproxTime") + } + return cmp.FilterValues(func(x, y time.Time) bool { + return !x.IsZero() && !y.IsZero() + }, cmp.Comparer(timeApproximator{margin}.compare)) +} + +type timeApproximator struct { + margin time.Duration +} + +func (a timeApproximator) compare(x, y time.Time) bool { + // Avoid subtracting times to avoid overflow when the + // difference is larger than the largest representible duration. + if x.After(y) { + // Ensure x is always before y + x, y = y, x + } + // We're within the margin if x+margin >= y. + // Note: time.Time doesn't have AfterOrEqual method hence the negation. + return !x.Add(a.margin).Before(y) +} diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index ed0fbb1..3c01cac 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -450,6 +450,64 @@ func TestOptions(t *testing.T) { }, wantEqual: true, reason: "equal because named type is transformed to float64", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(0)}, + wantEqual: true, + reason: "equal because times are identical", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin (negative)", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)}, + reason: "not equal because time is outside allowed margin", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)}, + reason: "not equal because time is outside allowed margin (negative)", + }, { + label: "EquateApproxTime", + x: time.Time{}, + y: time.Time{}, + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because both times are zero", + }, { + label: "EquateApproxTime", + x: time.Time{}, + y: time.Time{}.Add(1), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + reason: "not equal because zero time is always not equal not non-zero", + }, { + label: "EquateApproxTime", + x: time.Time{}.Add(1), + y: time.Time{}, + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + reason: "not equal because zero time is always not equal not non-zero", + }, { + label: "EquateApproxTime", + x: time.Date(2409, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2000, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + reason: "time difference overflows time.Duration", }, { label: "IgnoreFields", x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, @@ -888,6 +946,12 @@ func TestPanic(t *testing.T) { fnc: EquateApprox, args: args(0.0, math.Inf(+1)), reason: "margin of infinity is valid", + }, { + label: "EquateApproxTime", + fnc: EquateApproxTime, + args: args(time.Duration(-1)), + wantPanic: "negative duration in EquateApproxTime", + reason: "negative duration is invalid", }, { label: "SortSlices", fnc: SortSlices,