Reputation: 2289
Lets say I have an @assortment
of numbers in a hash, e.g. 1 to 100.
Each number in the @assortment
can have a status of :free
, or :used
.
An example @assortment
could be:
{ 1 => :free, 2 => :free, 3=> :used etc ... }
Lets say I want to split the @assortment
up based on the used numbers, and extract the free numbers into their own hash (or an array or hashes?)
For example, for an @assortment
of 1 to 100, if numbers 25 and 75 were 'used' and the rest were 'free', then the result would be 3 new hashes of all the free values:
1 to 24
26 to 74
76 to 100
Similarly, lets say we have a different @assortmen
t, with numbers 1 to 100, but I want to extract numbers 20 to 80, but numbers 30, 31, 32 and 40 are used then the result is like this :
hash1 -> 20 to 29
hash2 ->33 to 39
hash3 -> 41 to 80
Is there a nice functional way to do this in Ruby, where I can pass in a complete @assortment
of numbers, and an optional range to extract and get the resulting hashes, perhaps in an array?
I guess the original hash gets broken up or split based on the :used
elements...
If you were to loop through the hash, then every free number would be added to a new hash (e.g. hash1) until you reach a used number. Keep going through the loop until you reach a free number, this and all subsequent free numbers get added to a new hash (hash2). Keep this going until you have all the free numbers in new hashes...
Upvotes: 0
Views: 121
Reputation: 110685
@assortment = (20..50).to_a.product([:free]).to_h
[30,31,32,40].each { |n| @assortment[n] = :used }
@assortment
# => {20=>:free, 21=>:free, 22=>:free, 23=>:free, 24=>:free, 25=>:free,
# 26=>:free, 27=>:free, 28=>:free, 29=>:free, 30=>:used, 31=>:used,
# 32=>:used, 33=>:free, 34=>:free, 35=>:free, 36=>:free, 37=>:free,
# 38=>:free, 39=>:free, 40=>:used, 41=>:free, 42=>:free, 43=>:free,
# 44=>:free, 45=>:free, 46=>:free, 47=>:free, 48=>:free, 49=>:free, 50=>:free}
Return an array of hashes
@assortment.reject { |_,v| v == :used }.
slice_when { |(a,_),(b,_)| b > a+1 }.
to_a.
map(&:to_h)
#=> [{20=>:free, 21=>:free,...29=>:free},
# {33=>:free, 34=>:free,...39=>:free},
# {41=>:free, 42=>:free,...50=>:free}]
See Hash#reject (which returns a hash) and Enumerable#slice_when.
Return an array of arrays
Having a hash whose values are all the same doesn't seem very useful. If you'd prefer returning an array of array, just drop to_h
.
arr = @assortment.reject { |_,v| v == :used }.
keys.
slice_when { |a,b| b > a+1 }.
to_a
#=> [[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
# [33, 34, 35, 36, 37, 38, 39],
# [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]]
Return an array of ranges
A third option is to return an array of ranges. To do that map each of arr
's elements (arrays) to a range:
arr.map { |f,*_,l| f..l }
#=> [20..29, 33..39, 41..50]
The first element of arr
passed to the block is [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
. The three block variables are computed using parallel assignement:
f,*b,l = [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
f #=> 20
_ #=> [21, 22, 23, 24, 25, 26, 27, 28]
l #=> 29
I wish to underscore that I've used an underscore for the second block variable to underscore that it is not used in the block calculation.
Upvotes: 2