user3437721 user3437721 - 13 days ago 6
Ruby Question

creating multiple arrays by splitting a hash

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...

Answer
@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.