5

I am wondering how to split a range into N parts in ruby, While adding them to a hash with a zero based value for each range generated.

For example:

range = 1..60
p split(range, 4)
#=> {1..15 => 0, 16..30 => 1, 31..45 => 2, 46..60 => 3}

I've read How to return a part of an array in Ruby? for how to slice a range into an array, and a few others on how to convert the slices back into ranges, but I can't quite seem to piece all the pieces together to create the method I want.

Thanks for the help

randy newfield
  • 1,221
  • 3
  • 25
  • 38

1 Answers1

6
range = 1..60

range.each_slice(range.last/4).with_index.with_object({}) { |(a,i),h|
  h[a.first..a.last]=i }
  #=> {1..15=>0, 16..30=>1, 31..45=>2, 46..60=>3}

The steps are as follows:

enum0 = range.each_slice(range.last/4)
  #=> range.each_slice(60/4)
  #   #<Enumerator: 1..60:each_slice(15)> 

You can convert this enumerator to an array to see the (4) elements it will generate and pass to each_with_index:

enum0.to_a
  #=> [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  #    [16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
  #    [31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45],
  #    [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]] 

enum1 = enum0.with_index
  #=> #<Enumerator: #<Enumerator: 1..60:each_slice(15)>:with_index>
enum1.to_a
  #=> [[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 0],
  #    [[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30], 1],
  #    [[31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45], 2],
  #    [[46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60], 3]] 

enum2 = enum1.with_object({})
  #=> #<Enumerator: #<Enumerator: #<Enumerator: 1..60:each_slice(15)>
  #     :with_index>:with_object({})>
enum2.to_a
  #=> [[[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 0], {}],
  #    [[[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30], 1], {}],
  #    [[[31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45], 2], {}],
  #    [[[46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60], 3], {}]]

Carefully examine the return values for the calculations of enum1 and enum2. You might want to think of them as "compound" enumerators. The second and last element of each of enum2's four arrays is the empty hash that is represented by the block variable h. That hash will be constructed in subsequent calculations.

enum2.each { |(a,i),h| h[a.first..a.last]=i }
  #=> {1..15=>0, 16..30=>1, 31..45=>2, 46..60=>3} 

The first element of enum2 that is passed by each to the block (before enum.each... is executed) is

arr = enum2.next
  #=>[[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 0], {}]

The block variables are assigned to the elements of arr using parallel assignment (sometimes called multiple assignment)

(a,i),h = arr
  #=> [[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 0], {}] 
a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] 
i #=> 0 
h #=> {}

The block calculation is therefore

h[a.first..a.last]=i 
  #=> h[1..15] = 0

Now

h #=> {1..15=>0} 

The calculations are similar for each of the other 3 elements generated by enum2.

The expression

enum2.each { |(a,i),h| h[(a.first..a.last)]=i }

could alternatively be written

enum2.each { |((f,*_,l),i),h| h[(f..l)]=i }
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • or `.each_slice(15).map.with_index { |d, i| { (d.first..d.last) => i } }` may be more readable – Ilya May 29 '16 at 20:49
  • @llya, a hash is desired, not an array of hashes, each having one key-value pair. Your suggestion to pass the range is a good one, however. I'll incorporate it into my answer. – Cary Swoveland May 29 '16 at 20:56
  • Thanks for the answer! Unfortunately on my machine `ruby 2.2.3p173 (2015-08-18 revision 51636) [x64-mingw32]` your code returns `{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]=>0, [16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]=>1, [31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45]=>2, [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]=>3}` rather than `{1..15=>0, 16..30=>1, 31..45=>2, 46..60=>3} ` was there a slight over sight somewhere? If not how can I detect if a single integer falls between the keys range and return its value. Thanks again. – randy newfield May 29 '16 at 21:59
  • Continuation: Before I was using `hash.detect {|k,v| k === 4}.last` to return the value of the range where 4 falls into. This will no longer work as rather than ranges the hash is of type `array=>integer` rather than `range=>integer`. Is there another way to solve this part of the problem? Edit: `hash.detect {|k,v| k.include?(4)}.last` works for now. Thanks again. – randy newfield May 29 '16 at 22:08
  • randy, sorry for the inconvenience. I rolled back to my original answer, which I believe is correct. I had made a change that I thought would simplify, but did not test it throughly enough to see that it was wrong. I will add the explanation back in. – Cary Swoveland May 29 '16 at 22:32