Project

General

Profile

Feature #12133

Ability to exclude start when defining a range

Added by slash_nick (Ryan Hosford) over 1 year ago. Updated over 1 year ago.

Status:
Feedback
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:74086]

Description

An intuitive, approach would be to allow defining ranges like so:

[1..10]
[1..10)
(1..10]
(1..10)

... where a square bracket indicates boundary inclusion and a parenthesis represents boundary exclusion. The syntax there is obviously not going to work, but it demonstrates the idea.

A more feasible, still intuitive, solution might look like the following

(1..10)                # [1..10]
(1...10)               # [1..10) ... Alternatively: (1..10).exclude_end
(1..10).exclude_start  # (1..10]
(1...10).exclude_start # (1..10) ... Alternatively: (1..10).exclude_start.exclude_end

For consistency, I think we'd also want to add #exclude_start? & #exclude_end methods.

History

#1 [ruby-core:74333] Updated by shyouhei (Shyouhei Urabe) over 1 year ago

  • Status changed from Open to Feedback

Do you have any practical situation where this is useful? The proposed grammar is ultra-hard to implement at sight (if not impossible). You are advised to show us why this feature is worth tackling.

#2 [ruby-core:74334] Updated by nobu (Nobuyoshi Nakada) over 1 year ago

Combination of different parens would be problematic for many reasons, e.g., editors.
Maybe, neko operator in Perl6?

#3 [ruby-core:74536] Updated by slash_nick (Ryan Hosford) over 1 year ago

Please accept apologies for confusion caused by non-matching parenthesis/brackets -- this notation is just a standard way of denoting intervals in mathematics (ref. ISO 31-11 & Wikipedia entry for including excluding endpoints in intervals).

I'm not proposing we support non-matching parenthesis or brackets. Here's what I'm proposing:

# Including both endpoints ("a" and "b").
(a..b)

# Excluding endpoint "b"
(a...b) # OR
(a..b).exclude_end

# Excluding endpoint "a"
(a..b).exclude_start

# Excludes both endpoints ("a" and "b").
(a...b).exclude_start # OR
(a..b).exclude_start.exclude_end

I think this is useful because it would give the ruby language a more complete implementation of ranges/intervals. I recently built a feature that requires me to cover the range from a to b with 2 to 5 sub-ranges, validating that there are no gaps and no overlaps in the sub-ranges. I also needed to be flexible with choosing into which sub-range an endpoint should fall. Examples: If your systolic blood pressure is 120 mmHg, do I say you are in a normal range or do I say you have Prehypertension? If your HBA1C is 6.5%, are you in a pre-diabetic condition or do you have diabetes?

I believe I could've written a simpler solution with a more complete range implementation.

Thank you, Shyouhei and Nobu, for your responses.

#4 [ruby-core:74577] Updated by Eregon (Benoit Daloze) over 1 year ago

Ryan Hosford wrote:

I'm not proposing we support non-matching parenthesis or brackets. Here's what I'm proposing:

Am I missing something or the ability to exclude start could be done with just (start.next..last) ?
If that's the case, it seems to be too much of an effort to add new syntax, or to complicate further Range's implementation.

#5 [ruby-core:74582] Updated by Eregon (Benoit Daloze) over 1 year ago

On Fri, Mar 25, 2016 at 9:58 PM, Matthew Kerwin wrote:

That's only true for ranges of discrete values. A range like: (0.0..1.0).exclude_first would look absolutely horrible as 0.0.next_float..1.0, and I don't think there's even an equivalent for Rationals or Times.

Ah right, in the case a Range is used mostly for #include? and co, then .next is not really a good fix.
Please disregard my comment then.
Maybe Range should use another method for iterating over values so it would behave more consistently.

#6 [ruby-core:74587] Updated by naruse (Yui NARUSE) over 1 year ago

Ryan Hosford wrote:

