Add a `clamp` function to Algorithm.swift

Swift Evolution,

Having a`clamp` function available in the standard library would complement
`min` and `max` really well.

I mentioned this before when the discussion for Swift 4 was still in stage
1, but now that stage 2 has started I thought it might be worth while to
bring up again.

Still needs some work, but here is the draft proposal I wrote previously

Basically it boils down to something like this.

public func clamp<T : Comparable>(value: T, _ lower: T, _ upper: T) -> T {
  return max(lower, min(value, upper))
}

What does the community think?

- Nick

3 Likes

IIRC, we’ve been avoiding top-level functions lately… What about as an extension on Range?
extension Range {
    func clamp(_ x: Bound) -> Bound {
        return max(lowerBound, min(x, upperBound))
    }
}

- Dave Sweeris

···

On Mar 9, 2017, at 5:37 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org> wrote:

Swift Evolution,

Having a`clamp` function available in the standard library would complement `min` and `max` really well.

I mentioned this before when the discussion for Swift 4 was still in stage 1, but now that stage 2 has started I thought it might be worth while to bring up again.

Still needs some work, but here is the draft proposal I wrote previously

https://github.com/Nirma/swift-evolution/commit/a51c543a76e9a1021996fb4b617311a588e7f397

Basically it boils down to something like this.

public func clamp<T : Comparable>(value: T, _ lower: T, _ upper: T) -> T {
  return max(lower, min(value, upper))
}

What does the community think?

5 Likes

Dave,

Thanks for the feedback!

Yes, there is no explicit need to make `clamp` a global function, and the
extension you proposed would work just fine I think.

`CountableClosedRange` already provides a `clamped(to:)` method but that
does not suit the use case I was thinking of wanting to receive a single
scalar return value rather than a range.

Any chance of dumping `min` and `max` as global functions?
There is already an extension on `Array` for `min` and `max` but that might
carry the overhead of creating an array every time a `min` or `max` is
performed, unless there was some compiler optimization magic that could
come to the rescue.

- Nick

···

On Fri, Mar 10, 2017 at 10:44 AM, David Sweeris <davesweeris@mac.com> wrote:

On Mar 9, 2017, at 5:37 PM, Nicholas Maccharoli via swift-evolution < > swift-evolution@swift.org> wrote:

Swift Evolution,

Having a`clamp` function available in the standard library would
complement `min` and `max` really well.

I mentioned this before when the discussion for Swift 4 was still in stage
1, but now that stage 2 has started I thought it might be worth while to
bring up again.

Still needs some work, but here is the draft proposal I wrote previously

https://github.com/Nirma/swift-evolution/commit/
a51c543a76e9a1021996fb4b617311a588e7f397

Basically it boils down to something like this.

public func clamp<T : Comparable>(value: T, _ lower: T, _ upper: T) -> T {
  return max(lower, min(value, upper))
}

What does the community think?

IIRC, we’ve been avoiding top-level functions lately… What about as an
extension on Range?

extension Range {
    func clamp(_ x: Bound) -> Bound {
        return max(lowerBound, min(x, upperBound))
    }
}

- Dave Sweeris

I’d be on board with an extension of Comparable so you could write
“16.clamped(to: 0...10)”. Something along the lines of:

extension Comparable {
    func clamped(to range: ClosedRange<Self>) -> Self {
        return max(range.lowerBound, min(self, range.upperBound))
    }
}

Nevin

4 Likes

Nevin,

Yeah I think this works well as an extension on `Comparable`,
`foo.clamped(to: 1...100)` seems pretty natural.

Why not go one step further and move the versions of min, max that take two
arguments on over to `Comparable` as a protocol extension?

Perhaps something like this?

extension Comparable {

    func max(with value: Self) -> Self {
        if value > self {
            return value
        }
        return self
    }

    func min(with value: Self) -> Self {
        if value < self {
            return value
        }
        return self
    }

    func clamped(to range: ClosedRange<Self>) -> Self {
        let selfUpperMin = range.upperBound.min(with: self)
        return range.lowerBound.max(with: selfUpperMin)
    }
}

- Nick

···

On Fri, Mar 10, 2017 at 1:41 PM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org> wrote:

I’d be on board with an extension of Comparable so you could write
“16.clamped(to: 0...10)”. Something along the lines of:

