Apply an array containg keys iterative to a hash - ruby

I have the following array
["key", "key_deeper", "key_even_deeper"]
and a hash:
{ "key" => { "key_deeper" => { "key_even_deeper" => "BINGO!" } } }
What is the shortest or most expressive way to apply the array on the hash to receive "BINGO!"?
That is for the base case, but there is also a special case where the value to a key is not only String => Hash, but also String => [Integer, Hash].
For instance
["key1", "key2"]
on Hash
{"key1" => [5, {"key2" => "BINGO!" }] }
should return again "BINGO!", but an array containing only ["key1"] would simply return 5.

Probably the easiest way is to use inject:
array.inject(hash) do |h, i|
h.fetch(i){ {} }
end
# => "BINGO!"
The fetch is used to prevent a NoMethodError in case one of your lookup values is not present in the hash. However, in that case, it will return an empty hash. You may want to do the standard lookup instead, i.e.
array.inject(hash) {|h,i| h[i] }
Edit:
Here's an even shorter way to do this (I don't know if I would say it's 'more expressive', but it is shorter):
array.inject(hash, :[])

You can change the original answer a little bit for your second version of question:
array.inject(hash){ |h,i| h[i].is_a?(Array) ? h[i].last : h[i] }

Related

How to sort a hash within a hash?

I'm trying to sort a hash within a hash. I'd like to sort the hash by the sub-key. I'm using ruby.
I've tried using the sort_by method and iterating the over the hash to reorganize the sub-key. I received the error "ArgumentError: comparison of Hash with Hash failed"
hash = {2012 => {"regularseason" => [game_1, game_2, game_3],
"post_season" => [game_4, game_5]
},
2013 => {"regularseason" => [game_6, game_7, game_8],
"post_season" => [game_9, game_10]
},
2014 => {"post_season" => [game_11, game_12, game_13],
"regularseason" => [game_14, game_15]
}
}
Desired Result:
I would like to sort this hash so sub-key post_season will always appear before sub-key regularseason.
Use Hash#transform_values to sort values:
hash.transform_values { |v| v.sort.to_h }
#⇒ {2012=>{"post_season"=>[:game_4, :game_5],
# "regularseason"=>[:game_1, :game_2, :game_3]},
# 2013=>{"post_season"=>[:game_9, :game_10],
# "regularseason"=>[:game_6, :game_7, :game_8]},
# 2014=>{"post_season"=>[:game_11, :game_12, :game_13],
# "regularseason"=>[:game_14, :game_15]}}
Hashes return keys in the order they are inserted so I believe you'll essentially need to rewrite the nested hash.
For example:
hash.each { |(key, nested_hash)| hash[key] = nested_hash.sort.to_h }
This is rather inefficient, however, so you'd be better to see if you can ensure they are always entered in that order or somehow extract them in the order you desire.
Given a hash with keys that include k, we can return a new hash with the same key/value pairs, with k being the first key inserted, and the remaining keys maintaining their original relative order, as follows:
def reorder_key(h, key)
{ key=>h[key] }.merge h
end
For example:
h = { 1=>2, :a=>:b, "c"=>"d" }
reorder_key(h, :a)
#=> {:a=>:b, 1=>2, "c"=>"d"}
We can use this method to obtain the desired hash in the present problem.
hash.transform_values { |h| reorder_key(h, "post_season") }
#=> {2012=>{"post_season" =>[:game_4, :game_5],
# "regularseason"=>[:game_1, :game_2, :game_3]},
# 2013=>{"post_season" =>[:game_9, :game_10],
# "regularseason"=>[:game_6, :game_7, :game_8]},
# 2014=>{"post_season" =>[:game_11, :game_12, :game_13],
# "regularseason"=>[:game_14, :game_15]}}
This approach does not depend on "post_season" coincidentally preceding "regularseason" lexicographically. If, for example, it were decided to add the key "spring_training" and make that the first key to appear for each year in the returned hash, it would only be necessary to change the value of the second argument of reorder_key to "spring_training".

