[Revision] Fixing SE-0177: Add clamp(to:) to the stdlib

Hi, is there an update on this proposal? Is this something that needs to be completed before ABI lock-in or can it wait?

@Douglas_Gregor commented on the PR at (Returned for Revisions) [SE-0177] Revise clamping proposal by Nirma · Pull Request #723 · apple/swift-evolution · GitHub saying it was put on ice pending further discussion but I haven't found anything further.

It would be great to see this included – It's something that I seem to encounter on a near daily basis.

I've attempted my own version with the suggestions of @xwu. A deviation from the original proposal is to constrain the ClosedRange and PartialRangeUpTo variants to FloatingPoint types. clamping to something like (0..<0) would be undefined anyhow, but FloatingPoint at least allows us to return .nan in this case – so I think this might make more sense than Strideable which doesn't seem to have the concept of a minimum stride as far as I can tell.

// Comparable extension

extension Comparable {
    func clamped<T: ClampableRangeExpression>(to range: T) -> Self where T.Bound == Self {
        return range.clamping(self)
    }
}

// RangeExpression extension

protocol ClampableRangeExpression: RangeExpression {
    func clamping(_ value: Bound) -> Bound
}

// Conformance for various Range Types

extension PartialRangeFrom: ClampableRangeExpression {
    func clamping(_ value: Bound) -> Bound {
        return contains(value) ? value : lowerBound
    }
}

extension PartialRangeThrough: ClampableRangeExpression {
    func clamping(_ value: Bound) -> Bound {
        return contains(value) ? value : upperBound
    }
}

extension ClosedRange: ClampableRangeExpression {
    func clamping(_ value: Bound) -> Bound {
        return (lowerBound...).clamping((...upperBound).clamping(value))
    }
}

// Constrained conformance for relevant Range Types

extension PartialRangeUpTo: ClampableRangeExpression where Bound: FloatingPoint {
    func clamping(_ value: Bound) -> Bound {
        return (...upperBound.nextDown).clamping(value)
    }
}

extension Range: ClampableRangeExpression where Bound: FloatingPoint {
    func clamping(_ value: Bound) -> Bound {
        guard lowerBound != upperBound else { return .nan }
        return (lowerBound...).clamping((..<upperBound).clamping(value))
    }
}

The alternative is the simple:

public func clamped<T>(_ lowerBound: T, _ upperBound: T, _ value: T) -> T where T : Comparable {
    return min(upperBound, max(lowerBound, value))
}

Which seems like it would fit alongside the existing min and max implementations quite neatly to me, but I understand there has been some discussion which discourages this direction.

Either way, it would be good to get a solution of some sort.