From 8af33ba1bade2e0d9eae1de69ae36b4027c4518c Mon Sep 17 00:00:00 2001 From: knQzx <75641500+knQzx@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:34:46 +0200 Subject: [PATCH] cmpopts: add EquateApproxDuration --- cmp/cmpopts/equate.go | 24 +++++++++++++++++++++++ cmp/cmpopts/util_test.go | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 2ff424c..526f628 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -124,6 +124,30 @@ func (a timeApproximator) compare(x, y time.Time) bool { return !x.Add(a.margin).Before(y) } +// EquateApproxDuration returns a [cmp.Comparer] option that determines two +// [time.Duration] values to be equal if they are within some margin of one +// another. The margin must be non-negative. +func EquateApproxDuration(margin time.Duration) cmp.Option { + if margin < 0 { + panic("margin must be a non-negative number") + } + a := durationApproximator{margin} + return cmp.Comparer(a.compare) +} + +type durationApproximator struct { + margin time.Duration +} + +func (a durationApproximator) compare(x, y time.Duration) bool { + if x > y { + x, y = y, x + } + // Avoid subtracting durations to prevent overflow when the + // difference is larger than the largest representable duration. + return !(x+a.margin < y) +} + // AnyError is an error that matches any non-nil error. var AnyError anyError diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index 6c3066d..7129291 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -547,6 +547,48 @@ func TestOptions(t *testing.T) { opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, wantEqual: false, reason: "time difference overflows time.Duration", + }, { + label: "EquateApproxDuration", + x: time.Second, + y: time.Second, + opts: []cmp.Option{EquateApproxDuration(0)}, + wantEqual: true, + reason: "equal because durations are identical", + }, { + label: "EquateApproxDuration", + x: time.Duration(0), + y: 3 * time.Second, + opts: []cmp.Option{EquateApproxDuration(3 * time.Second)}, + wantEqual: true, + reason: "equal because duration is exactly at the allowed margin", + }, { + label: "EquateApproxDuration", + x: 3 * time.Second, + y: time.Duration(0), + opts: []cmp.Option{EquateApproxDuration(3 * time.Second)}, + wantEqual: true, + reason: "equal because duration is exactly at the allowed margin (negative)", + }, { + label: "EquateApproxDuration", + x: 3 * time.Second, + y: time.Duration(0), + opts: []cmp.Option{EquateApproxDuration(3*time.Second - 1)}, + wantEqual: false, + reason: "not equal because duration is outside allowed margin", + }, { + label: "EquateApproxDuration", + x: time.Duration(0), + y: 3 * time.Second, + opts: []cmp.Option{EquateApproxDuration(3*time.Second - 1)}, + wantEqual: false, + reason: "not equal because duration is outside allowed margin (negative)", + }, { + label: "EquateApproxDuration", + x: time.Duration(math.MaxInt64), + y: time.Duration(math.MinInt64), + opts: []cmp.Option{EquateApproxDuration(3 * time.Second)}, + wantEqual: false, + reason: "duration difference overflows time.Duration", }, { label: "EquateErrors", x: nil,