Manipulate hash in Ruby

I have a hash that looks like
{
"lt"=>"456",
"c"=>"123",
"system"=>{"pl"=>"valid-player-name", "plv"=>"player_version_1"},
"usage"=>{"trace"=>"1", "cq"=>"versionid", "stream"=>"od",
"uid"=>"9", "pst"=>[["0", "1", "10"]], "dur"=>"0", "vt"=>"2"}
}
How can I go about turning it into a hash that looks like
{
"lt"=>"456",
"c"=>"123",
"pl"=>"valid-player-name",
"plv"=>"player_version_1",
"trace"=>"1",
"cq"=>"versionid",
"stream"=>"od",
"uid"=>"9",
"pst"=>[["0", "1", "10"]], "dur"=>"0", "vt"=>"2"
}
I basically want to get rid of the keys system and usage and keep what's nested inside them
"Low-tech" version :)
h = { ... }
h.merge!(h.delete('system'))
h.merge!(h.delete('usage'))
Assuming no rails:
hash.reject { |key, _| %w(system usage).include? key }.merge(hash['system']).merge(hash['usage'])
With active support:
hash.except('system', 'usage').merge(hash['system']).merge(hash['usage'])
A more generic version.
Merge any key that contains a hash:
h = { ... }
hnew = h.inject(h.dup) { |h2, (k, v)|
h2.merge!(h2.delete(k)) if v.is_a?(Hash)
h2
}
Assuming that your data has the same structure each time, I might opt for something simple and easy to understand like this:
def manipulate_hash(h)
{
"lt" => h["lt"],
"c" => h["c"],
"pl" => h["system"]["pl"],
"plv" => h["system"]["plv"],
"trace" => h["usage"]["trace"],
"cq" => h["usage"]["cq"],
"stream" => h["usage"]["stream"],
"uid" => h["uid"],
"pst" => h["pst"],
"dur" => h["dur"],
"vt" => h["vt"]
}
end
I chose to make the hash using one big hash literal expression that spans multiple lines. If you don't like that, you could build it up on multiple lines like this:
def manipulate_hash
r = {}
r["lt"] = h["lt"]
r["c"] = h["c"]
...
r
end
You might consider using fetch instead of the [] angle brackets. That way, you'll get an exception if the expected key is missing from the hash. For example, replace h["lt"] with h.fetch("lt").
If you plan to have an arbitrarily large list of keys to merge, this is an easily scaleable method:
["system", "usage"].each_with_object(myhash) do |key|
myhash.merge!(myhash.delete(key))
end

Get the key of a hash by knowing only one item inside the value which is an array in Ruby

I want to be able to get the key called "needed key" as a string out of this considering I just the value of "known_value
hash = {
"needed key" => {
:key1 => ["know_value", "unknown_value"],
:key2 => ["stuff", "more_stuff"]
}
"otherhash" => {
:key1 => ["unknow_value", "unknown_value"],
:key2 => []
}
}
In short how would I be able to retreive "needed key" from this code if I only know "know_value". So by simply using "know_value" out of this I need to get "needed key".
Thank you very much for your help.
Hopefully I am clear enough, otherwise please ask me to be clearer.
hash.select{|k, v| v.values.flatten.include?('known_value')}.keys
This is a way to extract the desired key that is efficient in the sense that it avoids the construction of temporary, intermediate arrays, such as hash.keys, flatten, hash[key], and so on.
target = "known_value"
hash.each_key.find { |k| hash[k].each_value { |a| a.include?(target) } }
#=> "needed key"
This assumes, of course, that we are looking for any key that satisfies the requirement.
If you only care about one of them then you could use find:
hash.find { |k, h| h.values.flatten.include?('know_value') }.first
or if you want to avoid flatten, you could use any? on the inner arrays:
hash.find { |k,h| h.values.any? { |a| a.include?('know_value') } }.first
Not a Ruby expert but I think this gets the job done.
hash.keys.select{ |key|
subhash = hash[key]
subhash.keys.any? { |subkey|
subhash[subkey].include? 'known_value'
}
}.first