extension Comparable {
    func clamped(to range: ClosedRange<Self>) -> Self {
        return max(range.lowerBound, min(self, range.upperBound))
    }
}

Nevin

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Nevin,

Yeah I think this works well as an extension on `Comparable`, `foo.clamped(to: 1...100)` seems pretty natural.

Why not go one step further and move the versions of min, max that take two arguments on over to `Comparable` as a protocol extension?

I think that a symmetric operation like `min` or `max` ought to treat both arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but privileges one argument over the other syntactically. I’d very much like to avoid this.

I would be okay with removing top-level min and max if `Array` min and max could generate equivalent code given an array literal. This seems possible.

···

On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org> wrote:

Perhaps something like this?

extension Comparable {

    func max(with value: Self) -> Self {
        if value > self {
            return value
        }
        return self
    }

    func min(with value: Self) -> Self {
        if value < self {
            return value
        }
        return self
    }

    func clamped(to range: ClosedRange<Self>) -> Self {
        let selfUpperMin = range.upperBound.min(with: self)
        return range.lowerBound.max(with: selfUpperMin)
    }
}

- Nick

On Fri, Mar 10, 2017 at 1:41 PM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
I’d be on board with an extension of Comparable so you could write “16.clamped(to: 0...10)”. Something along the lines of:

extension Comparable {
    func clamped(to range: ClosedRange<Self>) -> Self {
        return max(range.lowerBound, min(self, range.upperBound))
    }
}

Nevin

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

1 Like

Nevin,

Yeah I think this works well as an extension on `Comparable`, `foo.clamped(to: 1...100)` seems pretty natural.

Why not go one step further and move the versions of min, max that take two arguments on over to `Comparable` as a protocol extension?

I think that a symmetric operation like `min` or `max` ought to treat both arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but privileges one argument over the other syntactically. I’d very much like to avoid this.

Agreed.

I would be okay with removing top-level min and max if `Array` min and max could generate equivalent code given an array literal. This seems possible.

Yeah, it seems like it’d be technically possible, but not without either a lot of compiler magic (not only in somehow optimizing away the overhead of creating an Array, but `[1, 2].max()` returns an `Int?` instead of an `Int`) , or maybe a sufficiently sophisticated macro… The 1st is something we’re trying to avoid, and IIRC the 2nd is out-of-scope for Swift 4. Also, I’m wary of not having the normal “math.h” functions, simply because they’re so universal.

- Dave Sweeris

···

On Mar 10, 2017, at 12:22 AM, Jaden Geller via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Nevin,

Yeah I think this works well as an extension on `Comparable`, `foo.clamped(to: 1...100)` seems pretty natural.

Why not go one step further and move the versions of min, max that take two arguments on over to `Comparable` as a protocol extension?

I think that a symmetric operation like `min` or `max` ought to treat both arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but privileges one argument over the other syntactically. I’d very much like to avoid this.

Agreed.

I would be okay with removing top-level min and max if `Array` min and max could generate equivalent code given an array literal. This seems possible.

Yeah, it seems like it’d be technically possible, but not without either a lot of compiler magic (not only in somehow optimizing away the overhead of creating an Array, but `[1, 2].max()` returns an `Int?` instead of an `Int`) , or maybe a sufficiently sophisticated macro… The 1st is something we’re trying to avoid, and IIRC the 2nd is out-of-scope for Swift 4. Also, I’m wary of not having the normal “math.h” functions, simply because they’re so universal.

I suspect the compiler magic wouldn’t be too difficult if the array were a literal, but I don’t know. I forgot that it returns an optional though, that’s definitely problematic and probably a deal-breaker. Perhaps if we ever get vectors (with the size known by the type-system) we could do this (as the result would be non-optional), but I don’t know how it would choose this overload since Array is preferred…

*leaves lid on Pandora’s box and backs away*

···

On Mar 10, 2017, at 1:13 AM, David Sweeris <davesweeris@mac.com> wrote:

On Mar 10, 2017, at 12:22 AM, Jaden Geller via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

- Dave Sweeris

Sorry for sidetracking by talking about dumping the global definitions of
`min` and `max` but if that could be done and it were decided by the swift
community that adding a clamp function would be appropriate, I guess with
the array implementations of min / max the clamp function might be
implemented like this?

extension Comparable {

    func clamped(to range: ClosedRange<Self>) -> Self {

        return [range.lowerBound, [self, range.upperBound].min()!].max()!

    }

}

