Project

General

Profile

Actions

Feature #22175

open

Add `Range#clamp`

Feature #22175: Add `Range#clamp`
1

Added by nobu (Nobuyoshi Nakada) 2 days ago. Updated 1 day ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:125920]

Description

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:

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:

(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:

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

Beginless and endless ranges are also supported:

(..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:

(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:

(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:

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

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

(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:

(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.

Implementation

GH-17652

Actions

Also available in: PDF Atom