Converting nested hash keys from CamelCase to snake_case in Ruby

I'm trying to build an API wrapper gem, and having issues with converting hash keys to a more Rubyish format from the JSON the API returns.
The JSON contains multiple layers of nesting, both Hashes and Arrays. What I want to do is to recursively convert all keys to snake_case for easier use.
Here's what I've got so far:
def convert_hash_keys(value)
return value if (not value.is_a?(Array) and not value.is_a?(Hash))
result = value.inject({}) do |new, (key, value)|
new[to_snake_case(key.to_s).to_sym] = convert_hash_keys(value)
new
end
result
end
The above calls this method to convert strings to snake_case:
def to_snake_case(string)
string.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
Ideally, the result would be similar to the following:
hash = {:HashKey => {:NestedHashKey => [{:Key => "value"}]}}
convert_hash_keys(hash)
# => {:hash_key => {:nested_hash_key => [{:key => "value"}]}}
I'm getting the recursion wrong, and every version of this sort of solution I've tried either doesn't convert symbols beyond the first level, or goes overboard and tries to convert the entire hash, including values.
Trying to solve all this in a helper class, rather than modifying the actual Hash and String functions, if possible.
Thank you in advance.
If you use Rails:
Example with hash: camelCase to snake_case:
hash = { camelCase: 'value1', changeMe: 'value2' }
hash.transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => "value2" }
source:
http://apidock.com/rails/v4.0.2/Hash/transform_keys
For nested attributes use deep_transform_keys instead of transform_keys, example:
hash = { camelCase: 'value1', changeMe: { hereToo: { andMe: 'thanks' } } }
hash.deep_transform_keys { |key| key.to_s.underscore }
# => {"camel_case"=>"value1", "change_me"=>{"here_too"=>{"and_me"=>"thanks"}}}
source: http://apidock.com/rails/v4.2.7/Hash/deep_transform_keys
You need to treat Array and Hash separately. And, if you're in Rails, you can use underscore instead of your homebrew to_snake_case. First a little helper to reduce the noise:
def underscore_key(k)
k.to_s.underscore.to_sym
# Or, if you're not in Rails:
# to_snake_case(k.to_s).to_sym
end
If your Hashes will have keys that aren't Symbols or Strings then you can modify underscore_key appropriately.
If you have an Array, then you just want to recursively apply convert_hash_keys to each element of the Array; if you have a Hash, you want to fix the keys with underscore_key and apply convert_hash_keys to each of the values; if you have something else then you want to pass it through untouched:
def convert_hash_keys(value)
case value
when Array
value.map { |v| convert_hash_keys(v) }
# or `value.map(&method(:convert_hash_keys))`
when Hash
Hash[value.map { |k, v| [underscore_key(k), convert_hash_keys(v)] }]
else
value
end
end
I use this short form:
hash.transform_keys(&:underscore)
And, as #Shanaka Kuruwita pointed out, to deeply transform all the nested hashes:
hash.deep_transform_keys(&:underscore)
The accepted answer by 'mu is too short' has been converted into a gem, futurechimp's Plissken:
https://github.com/futurechimp/plissken/blob/master/lib/plissken/ext/hash/to_snake_keys.rb
This looks like it should work outside of Rails as the underscore functionality is included.
Use deep_transform_keys for recursive conversion.
transform_keys only convert it in high level
hash = { camelCase: 'value1', changeMe: {nestedMe: 'value2'} }
hash.transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => {nestedMe: 'value2'} }
deep_transform_keys will go deeper and transform all nested hashes as well.
hash = { camelCase: 'value1', changeMe: {nestedMe: 'value2'} }
hash.deep_transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => {nested_me: 'value2'} }
If you're using the active_support library, you can use deep_transform_keys! like so:
hash.deep_transform_keys! do |key|
k = key.to_s.snakecase rescue key
k.to_sym rescue key
end
This works both to camelCase and snake_case deep nested keys of an object, which is very useful for a JSON API:
def camelize_keys(object)
deep_transform_keys_in_object!(object) { |key| key.to_s.camelize(:lower) }
end
def snakecase_keys(object)
deep_transform_keys_in_object!(object) { |key| key.to_s.underscore.to_sym }
end
def deep_transform_keys_in_object!(object, &block)
case object
when Hash
object.keys.each do |key|
value = object.delete(key)
object[yield(key)] = deep_transform_keys_in_object!(value, &block)
end
object
when Array
object.map! { |e| deep_transform_keys_in_object!(e, &block) }
else
object
end
end

