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.
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.
Part of the impetus for writing mine was I tend to get the
Hash[*...]
bit wrong… e.g. I'll leave out the splat.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?
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())
Yeah, the whole “returning an Array of Arrays” business is a weird wart and IMO violates the POLS.
Yeah, the whole “returning an Array of Arrays” business is a weird wart and IMO violates the POLS.
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.
Part of the impetus for writing mine was I tend to get the
Hash[*...]
bit wrong… e.g. I'll leave out the splat.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.
Yeah, the whole “returning an Array of Arrays” business is a weird wart and IMO violates the POLS.
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
Patches welcome 😉
Here ya go: https://gist.github.com/1070399