1

I'm looking for a more elegant way to create bounded limiters for numbers, primarily to be used in setters. There are plenty of techniques for determining whether a value falls within bounds, but I don't see any native functions for forcing an incoming value to conform to those bounds.

The accepted answer here comes close but I want to cap the values rather than merely enforce them.

Here's what I have so far. I'm not sure about the Int extension. And I'd prefer to collapse the if-else into a single elegant line of code, if possible. Ideally I'd like to shorten the actual implementation in the struct, as well.

extension Int {
    func bounded(_ min: Int, _ max: Int) -> Int {
        if self < min {
            return min
        } else if self > max {
            return max
        } else {
            return self
        }
    }
}

print(5.bounded(4, 6))  // 5
print(5.bounded(1, 3))  // 3
print(5.bounded(6, 9))  // 6

// Used in a sentence
struct Animal {
    var _legs: Int = 4
    var legs: Int {
        get {
            return _legs
        }
        set {
            _legs = newValue.bounded(1, 4)
        }
    }
}

var dog = Animal()
print(dog.legs) // 4
dog.legs = 3
print(dog.legs) // 3
dog.legs = 5
print(dog.legs) // 4
dog.legs = 0
print(dog.legs) // 1
Community
  • 1
  • 1
Justin Whitney
  • 1,242
  • 11
  • 17
  • 2
    Take a look [this Q&A](http://stackoverflow.com/q/36110620/2976878) – personally, I like the approach shown in [this answer](http://stackoverflow.com/a/40868784/2976878). – Hamish Mar 19 '17 at 17:31
  • 2
    Although I don't see anything wrong with the if-else statement – `min(max(self, limits.lowerBound), limits.upperBound)` is cute, but I find the if-else much more readable. Although the ternary operator ([as shown by Martin](http://stackoverflow.com/a/36111464/2976878)) is a nice alternative. – Hamish Mar 19 '17 at 17:39
  • So, if I got this well, you just want to organize things up and make the code cleaner, right? I personally think this question would be more suited to [Code Review](http://codereview.stackexchange.com/tour). – Mr. Xcoder Mar 19 '17 at 17:44
  • @Mr.Xcoder - that's exactly right. – Justin Whitney Mar 19 '17 at 17:47
  • @Hamish - thank you! That's exactly what I was looking for. I was sniffing around nested min/max, also trying to incorporate generics, but I didn't think of extending `Comparable` - that's brilliant. – Justin Whitney Mar 19 '17 at 17:48
  • 1
    @JustinWhitney a good alternative to the `if-else` would be `return self < min ? min : self > max ? max : self`. I think it's very readable and way more compact. – Mr. Xcoder Mar 19 '17 at 17:55
  • Man, why are all these great answers added as Comments? Maybe I don't understand SO enough to know what constitutes an answer? In any case, thank you, @Mr.Xcoder - I do like this better than nested min/max. I also wonder if there's a difference, albeit slight, in the computation time of the two. – Justin Whitney Mar 19 '17 at 17:59
  • "but I want to cap the values rather than merely enforce them." what do you mean? – Alexander Mar 19 '17 at 18:00
  • @Alexander I'm seeing now that the terminology is "clamp". What I mean is that in the example posted, the return was `nil` if the `newValue` fell out of range. That's enforcement. Clamping, or as I was calling it capping, the value means returning the `min` or `max` rather than `nil`. – Justin Whitney Mar 19 '17 at 18:02
  • @JustinWhitney Oh look, that's my answer! It could also be applied here by setting the `value` to a provided min or max as appropriate. – Alexander Mar 19 '17 at 18:06
  • 1
    @JustinWhitney I advise against what you're trying to do, with the animal example. I think it's better to throw an error when such invalid values are provided (e.g. a dog with 5 legs) rather than carrying on as if they meant 4. They didn't, they meant 5. – Alexander Mar 19 '17 at 18:07
  • "I'm seeing now that the terminology is "clamp"." Correct. This is a standard thing to want to do, and has been well discussed. Even Apple has some very nice example clamp code. – matt Mar 19 '17 at 18:09
  • Why the downvote? Is it because I didn't know the correct terminology for this? – Justin Whitney Mar 19 '17 at 19:19

3 Answers3

5

This is Apple's own approach, taken from this sample code:

func clamp<T: Comparable>(value: T, minimum: T, maximum: T) -> T {
    return min(max(value, minimum), maximum)
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Couldn't get better: It is short and readable, can be applied to multiple types and it suits the OP's request perfectly. – Mr. Xcoder Mar 19 '17 at 18:15
  • 1
    Not a fan of using 2 separate parameter for ranges like this – Alexander Mar 19 '17 at 18:21
  • @Alexander, why? Looking at this: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/clamp.xhtml this function is *exactly* what OpenGL Shading Language does. Sometimes you shouldn't think being "Swifty", just go with what's been optimized since last century. –  Mar 19 '17 at 18:52
  • 2
    @dfd **1)** It's completely senseless to compare the design of a low level language like C or GLSL to a high level one like Swift. **2)** Ranges are just as optimized as using two separate variables. `ClosedRange` is a struct, thus a value type that's stored on the stack. It's not like there's object allocation overhead or anything. **3)** Using a range also happens to enforce the (otherwise unconsidered) requirement that `min < max`. **4)** Using a range makes it more consistent with other APIs, so interoperation with other Swift code is easier. – Alexander Mar 19 '17 at 18:58
  • @Alexander, thanks. I think I still prefer this answer, but now at least I understand the reasons behind your comment. I particularly like reason #3. –  Mar 19 '17 at 19:05
3

I would generalize this extension to any Comparable, so that more types can benefit from it. Also, I would change the parameter to be a ClosedRange<Self> rather than two separate Self parameters, because that's the more common way of handling ranges in Swift. That'll come in especially handy when dealing with array indices.

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

// Usage examples:
10.clamped(to: 0...5) // => 5
"a".clamped(to: "x"..."z") // => "x"
-1.clamped(to: 0...1) // => 0
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • I prefer this implementation because of the reasons outlined in the other comments, particularly the use of `ClosedRanged` to enforce ordered min/max values. – Justin Whitney Mar 21 '17 at 16:35
0

A very clean alternative to your if-else statements, keeping the readability would be:

extension Comparable{
    func clamp(_ min: Self,_ max: Self) -> Self{
        return min...max~=self ? self : (max < self ? max : min)
    }
}

I think this is a good alternative to using a range as parameter, because, in my opinion, it is annoying to write 6.clamp((4...5)) each time insted of 6.clamp(4,5).

When it comes to your struct, I think you should not use this clamp extension at all, because, say, 100 does not mean 4... I cannot see the reason for doing this, but it's up to you.

Mr. Xcoder
  • 4,719
  • 5
  • 26
  • 44