I think this is useful because it would give the ruby language a more complete implementation of ranges/intervals. I recently built a feature that requires me to cover the range from a to b with 2 to 5 sub-ranges, validating that there are no gaps and no overlaps in the sub-ranges. I also needed to be flexible with choosing into which sub-range an endpoint should fall. Examples: If your systolic blood pressure is 120 mmHg, do I say you are in a normal range or do I say you have Prehypertension? If your HBA1C is 6.5%, are you in a pre-diabetic condition or do you have diabetes?

On such case, an implementation will be something like following.
If you want more easy to read one, which declares inclusive-or-exclusive range and validates there's no duplication,
it seems something different one from current simple range object, but more complex one.

case ARGV.shift.to_r
when 0..20.1.to_r
  puts "low"
when 20.1.to_r...23.4.to_r
  puts "middle"
when 23.4.to_r..99
  puts "high"
end

#7 [ruby-core:74591] Updated by slash_nick (Ryan Hosford) over 1 year ago

Yui NARUSE wrote:

On such case, an implementation will be something like following.
If you want more easy to read one, which declares inclusive-or-exclusive range and validates there's no duplication,
it seems something different one from current simple range object, but more complex one.

case ARGV.shift.to_r
when 0..20.1.to_r
  puts "low"
when 20.1.to_r...23.4.to_r
  puts "middle"
when 23.4.to_r..99
  puts "high"
end

Here you've used the case statement to exclude start on the middle range. The problem is the code is largely static while the values of the endpoints (and which ranges should include those endpoints) may change. Physicians may all agree that 20.1 is in a low range today, but tomorrow?

I think this workaround could be made to work ( if 'middle' range is exclude start, 'low' needs #..; if 'middle' range is not exclude start, 'low' needs #...) but it would be tricky, unintuitive, complicated, etc..

#8 [ruby-core:74610] Updated by naruse (Yui NARUSE) over 1 year ago

Ryan Hosford wrote:

Here you've used the case statement to exclude start on the middle range. The problem is the code is largely static while the values of the endpoints (and which ranges should include those endpoints) may change. Physicians may all agree that 20.1 is in a low range today, but tomorrow?

I think this workaround could be made to work ( if 'middle' range is exclude start, 'low' needs #..; if 'middle' range is not exclude start, 'low' needs #...) but it would be tricky, unintuitive, complicated, etc..

What I want to ask is "Is the range extension really helps developers?".

I can implement the same behavior with separating data and code like following:

x = 23.5r
ranges = [(0..20.1r), (20.1...23.4r), (23.4r..99)]
values = %w[low middle high]
idx = ranges.index{|range|range.include?(x)}
puts values[idx]

Does the new feature enables more clear code?

#9 [ruby-core:74654] Updated by slash_nick (Ryan Hosford) over 1 year ago

Here's what I would've written: (see: sample range_sections)

def which_range?(value)
  range = nil

  range_sections.each do |rs|
    range = Range.new(rs.start, rs.end, rs.end_condition != "=", rs.start_condition != "=")

    break if range.include?(value)

    range = nil
  end

  range
end

And here is what I can write with what's possible now:

def which_range?(value)
  range = nil

  range_sections.each do |rs|
    range = Range.new(rs.start, rs.end, rs.end_condition == "=")

    break if range.include?(value) && (value == range.begin && rs.start_condition != "=")

    range = nil
  end

  range
end

I believe the first is more clear. The real benefit to the first is that the returned range would convey the appropriate boundary information.

What this issue is asking:

  • Is #exclude_end? meaningful and useful?
  • Would #exclude_start? also be meaningful and useful?
  • If #exclude_end? is meaningful and useful, should we add #exclude_start??

If we decide we want it, we'd need to consider another thing:

  • Do we need a shorthand? (something like the neko operator as Nobu mentioned). Examples:
^1..10^ #=> something we can't do now, but equivalent to ^1...10: 1  through 10, excluding both 1 and 10
10...20 #=> something we can do now, it is equivalent to 10..20^: 10 through 20, excluding 20
20..30  #=> something we can do now, it is equivalent to 10..20:  20 through 30

Also available in: Atom PDF