Feature #20027
closedAdd Range Deconstruction
Description
Ranges are a powerful tool in ruby. A common range I use is a date range such as (Date.yesterday..Date.tomorrow)
. A range will often be passed around to methods because the dates hold meaning together such as a timeframe for a table filter.
Often I want to grab the original values out of a range like:
timeframe = (Date.yesterday..Date.tomorrow)
start_date = timeframe.begin
end_date = timeframe.end
#=> start_date = yesterday
#=> end_date = today
Similar to array and hash deconstruction I thought it would be useful to support range deconstruction like this:
start_date, end_date = (Date.yesterday..Date.tomorrow)
#=> start_date = yesterday
#=> end_date = today
This would also work for endless or beginless ranges since the beginning and end are just nil in those cases:
start_date, end_day = ..Date.tomorrow
#=> start_date = nil
#=> end_date = tomorrow
You could do this now using to_a
like:
start_date, *middle_dates, end_date = (Date.new(2000,1,1)..Date.new(2023,1,1).to_a
However this has unnecessary performance issues by converting the range to an array especially if the range spans a large period, middle_dates
would hold a very large array. Also if the range resulted in an array with 2 values, end_date
would be nil and this wouldn't actually work to get the begin and end values.
I think this provides a simple interface for a common pattern of deconstructing ranges into their beginning and end values. It would be useful for ranges regardless of date ranges or other types of ranges since they are essentially tuples. Would love to know what others think about this <3
Updated by stuyam (Stuart Yamartino) about 1 year ago
- Subject changed from Range Deconstruction to Add Range Deconstruction
Update title
Updated by shan (Shannon Skipper) about 1 year ago
Pattern matching would be another option though Range doesn't implement #deconstruct or #deconstruct_keys by default.
class Range def deconstruct = [self.begin, self.end] end
42..420 => low, high
Just an aside, but you could avoid the performance issues of splatting #to_a by using Range #begin and #end.
low, high = (42..420).then { [_1.begin, _1.end] }
Updated by mame (Yusuke Endoh) about 1 year ago
- Status changed from Open to Rejected
We discussed this at the dev meeting and decided to reject it.
Currently, assignment deconstruction is available only for Arrays (note that assignment deconstruction is not pattern matching). Such special handling for Ranges is not justified by the expected frequency of use.
This is my personal opinion, but it might be conceivable to introduce some method that returns Range#begin
and #end
as arrays.
p (1..100).begin_and_end #=> [1, 100]
start_date, end_date = (Date.yesterday..Date.tomorrow).begin_and_end
Updated by stuyam (Stuart Yamartino) about 1 year ago
Thanks for the thoughtful responses @shan (Shannon Skipper) and @mame (Yusuke Endoh)!
Shan, you make great points. With the next it
syntax coming in ruby 3.3 you could also do:
low, high = (42..420).then { [it.begin, it.end] }
Mame, thank you for discussing this proposal! I like your suggestion of the #begin_and_end
method, that solves the performance issues, it also then relies on solely array deconstruction doing it's own job which is nice! I could see #begin_and_end
being useful for something like an ORM where you need the endpoints to do a BETWEEN
in SQL or something and it would allow you to more easily deconstruct a range when a range is being used more as an object to store the endpoints rather than an object to ask questions about things in between the endpoints etc.
Updated by Dan0042 (Daniel DeLorme) about 1 year ago
(1..42).minmax #=> [1, 42]
(42..1).minmax #=> [nil, nil]
Updated by stuyam (Stuart Yamartino) about 1 year ago
Yeah great point @Dan0042 (Daniel DeLorme), #minmax
almost works, but as you point out it does not work because if the range is a reverse range you get nil
for both values. Interesting!
(1..42).minmax #=> [1, 42]
(42..1).minmax #=> [nil, nil]
(1..42).then { [_1.begin, _1.end] } #=> [1, 42]
(42..1).then { [_1.begin, _1.end] } #=> [42, 1]
Updated by stuyam (Stuart Yamartino) about 1 year ago
Conversation moved to new issue for implementation of #begin_and_end
method on Range: #20080