Standard way to “clamp” a number between two values in Swift

后端 未结 8 1239
梦谈多话
梦谈多话 2020-12-02 15:10

Given:

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

I want to know the simplest, Swiftiest way to clamp these values to a given range, say 0...

相关标签:
8条回答
  • 2020-12-02 15:37

    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)
        }
    }
    
    // Swift < 5.1
    extension Strideable where Stride: SignedInteger {
        func clamped(to limits: CountableClosedRange<Self>) -> Self {
            return min(max(self, limits.lowerBound), limits.upperBound)
        }
    }
    

    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
    
    0 讨论(0)
  • 2020-12-02 15:37

    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.

    0 讨论(0)
  • 2020-12-02 15:46

    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

    • Add clamp(value: Bound) -> Bound to ClosedInterval

    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

    • https://github.com/apple/swift/blob/master/stdlib/public/core/Interval.swift.gyb

    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
        }
    }
    
    0 讨论(0)
  • 2020-12-02 15:55

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

    extension Comparable {
        func clamped(_ a: Self, _ b: Self) -> Self {
            max(min(self, a), b)
        }
    }
    
    0 讨论(0)
  • 2020-12-02 16:00

    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).)

    0 讨论(0)
  • 2020-12-02 16:00

    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
    
    0 讨论(0)
提交回复
热议问题