···

On Fri, Mar 10, 2017 at 5:22 PM, Jaden Geller <jaden.geller@gmail.com> wrote:

On Mar 9, 2017, at 11:20 PM, Nicholas Maccharoli via swift-evolution < > swift-evolution@swift.org> wrote:

Nevin,

Yeah I think this works well as an extension on `Comparable`,
`foo.clamped(to: 1...100)` seems pretty natural.

Why not go one step further and move the versions of min, max that take
two arguments on over to `Comparable` as a protocol extension?

I think that a symmetric operation like `min` or `max` ought to treat both
arguments in a symmetric way. `3.max(with: 9)` not only reads badly, but
privileges one argument over the other syntactically. I’d very much like to
avoid this.

I would be okay with removing top-level min and max if `Array` min and max
could generate equivalent code given an array literal. This seems possible.

Perhaps something like this?

extension Comparable {

    func max(with value: Self) -> Self {
        if value > self {
            return value
        }
        return self
    }

    func min(with value: Self) -> Self {
        if value < self {
            return value
        }
        return self
    }

    func clamped(to range: ClosedRange<Self>) -> Self {
        let selfUpperMin = range.upperBound.min(with: self)
        return range.lowerBound.max(with: selfUpperMin)
    }
}

- Nick

On Fri, Mar 10, 2017 at 1:41 PM, Nevin Brackett-Rozinsky via > swift-evolution <swift-evolution@swift.org> wrote:

I’d be on board with an extension of Comparable so you could write
“16.clamped(to: 0...10)”. Something along the lines of:

extension Comparable {
    func clamped(to range: ClosedRange<Self>) -> Self {
        return max(range.lowerBound, min(self, range.upperBound))
    }
}

Nevin

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

1 Like

I’m ok with doing it as an extension on `Comparable`, although we should add an overload for regular ranges, too.

- Dave Sweeris

···

On Mar 10, 2017, at 1:12 AM, Nicholas Maccharoli via swift-evolution <swift-evolution@swift.org> wrote:

Sorry for sidetracking by talking about dumping the global definitions of `min` and `max` but if that could be done and it were decided by the swift community that adding a clamp function would be appropriate, I guess with the array implementations of min / max the clamp function might be implemented like this?

extension Comparable {

    func clamped(to range: ClosedRange<Self>) -> Self {

        return [range.lowerBound, [self, range.upperBound].min()!].max()!

    }

}

I’m ok with doing it as an extension on `Comparable`, although we should add an overload for regular ranges, too.

- Dave Sweeris

How would the semantics of that work?

Good questions! I was mostly thinking about how often we do stuff like “for i in 0 ..< arr.count”, and that it’d be handy to not have to switch over to using ClosedRange just to be able to clamp to it. To answer your questions, though…

Should “16.clamped(to: 0..<10)” produce 9 or 10?

9

What about “16.clamped(to: 0..<0)”, which is an empty range?

For `Int`? Crash (which, until about 5 minutes ago, is what I thought would happen if you tried to create a range that’s empty like that). For types that support it, I’d say NaN or something like “nil”/“empty” is the most appropriate return value

Does “16.0.clamped(to: 0..<10)” yield 10.0 or the next-smaller representable Double?

Next-smaller, IMHO. It’s not exactly semantically correct, but AFAIK that’s as correct as Float/Double can be.

Mostly though I’d really like to be able to clamp to array indices, which are pretty much always written as a `Range`, rather than a `ClosedRange`. We could write the function for `Range` to only be generic over `Comparable&Integer`, if the floating point corner cases are too much.

- Dave Sweeris

···

On Mar 10, 2017, at 11:32 AM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org> wrote:
On Fri, Mar 10, 2017 at 4:16 AM, David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

How would the semantics of that work?

Should “16.clamped(to: 0..<10)” produce 9 or 10?

What about “16.clamped(to: 0..<0)”, which is an empty range?

Does “16.0.clamped(to: 0..<10)” yield 10.0 or the next-smaller
representable Double?

Nevin

···

On Fri, Mar 10, 2017 at 4:16 AM, David Sweeris via swift-evolution < swift-evolution@swift.org> wrote:

I’m ok with doing it as an extension on `Comparable`, although we should
add an overload for regular ranges, too.

- Dave Sweeris

​Dave,

I’m ok with doing it as an extension on `Comparable`, although we should
add an overload for regular ranges, too.

