Project

General

Profile

Feature #22175

Updated by nobu (Nobuyoshi Nakada) 3 months ago

I would like to propose `Range#clamp`, which returns a new `Range` whose begin and end values are clamped to the given bounds. 

 Proposed call-seq: 

 ```ruby 
 range.clamp(min, max) -> range 
 range.clamp(bounds)     -> range 
 ``` 

 This is a range counterpart of `Comparable#clamp`. While `Comparable#clamp` clamps a single value, `Range#clamp` clamps both endpoints of a range. 

 Examples: 

 ```ruby 
 (1..10).clamp(3, 7)         #=> 3..7 
 (1...10).clamp(3, 7)        #=> 3..7 
 (1...10).clamp(3, 10)       #=> 3...10 
 (0...).clamp(0, 10)         #=> 0..10 

 (1..10).clamp(3..7)         #=> 3..7 
 (1..10).clamp(3...7)        #=> 3...7 
 (1..5).clamp(3...7)         #=> 3..5 
 ``` 

 `clamp(min, max)` behaves like clamping by an inclusive range `min..max`. If an exclusive upper bound is needed, a range argument can be used: 

 ```ruby 
 (1..10).clamp(3...7)        #=> 3...7 
 ``` 

 Beginless and endless ranges are also supported: 

 ```ruby 
 (..10).clamp(3, 7)          #=> 3..7 
 (0...).clamp(0, 10)         #=> 0..10 

 (1..10).clamp(..7)          #=> 1..7 
 (1..10).clamp(...7)         #=> 1...7 
 (1..10).clamp(3..)          #=> 3..10 
 ``` 

 If the receiver is entirely outside the clamping bounds, the returned range is empty: 

 ```ruby 
 (1..10).clamp(20..30)       #=> 20...20 
 (1..10).clamp(-10..0)       #=> 0...0 
 (1..).clamp(..0)            #=> 0...0 
 ``` 

 The returned range excludes its end when the returned end value is an excluded end value of either the receiver or the argument range: 

 ```ruby 
 (1...10).clamp(3, 10)       #=> 3...10 
 (1..10).clamp(3...10)       #=> 3...10 
 ``` 

 Otherwise, the returned range includes its end. 

 #### Relation to [Feature #16757] 

 [Feature #16757] proposes `Range#intersection` / `Range#&` as a general operation for intersecting two ranges. 

 `Range#clamp` is closely related, but intentionally narrower. It treats the argument as clamping bounds for the receiver, similar to how `Comparable#clamp` treats its arguments as bounds for one value. 

 For overlapping ranges, `range.clamp(bounds)` often produces the same result as a range intersection. For example: 

 ```ruby 
 (1..10).clamp(3..7)         #=> 3..7 
 ``` 

 However, `clamp` has a bounds-oriented API and naturally supports the two-argument form: 

 ```ruby 
 (1..10).clamp(3, 7)         #=> 3..7 
 ``` 

 Also, when the receiver is outside the bounds, `clamp` returns an empty `Range` at the nearest bound, rather than needing to decide whether a general intersection operation should return `nil`, `[]`, an empty range, or raise: 

 ```ruby 
 (1..10).clamp(20..30)       #=> 20...20 
 ``` 

 So this proposal can be considered either independently, as a range counterpart of `Comparable#clamp`, or as a smaller operation that could coexist with a future `Range#intersection`. 

 #### Motivation 

 It is common to restrict ranges to known boundaries, for example when limiting source locations, pagination windows, numeric domains, date/time windows, or user-provided ranges. 

 Currently this has to be written manually by clamping both endpoints and reconstructing the range while preserving the correct excluded-end behavior. That logic is easy to get subtly wrong, especially with exclusive ranges, beginless/endless ranges, and ranges that become empty after clamping. 

 `Range#clamp` would provide a small, direct API for this operation. 

 #### Notes 

 The method returns a new `Range` instance. 

 The single-argument form accepts a range-like object accepted by Ruby’s range conversion logic. 

 Source checked: [[Feature #16757]: Add intersection to Range](https://bugs.ruby-lang.org/issues/16757). 

 #### Implementation 

 [GH-17652](https://github.com/ruby/ruby/pull/17652) 

Back