"clamp" Swift 中两个值之间的数字的标准方法

Standard way to "clamp" a number between two values in Swift

鉴于:

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

我想知道将这些值限制在给定范围内的最简单、Swift最简单的方法,比如 0...5,这样:

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

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

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

或类似的东西:

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

但我想知道在 Swift 中是否还有其他方法可以做到这一点——特别是,我想知道(并记录在案,因为似乎没有关于钳制的问题Swift 中的数字)Swift 标准库中是否有专门用于此目的的内容。

可能没有,如果有,我也乐意接受。

ClosedInterval 类型已经有一个

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

方法以另一个 interval 作为参数。有一个 Swift 进化邮件列表

上的提案

添加另一种方法,将 单个值 固定到给定区间:

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

这正是您所需要的。

处使用现有clamp()方法的实施

例如,这个额外的clamp()方法可以实现为

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

示例:

(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 泛型 类型

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

因此,这不仅适用于 Double,而且适用于所有 Comparable 类型(如 IntCGFloatString、...):

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

Swift 3 (Xcode 8) 的更新: ClosedInterval 已重命名 到 ClosedRange,它的属性现在是 lower/upperBound

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

在 Swift 3 中有新的 CountableClosedRangeCountableRangeRangeClosedRange 协议。它们具有相同的 upperBoundlowerBound 属性。因此,您可以通过声明自定义协议来使用 clamp 方法一次扩展所有 Range 协议:

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 {}

用法:

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

Swift 4/5

Comparable/Strideable 的扩展类似于标准 Swift 库中的 ClosedRange.clamped(to:_) -> ClosedRange

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

用法:

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

使用与 Apple 相同的语法来执行最小和最大运算符:

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

你可以这样使用:

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

这种方法的妙处在于,任何值都定义了执行操作所需的类型,因此编译器会自行处理。

使用 Swift 5.1,实现所需钳位的惯用方法是 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) }
  }
}

用法:

@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。方法极其简单。

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
    }

虽然我 非常喜欢 Swift 中的范围,但我真的认为 clamp 函数的绝对标准语法(“50 年来每种计算机语言都有”)更简单更好:

x = x.clamped(0.5, 5.0)

直到它内置于 Swift,我真的认为那是最好的。

哲学角:

IMO clamp 函数中的两个值 并不是真正的 'range' - 它们只是“两个值”。

(举个例子:在游戏代码中,有时两个动态值的顺序“错误”(即,期望的结果是外部的东西)或相同(结果是就是那个值)。)

关于结束命名的意见...

对于我们所做的一切,我们坚持明确说明是包容还是排斥。例如,如果有电话

randomIntUpTo( 13 )

事实上我们会命名它

randomIntUpToExclusive( 13 )

或者确实是“包容性”,如果是这样的话。或者取决于语言,例如

randomInt(fromInclusive:  upToExclusive: )

或任何情况。这样绝对不会出现统一错误,也无需讨论任何内容。所有代码名称都应该是自我记录的。所以确实,对我们来说,上面的函数将被命名为

func clamped(fromExclusive: Self, toExclusive: Self)

或任何描述。

但这就是我们。但这是正确的做法:)

根据@Fattie 的回答和我的评论,为了清楚起见,这里是我的建议:

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

最短(但可能不是最有效)的钳制方法是:

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

来源:discussion on Hacker News

中的用户 tobr

扩展 FixedWidthInteger 并创建实例泛型方法以接受 RangeExpression 并处理边缘情况:

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
        }
    }
}

游乐场测试:

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

要使用 FloatingPoint 实现获得相同的结果,您可以将其 nextDown 属性 用于边缘情况:

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
        }
    }
}

游乐场测试:

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