Hash Transforms in Ruby

I often find myself wanting to perform some kind of transform on a Ruby Hash which results in another Hash. For instance, converting all keys to strings. Ruby’s built-in Hash methods make this a little inconvenient, because all the standard transforms (#map(), #select(), #reject(), etc…) return arrays of pairs instead of hashes:

h = { :foo => 1, :bar => {:baz => 3}, :buz => 4 }
h.map{|k,v| [k.to_s, v]}        # => [["foo", 1], ["bar", {:baz=>3}], ["buz", 4]]

So I wrote a generalized hash-transform utility method. It takes a Hash and a block and returns a new Hash. The block receives each key/value pair from the original hash, along with a new Hash to populate:

def transform_hash(original, options={}, &block)
  original.inject({}){|result, (key,value)|
    value = if (options[:deep] && Hash === value) 
              transform_hash(value, options, &block)
            else 
              value
            end
    block.call(result,key,value)
    result
  }
end

Here are some examples of use:

# Convert keys to strings
def stringify_keys(hash)
  transform_hash(hash) {|hash, key, value|       
    hash[key.to_s] = value
  }
end
stringify_keys(h)               # => {"foo"=>1, "buz"=>4, "bar"=>{:baz=>3}}

# Convert keys to strings, recursively
def deep_stringify_keys(hash)
  transform_hash(hash, :deep => true) {|hash, key, value|
    hash[key.to_s] = value
  }                               
end
deep_stringify_keys(h)          # => {"foo"=>1, "buz"=>4, "bar"=>{"baz"=>3}}

# Select a subset of entries
def slice_hash(hash, *keys)
  transform_hash(hash, :deep => true) {|hash, key, value|
    hash[key] = value if keys.include?(key)
  }
end
slice_hash(h, :foo, :buz)       # => {:foo=>1, :buz=>4}

Obviously any of these methods could have been written directly in terms of regular inject(), but transform_hash() lets you forget about the housekeeping required by inject() and focus on the task at hand. Rails users will probably also note that at least some of these tasks can also be accomplished using ActiveSupport extensions to Hash. transform_hash() is intended for situations where ActiveSupport may not be available, or does not provide exactly the transform you need.

The code is available as a gist, if you find it useful let me know.

13 comments

  1. Funny, I just googled and wrote a one-liner last night to transform keys AND values to strings.

    Hash[*h.map {|k,v| [k.to_s,v.to_s] }.flatten]

    Not as robust as your examples, but a quick way since I know my input is flat.

      1. In Ruby 2.1, I’ve just discovered that you can use the new “to_h” method (array of pairs -> hash converter), as in

        newhash = h.map{|k,v| [ key_transform(k), value_transform(v)]}.to_h

        for various key and value transforms. For instance, to lookup each key up in a separate 1-1 hash, eg numeric userid -> string name:

        newhash = h.map{ |k,v| [ username[k], v ]}.to_h

        but why can’t we build the newhash direct, without the stupid array of pairs intermediary? perhaps the splat is still the best way?

  2. When I was transitioning to Ruby from Python et al, the difficulty of doing this was remarkable to me. In python, it's just:

    In [1]: x = {1:2, 3:4, 5:6, 7:8, 9:0}

    In [2]: dict((str(k), v) for k,v in x.iteritems())

    Out[2]: {'1': 2, '3': 4, '5': 6, '7': 8, '9': 0}

    So the equivalent function, adding the “deep” option, would be:

    def dict_transform(a_dict, f, deep=False):
    return dict(f(k,v) if not deep or not isinstance(v, dict) else f(k, dict_transform(v, f, deep))
    for k,v in a_dict.iteritems())

  3. Funny, I just googled and wrote a one-liner last night to transform keys AND values to strings.

    Hash[*h.map {|k,v| [k.to_s,v.to_s] }.flatten]

    Not as robust as your examples, but a quick way since I know my input is flat.

  4. When I was transitioning to Ruby from Python et al, the difficulty of doing this was remarkable to me. In python, it's just:

    In [1]: x = {1:2, 3:4, 5:6, 7:8, 9:0}

    In [2]: dict((str(k), v) for k,v in x.iteritems())

    Out[2]: {'1': 2, '3': 4, '5': 6, '7': 8, '9': 0}

    So the equivalent function, adding the “deep” option, would be:

    def dict_transform(a_dict, f, deep=False):<br>  return dict(f(k,v) if not deep or not isinstance(v, dict) else f(k, dict_transform(v, f, deep))<br>      for k,v in a_dict.iteritems())

    Grr…. getting code formatted is annoying, I'm leaving it as it is.

  5. your deep transform will blow the stack if the hash has circular references:

    irb(main):043:0> hh ={:a => 1}; hh[:b] = hh
    => {:a=>1, :b=>{…}}
    irb(main):044:0> transform_hash(hh) {|h,k,v| h[k.to_s] = v }
    => {“a”=>1, “b”=>{:a=>1, :b=>{…}}}
    irb(main):045:0> transform_hash(hh, :deep=>true) {|h,k,v| h[k.to_s] = v }
    SystemStackError: stack level too deep

Leave a Reply

Your email address will not be published. Required fields are marked *