Feature #20080
Updated by stuyam (Stuart Yamartino) 11 months ago
Followup Reference: #20027
This feature request is to implement a method called `#begin_and_end` on `Range` that returns an array of the first and last value stored in a range:
```ruby
(1..300).begin_and_end #=> [1, 300]
first, last = (300..1).begin_and_end
first #=> 300
last #=> 1
```
I believe this would be a great addition to Ranges as they are often used to pass around a single object used to hold endpoints, and this allows easier retrieval of those endpoints.
This would allow easier deconstruction into start and end values using array deconstruction as well as a simpler way to serialize to a more primitive object such as an array for database storage.
This implementation was suggested by @mame in my initial feature suggestion regarding range deconstruction: https://bugs.ruby-lang.org/issues/20027
This implementation would work similar to how `#minmax` works where it returns an array of two numbers, however the difference is that `#minmax` doesn't work with reverse ranges as @Dan0042 pointed out in the link above:
```ruby
(1..42).minmax #=> [1, 42]
(42..1).minmax #=> [nil, nil]
```
**Updated Proposal:** (based on many wonderful suggestions!)
1. Call the method `#bounds` rather than `#begin_and_end`.
```ruby
first, last = (1..300).bounds # => [1, 300]
first, last = (300..1).bounds # => [300, 1]
first, last = (..300).bounds # => [nil, 300]
first, last = (1..).bounds # => [1, nil]
```
2. Add `exclude_end?` support so re-hydration of Range works:
```ruby
b = (1..2).bounds #=> [1,2]
Range.new(*b) #=> 1..2
b = (1...2).bounds #=> [1,2,true]
Range.new(*b) #=> 1...2
```
3. Options for controlling when and if `exclude_end?` is included:
```ruby
# automatic by default, only shows when exclude_end? is true
# useful for re-hydration and doing the right thing in most cases
(1..2).bounds #=> [1,2]
(1...2).bounds #=> [1,2,true]
# show exclude_end? always
# useful for mixed range usage in logic that needs to know the end, or storing in a table where you want to be explicit
(1..2).bounds(true) #=> [1,2,false]
(1...2).bounds(true) #=> [1,2,true]
# never show exclude_end?
# useful for sending to a front end or something where you just need the bounds
(1..2).bounds(false) #=> [1,2]
(1...2).bounds(false) #=> [1,2]
```
Option 3 here feels sort of unnecessarily actually, I think if you want something explicit in those formats it would be easy to implement, though I could still see it being useful to require a format. I like that option 2. makes it do the dynamic thing which supports loading and unloading range and allows you to represent the range in an array etc.