在 Swift 中“夹紧”两个值之间的数字的标准方法

给出:

let a = 4.2
let b = -1.3
let c = 6.4

我想知道最简单的 Swiftiest 方法,把这些值限制在一个给定的范围内,比如说 abc 0,这样:

a -> 4.2
b -> 0
c -> 5

我知道我能做到以下几点:

let clamped = min(max(a, 0), 5)

或者像这样:

let clamped = (a < 0) ? 0 : ((a > 5) ? 5 : a)

但我想知道在 Swift 中是否还有其他方法可以做到这一点ーー特别是,我想知道(以及关于 SO 的文档,因为在 Swift 中似乎没有关于限制数字的问题)在 Swift 标准库中是否有专门用于这一目的的内容。

可能没有,如果有的话,我很乐意接受这个答案。

34206 次浏览

The ClosedInterval type already has a

func clamp(_ intervalToClamp: ClosedInterval<Bound>) -> ClosedInterval<Bound>

method which takes another interval as an argument. There is a proposal on the Swift evolution mailing list

to add another method which clamps a single value to the given interval:

/// Returns `value` clamped to `self`.
func clamp(value: Bound) -> Bound

and that is exactly what you need.

Using the implementation of the existing clamp() method at

as an example, this additional clamp() method can be implemented as

extension ClosedInterval {
func clamp(value : Bound) -> Bound {
return self.start > value ? self.start
: self.end < value ? self.end
: value
}
}

Example:

(0.0 ... 5.0).clamp(4.2)    // 4.2
(0.0 ... 5.0).clamp(-1.3)   // 0.0
(0.0 ... 5.0).clamp(6.4)    // 5.0

ClosedInterval is a generic type

public struct ClosedInterval<Bound : Comparable> { ... }

therefore this works not only for Double but for all types which are Comparable (like Int, CGFloat, String, ...):

(1 ... 3).clamp(10)      // 3
("a" ... "z").clamp("ä") // "ä"

Update for Swift 3 (Xcode 8): ClosedInterval has been renamed to ClosedRange, and its properties are lower/upperBound now:

extension ClosedRange {
func clamp(_ value : Bound) -> Bound {
return self.lowerBound > value ? self.lowerBound
: self.upperBound < value ? self.upperBound
: value
}
}

In Swift 3 there are new CountableClosedRange, CountableRange, Range, ClosedRange protocols. They have the same upperBound and lowerBound properties. So you can extend all Range protocols at once with a clamp method by declaring a custom protocol:

protocol ClampableRange {


associatedtype Bound : Comparable


var upperBound: Bound { get }


var lowerBound: Bound { get }


}


extension ClampableRange {


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


}


extension Range : ClampableRange {}
extension ClosedRange : ClampableRange {}
extension CountableRange : ClampableRange {}
extension CountableClosedRange : ClampableRange {}

Usage:

(0...10).clamp(12) // 10
(0..<100).clamp(-2) // 0
("a"..."c").clamp("z") // c

Swift 4/5

Extension of Comparable/Strideable similar to ClosedRange.clamped(to:_) -> ClosedRange from standard Swift library.

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


#if swift(<5.1)
extension Strideable where Stride: SignedInteger {
func clamped(to limits: CountableClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}
#endif

Usage:

15.clamped(to: 0...10) // returns 10
3.0.clamped(to: 0.0...10.0) // returns 3.0
"a".clamped(to: "g"..."y") // returns "g"


// this also works (thanks to Strideable extension)
let range: CountableClosedRange<Int> = 0...10
15.clamped(to: range) // returns 10

Using the same syntax as Apple to do the min and max operator:

public func clamp<T>(_ value: T, minValue: T, maxValue: T) -> T where T : Comparable {
return min(max(value, minValue), maxValue)
}

You can use as that:

let clamped = clamp(newValue, minValue: 0, maxValue: 1)

The cool thing about this approach is that any value defines the necessary type to do the operation, so the compiler handles that itself.

With Swift 5.1, the idiomatic way to achieve the desired clamping would be with property wrappers. A touched-up example from NSHipster:

@propertyWrapper
struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>


init(wrappedValue: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(wrappedValue))
self.value = wrappedValue
self.range = range
}


var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}

Usage:

@Clamping(0...5) var a: Float = 4.2
@Clamping(0...5) var b: Float = -1.3
@Clamping(0...5) var c: Float = 6.4

2020. The extremely simple way.

extension Comparable {
func clamped(_ f: Self, _ t: Self)  ->  Self {
var r = self
if r < f { r = f }
if r > t { r = t }
// (use SIMPLE, EXPLICIT code here to make it utterly clear
// whether we are inclusive, what form of equality, etc etc)
return r
}

While I truly love ranges in Swift, I really think the absolutely standard syntax for a clamp function ("for 50 years now in every computer language") is just simpler and better:

x = x.clamped(0.5, 5.0)

Until it is built-in to Swift, really I think that's best.

Philosophical corner:

IMO the two values in a clamp function are not really a 'range' - they're just "two values".

(Just for example: it's completely common in game code to have the two dynamic values sometimes be in the "wrong order" (i..e, the desired result is something outside) or the same (the result is just that value).)

An opinion on end-naming ...

On everything we do, we insist on explicitly stating whether inclusive or exclusive. For example if there's a call

randomIntUpTo( 13 )

in fact we will name it

randomIntUpToExclusive( 13 )

or indeed "inclusive" if that is the case. Or depending on the language something like

randomInt(fromInclusive:  upToExclusive: )

or whatever the case may be. In this way there is absolutely never ever ever a unity error, and nothing needs to be discussed. All code names should be self-documenting. So indeed, for us, the function above would be named

func clamped(fromExclusive: Self, toExclusive: Self)

or whatever describes it.

But that's just us. But it's the right thing to do :)

Following up on @Fattie's answer and my comment, here's my suggestion for clarity:

extension Comparable {
func clamped(_ a: Self, _ b: Self) -> Self {
min(max(self, a), b)
}
}

The shortest (but maybe not most efficient) way to clamp, is:

let clamped = [0, a, 5].sorted()[1]

Source: user tobr in a discussion on Hacker News

Extending FixedWidthInteger and creating an instance generic method to accept a RangeExpression and taking care of the edge cases:

extension FixedWidthInteger {
func clamped<R: RangeExpression>(with range: R) -> Self where R.Bound == Self {
switch range {
case let range as ClosedRange<Self>:
return Swift.min(range.upperBound, Swift.max(range.lowerBound, self))
case let range as PartialRangeFrom<Self>:
return Swift.max(range.lowerBound, self)
case let range as PartialRangeThrough<Self>:
return Swift.min(range.upperBound, self)
case let range as Range<Self>:
return Swift.min(range.dropLast().upperBound, Swift.max(range.lowerBound, self))
case let range as PartialRangeUpTo<Self>:
return Swift.min(range.upperBound.advanced(by: -1), self)
default: return self
}
}
}

Playground testing:

100.clamped(with: 1...)     // 100
100.clamped(with: ..<100)   // 99
100.clamped(with: ...100)   // 100
100.clamped(with: 1..<100)  // 99
100.clamped(with: 1...100)  // 100


0.clamped(with: 1...)       // 1
0.clamped(with: ..<100)     // 0
0.clamped(with: ...100)     // 0
0.clamped(with: 1..<100)    // 1
0.clamped(with: 1...100)    // 1

To achieve the same results with a FloatingPoint implementation you can use its nextDown property for the edge cases:

extension BinaryFloatingPoint {
func clamped<R: RangeExpression>(with range: R) -> Self where R.Bound == Self {
switch range {
case let range as ClosedRange<Self>:
return Swift.min(range.upperBound, Swift.max(range.lowerBound, self))
case let range as PartialRangeFrom<Self>:
return Swift.max(range.lowerBound, self)
case let range as PartialRangeThrough<Self>:
return Swift.min(range.upperBound, self)
case let range as Range<Self>:
return Swift.min(range.upperBound.nextDown, Swift.max(range.lowerBound, self))
case let range as PartialRangeUpTo<Self>:
return Swift.min(range.upperBound.nextDown, self)
default: return self
}
}
}

Playground testing:

let value = 100.0


value.clamped(with: 1...)     // 100
value.clamped(with: ..<100)   // 99.99999999999999
value.clamped(with: ...100)   // 100
value.clamped(with: 1..<100)  // 99.99999999999999
value.clamped(with: 1...100)  // 100