Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #9746 (closed defect: fixed)

Opened 2 years ago

Last modified 2 years ago

Range extensions: #include?(range), #overlaps?, and #step without a block

Reported by: brandon Assigned to: core
Priority: normal Milestone: 2.x
Component: ActiveSupport Version: edge
Severity: normal Keywords: range core_ext verified
Cc:

Description

The attached patch adds support to Range for checking if another range overlaps or is included in the Range.

nap_time = (Time.now..1.hour.from_now)
return false if nap_time.overlaps?(@meeting)

work_day = (Time.now.change(:hour => 9, :minute => 0)..Time.now.change(:hour => 17, :minute => 0)
return true if work_day.include?(@meeting)

It also allows #step to be called without a block, which simply returns the values as an array:

every_15_minutes = (0..60).step(15) #=> [0,15,30,45,60]

Attachments

range_ext.patch (5.0 kB) - added by brandon on 10/01/07 02:45:08.
add #overlaps?(range), #include?(range), and #step without a block

Change History

10/01/07 02:45:08 changed by brandon

  • attachment range_ext.patch added.

add #overlaps?(range), #include?(range), and #step without a block

10/01/07 03:15:05 changed by lotswholetime

+1

All tests pass and implementation looks good. These methods would be very useful when dealing with the time ranges as the examples provided demonstrate.

(follow-up: ↓ 3 ) 10/01/07 23:23:50 changed by anthony.bailey

Sorry to bring up edge cases in worthy edge patches, but I'm not sure that the current behavior is intuitive for empty ranges.

>> (empty0 = (0...0)).to_a
=> []
>> (empty1 = (1...1)).to_a
=> []
>> (empty_flip = (1..0)).to_a
=> []
>> (only0 = (0..0)).to_a
=> [0]
>> (only1 = (1..1)).to_a
=> [1]

I think I might expect everything to include an empty range. But,

>> only0.include?(empty0)
=> true
>> only0.include?(empty1)
=> false
>> only0.include?(empty_flip)
=> false
>> only1.include?(empty0)
=> false
>> only1.include?(empty1)
=> true
>> only1.include?(empty_flip)
=> true
>> empty0.include?(empty0)
=> false
>> empty1.include?(empty1)
=> false
>> empty_flip.include?(empty_flip)
=> false

(Having said that, I'm already a bit surprised by the behavior of equals.

>> empty0 == empty0
=> true
>> empty0 == empty1
=> false
>> empty0 == empty_flip
=> false

So maybe I've misunderstood the Range concept a little.)

Anyway, I think it might be worth thinking about what the right behavior in these cases is, and testing for it once decided.

I don't really know what I expect the behavior of overlaps? to be on an empty range. On the one hand I would usually expect a.includes?(b) to imply a.overlaps?(b), but it seems odd in English to say that something overlaps with an empty range.

The current behavior is down to the way in which the range is defined again:

>> only1.overlaps?(empty0)
=> false
>> only1.overlaps?(empty1)
=> true
>> empty1.overlaps?(only1)
=> true

(in reply to: ↑ 2 ; follow-up: ↓ 6 ) 10/02/07 14:05:56 changed by brandon

Anthony,

Good points. I think the problem though is more due to the fact that a range is defined by its endpoints, and not necessarily what it contains.

Replying to anthony.bailey:

I think I might expect everything to include an empty range. But, {{{

only0.include?(empty0)

=> true

0 >= 0 and 0 <= 0

only0.include?(empty1)

=> false

only0.include?(empty_flip)

=> false

only0 won't actually return true for anything, including only0.include?(0). The reason is that making the ending 0 exclusive causes it to use < and not <=. 0 < 0 will obviously return false.

only1.include?(empty0)

=> false

Expected. Both endpoints for empty0 are less than both endpoints for empty1

only1.include?(empty1)

=> true

Expected.

only1.include?(empty_flip)

=> true

Definitely wrong. I'll add a test cast for this.

empty0.include?(empty0)

=> false

empty1.include?(empty1)

=> false

empty_flip.include?(empty_flip)

=> false

Same as empty0, a range with the same begin and end, and an exclusive end, will not return true for anything.

}}} (Having said that, I'm already a bit surprised by the behavior of equals. {{{

empty0 == empty0

=> true

empty0 == empty1

=> false

empty0 == empty_flip

=> false }}}

Why is that surprising? It will only be equal if it has the same beginning and end, and same inclusive/exclusive endpoint.

I don't really know what I expect the behavior of overlaps? to be on an empty range. On the one hand I would usually expect a.includes?(b) to imply a.overlaps?(b), but it seems odd in English to say that something overlaps with an empty range.

Yeah, that's a bit confusing, but again boils down to a range simply being defined by its endpoints.

Thanks for pointing out a couple of the inconsistencies. I'll definitely add test cases for them.

10/02/07 14:48:31 changed by brandon

Anthony,

Looking closer at the Range api docs and playing around a little more, it looks like range pretty much counts on the fact that, given (x..y), x >= y. It calls x#succ until x equals y. If x is not >= y, it won't do anything.

I'm not sure why the Range class even allows you to create a range where that is not the case, but it does, and none of the operations perform as you would expect:

>> (1..5).include?(3)
=> true
>> (5..1).include?(3)
=> false
>> (5..1).to_a
=> []

Given that, I would argue that, with the exception of only0.include?(empty_flip) returning true, all of the behaviors you outlined are "expected", for better or worse.

10/02/07 15:09:44 changed by danielmorrison

  • keywords set to range core_ext.

+1

(in reply to: ↑ 3 ) 10/02/07 18:50:49 changed by anthony.bailey

Brandon,

I think the problem though is more due to the fact that a range is defined by its endpoints, and not necessarily what it contains.

Agreed. Looks like ranges are specifications of sets, not the sets themselves. And this works especially well for time ranges where you want to use the exclusive range (t...t) to mean the zero length instant of time at t, as in e.g. various calendaring systems.

only0 won't actually return true for anything, including only0.include?(0). The reason is that making the ending 0 exclusive causes it to use < and not <=. 0 < 0 will obviously return false.

Perhaps you meant empty0 in the above quote; only0 was the inclusive range (0..0).

(Am I the only one who thinks ".." for inclusive and "..." for exclusive was a rarely unnatural syntax choice on Matz part?)

Re everything else in both your comments: I understand your rationale; your proposals seem perfectly sensible to me. Thanks again for a nice core extension. I'd +1 were it within my remit to do so (I'm a real newbie round here!)

10/04/07 13:09:19 changed by cch1

+1

10/04/07 13:15:29 changed by brandon

  • keywords changed from range core_ext to range core_ext verified.

10/04/07 21:38:33 changed by piyush

+1

10/08/07 06:05:48 changed by nzkoz

  • status changed from new to closed.
  • resolution set to fixed.

(In [7800]) * Add Range#overlaps?(range), Range#include?(range), and Range#step without a block. [brandon] Closes #9746