[ruby-core:125920] [Ruby Feature#22175] Add `Range#clamp`
Issue #22175 has been reported by nobu (Nobuyoshi Nakada). ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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). -- https://bugs.ruby-lang.org/
Issue #22175 has been updated by zverok (Victor Shepelev). +1 for this proposal. Needed this quite a few times. ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175#change-117890 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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) -- https://bugs.ruby-lang.org/
Issue #22175 has been updated by mame (Yusuke Endoh). For the record, here is the use case I had in mind when talking with @nobu: When traversing the 3x3 neighborhood of a cell (x, y) on a 2D grid of width x height, one is tempted to write code like this: ```ruby (y - 1).upto(y + 1) do |ny| (x - 1).upto(x + 1) do |nx| grid[ny][nx] end end ``` However, this may access cells outside the grid, so in practice we have to write something like the following, which I don't find very readable: ```ruby [y - 1, 0].max.upto([y + 1, height - 1].min) do |ny| [x - 1, 0].max.upto([x + 1, width - 1].min) do |nx| grid[ny][nx] end end ``` With `Range#clamp`, it can be written as: ```ruby (y - 1 .. y + 1).clamp(0...height).each do |ny| (x - 1 .. x + 1).clamp(0...width).each do |nx| grid[ny][nx] end end ``` That said, I'm a bit concerned about creating a Range object on every iteration, so I'm not sure I would actually use this in a hot loop. So I'm not super enthusiastic about this proposal, but I can imagine cases where it would be handy, so I'm not against it either. ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175#change-117897 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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) -- https://bugs.ruby-lang.org/
Issue #22175 has been updated by Eregon (Benoit Daloze). This looks nice but I think Range intersection is more general and more useful. It's also symmetrical which seems a nice property. Manually implementing Range intersection is not trivial and having built-in would be great. We already have `Set#intersection` and `Set#&` so this would be the Range variant of that. Are there use cases where `Range#clamp` would work but `Range#intersection` wouldn't, i.e. where the asymmetry would be useful? ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175#change-117937 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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) -- https://bugs.ruby-lang.org/
Issue #22175 has been updated by Eregon (Benoit Daloze). Regarding the 3x3 neighborhood of a cell, my favorite solution is this: ```ruby board = {} # x+y.i => v fill(board) [-1i-1, -1i, -1i+1, -1, +1, 1i-1, 1i, 1i+1].each do { |dir| board[pos + dir] } ``` It avoids the nested loop and the need to check for bounds since `Hash#[]` is just `nil` if out of bounds. ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175#change-117938 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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) -- https://bugs.ruby-lang.org/
Issue #22175 has been updated by mame (Yusuke Endoh). I feel `clamp` fits better here, since what I want is to "clamp" the range ends into the given bounds. I personally dislike `intersection` for a few reasons: (1) its set-theoretic name adds cognitive load to read/write, (2) the name is too long, and (3) having `intersection` makes me expect a `union` too. `clamp` avoids all of them. ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175#change-117946 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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) -- https://bugs.ruby-lang.org/
Issue #22175 has been updated by nobu (Nobuyoshi Nakada). Eregon (Benoit Daloze) wrote in #note-5:
This looks nice but I think Range intersection is more general and more useful.
It’s true that [Feature #16757] is more general, but as a result, it exhibits strange behavior in edge cases. For example, `(1..10).intersection(20..30)` returns an empty `Array` rather than a `Range`.
Are there use cases where `Range#clamp` would work but `Range#intersection` wouldn't, i.e. where the asymmetry would be useful?
`Range#clamp` is intentionally designed to be asymmetric in order to avoid such edge cases. ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175#change-117968 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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) -- https://bugs.ruby-lang.org/
Issue #22175 has been updated by matz (Yukihiro Matsumoto). Accepted. `Range#clamp` is a natural range counterpart of `Comparable#clamp`, and the use case (restricting a range to known boundaries) is common enough. Regarding `Range#intersection` (#16757): I see them as different operations, even though they often produce the same result. `clamp` is asymmetric by design; the argument is bounds, not a peer range, which is why the two-argument form `clamp(min, max)` makes sense and why returning an empty range at the nearest bound is a natural answer for disjoint cases. A symmetric `intersection` would have to decide between `nil` and an empty range for disjoint inputs. Also, once we have `intersection`, people would naturally expect `union` as well, but `Range#union` cannot be defined in general because the union of two ranges may be discontiguous. These points can be discussed separately in #16757; accepting `clamp` does not preclude `intersection`. Matz. ---------------------------------------- Feature #22175: Add `Range#clamp` https://bugs.ruby-lang.org/issues/22175#change-117997 * Author: nobu (Nobuyoshi Nakada) * Status: Open ---------------------------------------- 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) -- https://bugs.ruby-lang.org/
participants (5)
-
Eregon (Benoit Daloze) -
mame (Yusuke Endoh) -
matz (Yukihiro Matsumoto) -
nobu (Nobuyoshi Nakada) -
zverok (Victor Shepelev)