Issue #14784 has been updated by zverok (Victor Shepelev).


@nobu is there any chance you can reconsider your implementation?

The important justification for the proposal was using `#clamp` with open ranges (begin- and end-less), but your implementation just rejects them all (with misleading error message).

```ruby
1.clamp(0..) # ArgumentError (cannot clamp with an exclusive range)
1.clamp(2..) # ArgumentError (cannot clamp with an exclusive range)
1.clamp(..2) # ArgumentError (cannot clamp with an exclusive range) 
1.clamp(..0) # ArgumentError (cannot clamp with an exclusive range)
1.clamp(0...3) # ArgumentError (cannot clamp with an exclusive range)
```

Here is behavior of my implementation (I developed it yesterday, because Redmine doesn't send "Closed via changeset" notifications so I believed the proposal still waits for implementation):

```ruby
1.clamp(0..) #=> 1
1.clamp(2..) #=> 2
1.clamp(..2) #=> 1
1.clamp(..0) #=> 0
1.clamp(0...3) #=> 1
1.clamp(-1...0) # ArgumentError: #clamp with excluding end can't clamp from top -- the only prohibited situation
```
I believe this behavior is much more PoLS and useful for lot of cases.

Here is the implementation:
```c
VALUE cmp_clamp2(VALUE x, VALUE min, VALUE max)
{
    int c;

    if (cmpint(min, max) > 0) {
    rb_raise(rb_eArgError, "min argument must be smaller than max argument");
    }

    c = cmpint(x, min);
    if (c == 0) return x;
    if (c < 0) return min;
    c = cmpint(x, max);
    if (c > 0) return max;
    return x;
}

VALUE cmp_clamp_range(VALUE x, VALUE range)
{
    VALUE beg, end;

    if (!rb_obj_is_kind_of(range, rb_cRange)) {
        rb_raise(rb_eArgError, "#clamp with 1 argument expects Range");
    }

    beg = RANGE_BEG(range);
    end = RANGE_END(range);

    if (!NIL_P(beg) && !NIL_P(end) && cmpint(beg, end) > 0) {
        rb_raise(rb_eArgError, "range begin is larger than range end");
    }

    if (!NIL_P(beg) && cmpint(x, beg) <= 0) {
        return beg;
    }
    if (!NIL_P(end) && cmpint(x, end) >= 0) {
        if (RANGE_EXCL(range)) {
            rb_raise(rb_eArgError, "#clamp with excluding end can't clamp from top");
        }
        return end;
    }

    return x;
}


/*
 *  call-seq:
 *     obj.clamp(min, max) ->  obj
 *     obj.clamp(range)    ->  obj
 *
 * In <code>(min, max)</code> form, returns _min_ if _obj_
 * <code><=></code> _min_ is less than zero, _max_ if
 * _obj_ <code><=></code> _max_ is greater than zero and
 * _obj_ otherwise.
 *
 *     12.clamp(0, 100)         #=> 12
 *     523.clamp(0, 100)        #=> 100
 *     -3.123.clamp(0, 100)     #=> 0
 *
 *     'd'.clamp('a', 'f')      #=> 'd'
 *     'z'.clamp('a', 'f')      #=> 'f'
 *
 * In <code>(range)</code> form, returns <i>range.begin</i> if <i>obj</i>
 * <code><=></code> <i>range.begin</i> is less than zero,
 * <i>range.end</i> if <i>obj</i> <code><=></code> <i>range.end</i>
 * is greater than zero and <i>obj</i> otherwise.
 *
 *     12.clamp(0..100)          #=> 12
 *     523.clamp(0..100)         #=> 100
 *     -3.123.clamp(0..100)      #=> 0
 *
 *     # Works with endless/beginless ranges:
 *     -20.clamp(0..)            #=> 0
 *     523.clamp(..100)          #=> 100
 *
 *     # When range excludes end, and the value is more than end,
 *     # an exception is raised.
 *     523.clamp(0...100)       # ArgumentError: #clamp with excluding end can't clamp from top
 *
 */

static VALUE
cmp_clamp(int argc, const VALUE *argv, VALUE x)
{
    rb_check_arity(argc, 1, 2);
    if (argc == 2) {
        return cmp_clamp2(x, argv[0], argv[1]);
    }
    return cmp_clamp_range(x, argv[0]);
}
```

...and tests for it:
```ruby
  def test_clamp_range
    cmp->(x) do 0 <=> x end
    assert_equal(1, @o.clamp(1..2))
    assert_equal(-1, @o.clamp(-2..-1))
    assert_equal(@o, @o.clamp(-1..3))

    assert_equal(1, @o.clamp(1..1))
    assert_equal(@o, @o.clamp(0..0))

    assert_equal(1, @o.clamp(1..))
    assert_equal(-1, @o.clamp(..-1))
    assert_equal(@o, @o.clamp(-2...1))

    assert_raise_with_message(ArgumentError, "#clamp with excluding end can't clamp from top") {
      @o.clamp(-2...-1)
    }

    assert_raise_with_message(ArgumentError, 'range begin is larger than range end') {
      @o.clamp(2..1)
    }

    assert_raise_with_message(ArgumentError, '#clamp with 1 argument expects Range') {
      @o.clamp(2)
    }
  end
```

----------------------------------------
Feature #14784: Comparable#clamp with a range
https://bugs.ruby-lang.org/issues/14784#change-82316

* Author: zverok (Victor Shepelev)
* Status: Closed
* Priority: Normal
* Assignee: 
* Target version: 
----------------------------------------
**Proposal**

Allow "one-sided" `clamp` to limit only upper bound (and, ideally, only lower too).

Proposed implementation: allow `clamp(begin..end)` call sequence (without deprecating `clamp(begin, end)`), to take advantage from open-ended ranges with `clamp(begin..)`.

**Reasoning about range**

I looked through `#clamp` [discussion](https://bugs.ruby-lang.org/issues/10594), but couldn't find there why syntax `clamp(b, e)` was preferred to `clamp(b..e)`. The only one I could think of is possible confuse of how `clamp(b..e)` and `clamp(b...e)` behaviors should differ.

The problem becomes more important with the introduction of [open-ended ranges](https://bugs.ruby-lang.org/issues/12912). I believe this is pretty natural:

```ruby
some_calculation.clamp(0..)    # now, I use clamp(0, Float::INFINITY)
timestamp.clamp(Date.today..)  # now, I typically use clamp(Date.today..INFINITE_FUTURE_DATE) with custom defined constant
```

Counter-arguments:

1. This is "one-sided", you can't do `clamp(..Date.today)`. To this I can answer than from my experience "clamping only minimum" is more frequent, and if you need to clamp only maximum, most of the time there is some "reasonable minumum". Another idea is that maybe this is a proof why "start-less" ranges are necessary, after all, [doubted here](https://bugs.ruby-lang.org/issues/12912#note-12)
2. Why not just leave current `clamp(b, e)` and allow `clamp(b)`? Answer: because when you see `clamp(10)`, is it `clamp(10, nil)`, or `clamp(nil, 10)` (yes, logically it is the first argument that is left, but from readability point of view it is not that obvious). Possible alternative: `clamp(min: 0, max: 10)`, where you can omit any of two.
3. Why do you need one-sided clamp at all? Because alternatives is much more wordy, making reader think:

```ruby
# with clamp
chain.of.calculations.clamp(0..)

# without clamp
v = chain.of.calculations
v < 0 ? 0 : v

# or, with yield_self (renamed to then)
chain.of.calculations.then { |v| v < 0 ? 0 : v }
```

Both alternatives "without `#clamp`" shows intentions much less clear.



-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>