Convert array-of-hashes to a hash-of-hashes, indexed by an attribute of the hashes

I've got an array of hashes representing objects as a response to an API call. I need to pull data from some of the hashes, and one particular key serves as an id for the hash object. I would like to convert the array into a hash with the keys as the ids, and the values as the original hash with that id.
Here's what I'm talking about:
api_response = [
{ :id => 1, :foo => 'bar' },
{ :id => 2, :foo => 'another bar' },
# ..
]
ideal_response = {
1 => { :id => 1, :foo => 'bar' },
2 => { :id => 2, :foo => 'another bar' },
# ..
}
There are two ways I could think of doing this.
Map the data to the ideal_response (below)
Use api_response.find { |x| x[:id] == i } for each record I need to access.
A method I'm unaware of, possibly involving a way of using map to build a hash, natively.
My method of mapping:
keys = data.map { |x| x[:id] }
mapped = Hash[*keys.zip(data).flatten]
I can't help but feel like there is a more performant, tidier way of doing this. Option 2 is very performant when there are a very minimal number of records that need to be accessed. Mapping excels here, but it starts to break down when there are a lot of records in the response. Thankfully, I don't expect there to be more than 50-100 records, so mapping is sufficient.
Is there a smarter, tidier, or more performant way of doing this in Ruby?
Ruby <= 2.0
> Hash[api_response.map { |r| [r[:id], r] }]
#=> {1=>{:id=>1, :foo=>"bar"}, 2=>{:id=>2, :foo=>"another bar"}}
However, Hash::[] is pretty ugly and breaks the usual left-to-right OOP flow. That's why Facets proposed Enumerable#mash:
> require 'facets'
> api_response.mash { |r| [r[:id], r] }
#=> {1=>{:id=>1, :foo=>"bar"}, 2=>{:id=>2, :foo=>"another bar"}}
This basic abstraction (convert enumerables to hashes) was asked to be included in Ruby long ago, alas, without luck.
Note that your use case is covered by Active Support: Enumerable#index_by
Ruby >= 2.1
[UPDATE] Still no love for Enumerable#mash, but now we have Array#to_h. It creates an intermediate array, but it's better than nothing:
> object = api_response.map { |r| [r[:id], r] }.to_h
Something like:
ideal_response = api_response.group_by{|i| i[:id]}
#=> {1=>[{:id=>1, :foo=>"bar"}], 2=>[{:id=>2, :foo=>"another bar"}]}
It uses Enumerable's group_by, which works on collections, returning matches for whatever key value you want. Because it expects to find multiple occurrences of matching key-value hits it appends them to arrays, so you end up with a hash of arrays of hashes. You could peel back the internal arrays if you wanted but could run a risk of overwriting content if two of your hash IDs collided. group_by avoids that with the inner array.
Accessing a particular element is easy:
ideal_response[1][0] #=> {:id=>1, :foo=>"bar"}
ideal_response[1][0][:foo] #=> "bar"
The way you show at the end of the question is another valid way of doing it. Both are reasonably fast and elegant.
For this I'd probably just go:
ideal_response = api_response.each_with_object(Hash.new) { |o, h| h[o[:id]] = o }
Not super pretty with the multiple brackets in the block but it does the trick with just a single iteration of the api_response.

Resources