​Yeah, I completely agree and for now I'll drop the topic of removing the
global definitions for min / max​.

So the aim of this proposal would be to add `clamp` to both `Range` and
`Comparable`.

- Nick

···

On Fri, Mar 10, 2017 at 6:16 PM, David Sweeris <davesweeris@mac.com> wrote:

On Mar 10, 2017, at 1:12 AM, Nicholas Maccharoli via swift-evolution < > swift-evolution@swift.org> wrote:

Sorry for sidetracking by talking about dumping the global definitions of
`min` and `max` but if that could be done and it were decided by the swift
community that adding a clamp function would be appropriate, I guess with
the array implementations of min / max the clamp function might be
implemented like this?

extension Comparable {

    func clamped(to range: ClosedRange<Self>) -> Self {

        return [range.lowerBound, [self, range.upperBound].min()!].max()!

    }

}

I’m ok with doing it as an extension on `Comparable`, although we should
add an overload for regular ranges, too.

- Dave Sweeris

I'm very fond of this proposal. Unless I'm missing more-recent activity, it seems that quite some time has passed since this was last discussed. At the risk of "necroposting" I'd like to ask if there have been any updates regarding the addition of scalar clamping to the standard library. I see the original SE proposal was returned for revisions, but I cannot seem to find any associated commentary.

Thanks!
Will

1 Like

Lets get moving on this! I will send a pull request over to the evolution and swift repository of the current proof of concept I have working!

It is 100% useful, thanks for creating this topic.

I can only add my 5 cents providing a variant of implementation and some examples:

extension Comparable {
  public func boundedWith(_ lhs: Self, _ rhs: Self) -> Self {
    guard lhs != rhs else { return lhs }

    var lower = lhs
    var upper = rhs

    if rhs < lhs {
      swap(&lower, &upper)
    }

    return min(upper, max(self, lower))
  }
}

// The idea is that both
0.boundedWith(5, 1)
// and
0.boundedWith(1, 5)
// return the same result

Examples:

let totalSeconds = seconds.boundedWith(0, Int.max)

let boundedRadius = radius.boundedWith(0, min(bounds.width, bounds.height) / 2)

let amount = amount.boundedWith(0, Double.greatestFiniteMagnitude)

let progressValue = progress.boundedWith(0, 100)

extension CGRect {
  public func safeInsetBy(dx: CGFloat, dy: CGFloat) -> CGRect {
    let dxSafe = dx.boundedWith(0, width / 2)
    let dySafe = dy.boundedWith(0, height / 2)
    return insetBy(dx: dxSafe, dy: dySafe)
  }
}

func setRating(_ rating: Int) {
  let value = rating.boundedWith(1, 5)
  // ...
}
1 Like

Nice work with the implementation. I'm just concerned with the naming, as I would prefer a method named clamped:

x.clamped(between: 1, and: 5)
// and of course the range overload
x.clamped(between: 1 ... 5)

If we finally go with bounded though, I think the more Swifty naming would be bounded(with:) instead of boundedWith.

2 Likes

I've found myself implementing a clamped(to:) extension of some sort in several different projects, and the existence of the Range.clamped(to:) method always tricks me into thinking it's available on Comparable as well.

Without a strong justification I'd probably want to keep the argument constrained to ClosedRange—is there even a reasonable implementation for clamped(to: Range) on an arbitrary Comparable type? The only one I can see just implicitly includes the upper bound, and makes it redundant with the ClosedRange version.

3 Likes

Agreed, it should probably just be something like the following, naming in accordance with the existing ClosedRange.clamped(to:):

extension Comparable {
  func clamped(to limits: ClosedRange<Self>) -> Self {
    return max(limits.lowerBound, min(self, limits.upperBound))
  }
}
3 Likes

I believe a version with Range<_> makes sense as well as long as the range is countable. However, with ranges excluding their upperBound, one can form empty ranges though, so we'd need to return an Optional clamped value:


extension Comparable where Self: Strideable, Self.Stride: SignedInteger {
    func clamped(to range: CountableRange<Self>) -> Self? {
        guard !range.isEmpty else { return nil }
        return min(max(self, range.lowerBound), range.upperBound.advanced(by: -1))
    }
}

5.clamped(to: 0..<3) // 2
5.clamped(to: 0..<1) // 0
5.clamped(to: 0..<0) // nil
2 Likes