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.

This entry was posted in Uncategorized and tagged , . Bookmark the permalink.
  • http://jonathanjulian.com jjulian

    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.

    • http://avdi.org avdi

      Part of the impetus for writing mine was I tend to get the Hash[*...] bit wrong… e.g. I'll leave out the splat.

      • http://www.doc.ic.ac.uk/~dcw/ Duncan White

        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?

  • llimllib

    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())

    • http://avdi.org avdi

      Yeah, the whole “returning an Array of Arrays” business is a weird wart and IMO violates the POLS.

  • http://avdi.org avdi

    Yeah, the whole “returning an Array of Arrays” business is a weird wart and IMO violates the POLS.

  • http://jonathanjulian.com/ jjulian

    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.

  • http://wideteams.com Avdi Grimm

    Part of the impetus for writing mine was I tend to get the Hash[*...] bit wrong… e.g. I'll leave out the splat.

  • llimllib

    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())

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

  • http://wideteams.com Avdi Grimm

    Yeah, the whole “returning an Array of Arrays” business is a weird wart and IMO violates the POLS.

  • Devin Ben-Hur

    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