February 2008

A Better to_hash

For about a year ago Chris learned us how to build a hash from an array:

class Array
  def to_hash
    Hash[*self.flatten]
  end
end

>> array = [["name", "chris"], ["age", 47]]
=> [["name", "chris"], ["age", 47]]
>> array.to_hash
=> {"name"=>"chris", "age"=>47}

>> blog = { :name => 'err', :style => 'classic' }
=> {:name=>"err", :style=>"classic"}
>> blog.to_a    
=> [[:name, "err"], [:style, "classic"]]
>> blog.to_a.to_hash
=> {:name=>"err", :style=>"classic"}
>> blog.to_a.to_hash == blog
=> true

Using Hash[*array.flatten] is a small, clever hack, but what he didn’t tell us is that it’s broken with hashes which contains arrays:

>> me = {:name => ["Magnus", "Holm"], :other => [1, 2]}
=> {:name=>["Magnus", "Holm"], :other=>[1, 2]}
>> me.to_a
=> [[:name, ["Magnus", "Holm"]], [:other, [1, 2]]]
>> me.to_a.to_hash
=> {1=>2, :name=>"Magnus", "Holm"=>:other}
>> me.to_a.to_hash == me
=> false  

Solution 1: Inject

Who doesn’t love inject? Chris has actually written a great article about inject too. Just skim through it and look at this gorgeous code:

class Array
  def to_hash
    self.inject({}) do |memo, element|
      memo[element[0]] = element[1]
      memo
    end
  end
end

>> me = {:name => ["Magnus", "Holm"], :other => [1, 2]}
=> {:name=>["Magnus", "Holm"], :other=>[1, 2]}
>> me.to_a
=> [[:name, ["Magnus", "Holm"]], [:other, [1, 2]]]
>> me.to_a.to_hash
=> {:name=>["Magnus", "Holm"], :other=>[1, 2]}
>> me.to_a.to_hash == me
=> true

Solution 2: Flatten

What about making flatten take an optional argument which specify how many levels that should be flattened? Like this:

class Array
  def flatten(levels = -1)
    if levels > 0
      self.inject([]) do |m,e|
        if e.is_a?(Array)
          e.flatten(levels-1).map{|x|m<<x}
        else
          m<<e
        end
        m
      end
    elsif levels == 0
      self
    else
      e=self
      e=e.flatten(1) until !e.any?{|x|x.is_a?(Array)}
      e
    end
  end

  def to_hash
    Hash[*self.flatten(1)] 
  end
end

>> me = {:name => ["Magnus", "Holm"], :other => [1, 2]}
=> {:name=>["Magnus", "Holm"], :other=>[1, 2]}
>> me.to_a.flatten
=> [:name, "Magnus", "Holm", :other, 1, 2]
>> me.to_a.flatten(1)
=> [:name, ["Magnus", "Holm"], :other, [1, 2]]
>> me.to_a.to_hash
=> {:name=>["Magnus", "Holm"], :other=>[1, 2]}
>> me.to_a.to_hash == me
=> true

It’s a much longer solution1, but you will get a very nice flatten which you (probably/hopefully) can use other places too.