Proposal Details
While attempting to port over from Ruby code to Go, I ran into an issue where I needed to set a variable of type time.Time
to the maximum of three different times. The maximum here would apply to the farthest in the future time. In Ruby this involves putting the three times in an array and calling the max method on the array.
my_time = [someTime, someTime2, DateTime.now].max
This doesn't work in Go currently because the slices.Max
method only works on slices where of type cmp.Ordered
which time.Time
is not.
All time.Time
's can be converted to an int64 Unix representation which would satisfy the cmp.Ordered requirement for using slices.Max
and slices.Min
. However, this wouldn't work for times before 0 Unix time.
I'd instead purpose adding Min
and Max
functions for the time
package. They'd work by just using the Before
and After
methods that already exist in the package
func Max(x Time, y ...Time) Time {
maxTime := x
for _, val := range y {
if val.After(maxTime) {
maxTime = val
}
}
return maxTime
}
func Min(x Time, y ...Time) Time {
minTime := x
for _, val := range y {
if val.Before(minTime) {
minTime = val
}
}
return minTime
}
// Usage
thatTime := time.Max(someTime, someTime2, time.Now())
The function signature is just like the min and max built-in functions where at least one parameter is required, that way we can just return that value if no others are passed in. The first parameter would also just get used as our base for finding the min or max of the times. This also prevents us from unintentionally returning the time.Time zero value in the Min function
Comment From: seankhliao
This sounds quite specialized, how often is this operation used outside of your specific example?
https://go.dev/doc/faq#x_in_std
Comment From: earthboundkid
oldest := slices.MinFunc([]time.Time{now, someTime, someTime2}, time.Time.Compare)
https://go.dev/play/p/rVGx_3me6NQ
Comment From: jflambert
Thanks @earthboundkid, I ended up here while looking for golang equivalents to greatest
and least
functions in PostgreSQL which accept any data type.
Comment From: apparentlymart
Note that the conversion from a time.Time
to an offset from the Unix epoch can return a negative number, so Unix timestamp zero is not the lower bound.
The documentation for each of the methods that return Unix-epoch-relative offsets says, at the time of writing:
Time.Unix
"is valid for billions of years into the past or future" [relative to zero]Time.UnixMicro
: Year -290307 through 294246Time.UnixMilli
: "292 million years before or after 1970"Time.UnixNano
: Year 1678 through 2262
The nanosecond-grain result does have a somewhat constrained range if you want to talk about all years where humans might have done something, but the others seem like they ought to be sufficient for most cases unless you actually need to be able to distinguish timestamps that differ only in nanoseconds.
Does the ability for these functions to represent timestamps before Unix epoch make that a viable solution after all, or do you need to compare timestamps even outside of these wide ranges? :thinking:
(I personally don't feel opposed to having comparison functions that are valid for all time.Time
values, FWIW, but the argument for it in the proposal suggested that these Unix timestamp functions can only return positive timestamps, which doesn't seem to be true.)
Comment From: seankhliao
I'll closed given the availability of slices.MinFunc, slices.MaxFunc, and Time.Compare.
Comment From: igadmg
Somehow
time := max(time1, time2)
should be supported.
time := time.Unix(max(time1.Unix(), time2.Unix()), 0)
is ugly and not correct.
Comment From: apparentlymart
So far a typical way to handle custom implementations of operations similar to the built-in operators has been to offer a function that takes a helper function that defines custom rules for the operation.
Applying that pattern to the predeclared max
, we can write something like this:
// MaxFunc returns the greater of the two given arguments as decided
// by the given comparison function.
//
// If the comparison function reports that the values are equal
// then x is returned.
func MaxFunc[T any](x, y T, compare func(x, y T) int) T {
rel := compare(x, y)
if rel < 0 {
return y
}
return x
}
...which can be called with time.Time
values like this:
maxTime := MaxFunc(t1, t2, time.Time.Compare)
This uses the Time.Compare
function previously mentioned to represent the canonical ordering for time.Time
values. The signature of compare
matches an emerging convention in the standard library for comparison functions; it also matches cmp.Compare
, for example.
I suppose in principle a function like this could be added to package cmp
, along with the symmetrical MinFunc
, but I'll leave someone else to argue for this being important enough to include in the standard library (in a separate proposal issue!) if they are motivated to do so, since it's such a small function.
(This is similar to the slices.MinFunc
example from earlier, but without constructing a slice at the expense of only supporting comparison between two time values at a time. It could be generalized to support more by putting the comparison function parameter first in the signature and then using a variadic parameter, but then we'd effectively be back to making a slice again so slices.MinFunc
would achieve essentially the same effect. 🤷♂ )
Comment From: earthboundkid
Do you want to propose cmp.MaxFunc
? Seems like a good idea.