user3707736
user3707736

Reputation: 17

Ruby: Appending hashes to an array in an each loop

I have an array of arrays called intervals. I wish to build an array of hashes out of it, adding two key/value pairs to each hash (start_ts and stop_ts).

require 'date'
date = '2014-06-12'
totalhash = Hash.new
totalarray = Array.new
payload2 = Array.new
totals = Array.new

intervals = [["Current", 0, 9999],
             ["1 to 4", -4, -1],
             ["5 to 15", -15, -5],
             ["16 to 30", -30, -16],
             ["31 to 60", -60, -31],
             ["61 to 90", -90, -61],
             ["91+", -9999, -91]]

intervals.each do |int|
    label, start, stop = int
    # Parse date and then convert to UNIX epoch (.to_time.to_i chain)
    start_ts = (Date.parse("#{date}") + start).to_time.to_i
    stop_ts = (Date.parse("#{date}") + stop).to_time.to_i

    totalhash[:label]             = label
    totalhash[:start]             = start
    totalhash[:stop]              = stop
    totalhash[:start_ts]          = start_ts
    totalhash[:stop_ts]           = stop_ts

    totalarray << totalhash
    totals = totalarray.reduce Hash.new, :merge
    puts totals
    puts 'totals size: ' + totals.size.to_s
end

The end result should be an array of seven hashes. Currently the array totalarray seems to be overwritten on each pass as opposed to being appended to.

What am I doing wrong. Thanks.

Upvotes: 0

Views: 113

Answers (4)

Mark Thomas
Mark Thomas

Reputation: 37517

When you want a 1-for-1 output from an array, use map. It reduces the need for all those intermediate variables.

# Parse date outside the loop as per @Uri's comment
day = Date.parse(date)

t = intervals.map do |interval|
      label, start, stop = interval
      {
        label:    label,
        start:    start,
        stop:     stop,
        start_ts: (day + start).to_time.to_i,
        stop_ts:  (day + stop).to_time.to_i
      }
    end

This results in your desired seven-hash array.

As for the single hash output you are getting: your reduce line is the culprit. I'm not sure what you are trying to do there.

Upvotes: 1

Cary Swoveland
Cary Swoveland

Reputation: 110675

I suggest you consider changing your data structure. I don't think it's wise to include the computed times since the epoch in each hash; rather, just compute those values as needed with a helper method:

require 'date'

date = Date.parse('2014-06-12')
  #=> #<Date: 2014-06-12 ((2456821j,0s,0n),+0s,2299161j)>

def start_stop_to_time(d, date)
  (date + d).to_time.to_i
end  

For example,

start_stop_to_time(-4, date) #=> 1402210800

total_array would then be:

total_array = [[:label, :start, :stop]].product(intervals)
                                       .map { |k,v| k.zip(v).to_h }
  #=> [{:label=> "Current", :start=>    0, :stop=>9999},
  #    {:label=>  "1 to 4", :start=>   -4, :stop=>  -1},
  #    {:label=> "5 to 15", :start=>  -15, :stop=>  -5},
  #    {:label=>"16 to 30", :start=>  -30, :stop=> -16},
  #    {:label=>"31 to 60", :start=>  -60, :stop=> -31},
  #    {:label=>"61 to 90", :start=>  -90, :stop=> -61},
  #    {:label=>     "91+", :start=>-9999, :stop=> -91}]

I do not understand the purpose of totals, so I cannot comment on that.

Upvotes: 0

DMS
DMS

Reputation: 133

I usually do this sort of thing this way:

myArray = [['cow','moo'],['dog','woof'],['duck','quack'],['fox','?']]

myArray.collect! do |animal|
    animal = {animal[0]=>animal[1]}
end

puts myArray.inspect

I am not familiar enough with reduce or injectto comment on your use here. But here is an edited version of your original code that I think does what you want:

require 'date'
date = '2014-06-12'
#totalhash = Hash.new
totalarray = Array.new
payload2 = Array.new
totals = Array.new

intervals = [["Current", 0, 9999],
         ["1 to 4", -4, -1],
         ["5 to 15", -15, -5],
         ["16 to 30", -30, -16],
         ["31 to 60", -60, -31],
         ["61 to 90", -90, -61],
         ["91+", -9999, -91]]

intervals.each do |int|
    totalhash = Hash.new   #moved your hash creation here, in the iteration
    label, start, stop = int
    # Parse date and then convert to UNIX epoch (.to_time.to_i chain)
    start_ts = (Date.parse("#{date}") + start).to_time.to_i
    stop_ts = (Date.parse("#{date}") + stop).to_time.to_i

    totalhash[:label]             = label
    totalhash[:start]             = start
    totalhash[:stop]              = stop
    totalhash[:start_ts]          = start_ts
    totalhash[:stop_ts]           = stop_ts

    totalarray << totalhash
    #totals = totalarray.reduce Hash.new, :merge
    #puts totals
    #puts 'totals size: ' + totals.size.to_s
end

puts totalarray.inspect #see the array object as is using 'inspect'

~

Upvotes: 0

mu is too short
mu is too short

Reputation: 434665

This:

totalarray << totalhash

does not copy totalhash, it merely appends a reference to the end of totalarray. It would make a lot more sense to say:

totalarray << {
  # build the Hash inline right here
}

Your code ends up with intervals.length references to exactly the same Hash in totalarray. Then your reduce merges that Hash into itself and that does nothing useful. Actually your totals = totalarray.reduce Hash.new, :merge does nothing useful even if totalarray is properly built, you could just say totals = totalarray.last and get the same result.

Upvotes: 0

Related Questions