Getting key value in nested Hash by key array values - ruby

Sample hash:
{
"audio" => {
"audio/aac" => ["aac"],
"audio/mpeg" => ["mp3", "mp2"],
"audio/mp4" => ["m4a", "m4b", "m4r", "3gp"],
"audio/ogg" => ["ogg", "oga"],
"audio/flac" => ["flac"],
"audio/speex" => ["spx"],
"audio/x-ms-wma" => ["wma"],
"audio/x-pn-realaudio" => ["rm", "ram"],
"audio/vnd.wave" => ["wav"],
"audio/x-musepack" => ["mpc", "mp+", "mpp"],
"audio/x-aiff" => ["aiff", "aif", "aifc"],
"audio/x-tta" => ["tta"]
},
"video" => {
"video/mp4" => ["mp4"],
"video/mpeg" => ["mpg", "mpeg"],
"video/x-m4v" => ["m4v"],
"video/quicktime" => ["mov"],
"video/x-msvideo" => ["avi"],
"video/x-flv" => ["flv"],
"video/webm" => ["webm"]
}
}
What's the best way given a file extension to get the associated content type (first match is okay)?
Searching for "flac" should return "audio/flac".
Currently I'm using this:
hsh.each_key do |group|
hsh[group].each do |k,v|
return k if v.include?(extension)
end
end

Unraveling that sort of structure is best done when it's created. But, you can loop through the various levels and get something useful from it. If I assign your initial hash to mime_hash I can unravel it using:
Hash[*mime_hash.map{ |av, types| types.map{ |mime_type, extensions| extensions.product([mime_type]) } }.flatten]
or more verbosely:
Hash[
*mime_hash.map{ |av, types|
types.map{ |mime_type, extensions|
extensions.product([mime_type])
}
}.flatten
]
Which will return:
{
"aac" => "audio/aac",
"mp3" => "audio/mpeg",
"mp2" => "audio/mpeg",
"m4a" => "audio/mp4",
"m4b" => "audio/mp4",
"m4r" => "audio/mp4",
"3gp" => "audio/mp4",
"ogg" => "audio/ogg",
"oga" => "audio/ogg",
"flac" => "audio/flac",
"spx" => "audio/speex",
"wma" => "audio/x-ms-wma",
"rm" => "audio/x-pn-realaudio",
"ram" => "audio/x-pn-realaudio",
"wav" => "audio/vnd.wave",
"mpc" => "audio/x-musepack",
"mp+" => "audio/x-musepack",
"mpp" => "audio/x-musepack",
"aiff" => "audio/x-aiff",
"aif" => "audio/x-aiff",
"aifc" => "audio/x-aiff",
"tta" => "audio/x-tta",
"mp4" => "video/mp4",
"mpg" => "video/mpeg",
"mpeg" => "video/mpeg",
"m4v" => "video/x-m4v",
"mov" => "video/quicktime",
"avi" => "video/x-msvideo",
"flv" => "video/x-flv",
"webm" => "video/webm"
}

As you've already realized the data structure you have is horrible to search in the fashion you want. Assuming you're going to be searching the same data over and over what you should do is create an index for it.
There are many ways of doing this but the simplest is probably just to flatten the hash and invert it so that your keys become values and vice-versa. That way you can simply search it by calling content_types['flac']
A section of the example hash might end up like this:
{
"aac" => "audio/aac",
"mp3" => "audio/mpeg",
"mp2" => "audio/mpeg",
"m4a" => "audio/mp4",
"m4b" => "audio/mp4",
"m4r" => "audio/mp4",
"3gp" => "audio/mp4",
"flac" => "audio/flac"
}

Try using rassoc()
Definition:
Searches through the hash comparing obj with the value using ==. Returns the first key-value pair (two-element array) that matches. See also Array#rassoc.
a = {1=> "one", 2 => "two", 3 => "three", "ii" => "two"}
a.rassoc("two") #=> [2, "two"]
a.rassoc("four") #=> nil

Related

How to deep transform values on Ruby hash

I have a hash which looks like this:
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
}
}
I need to convert each value which is a single string inside an array so that it ends up like this:
hash = {
'key1' => 'value' ,
'key2' => {
'sub1' => 'string' ,
'sub2' => 'string' ,
},
'shippingInfo' => {
'shippingType' => 'Calculated' ,
'shipToLocations' => 'Worldwide' ,
'expeditedShipping' => 'false' ,
'oneDayShippingAvailable' => 'false' ,
'handlingTime' => '3' ,
}
}
I found this but couldn't get it work
https://gist.github.com/chris/b4138603a8fe17e073c6bc073eb17785
What about something like:
def deep_transform_values(hash)
return hash unless hash.is_a?(Hash)
hash.transform_values do |val|
if val.is_a?(Array) && val.length == 1
val.first
else
deep_transform_values(val)
end
end
end
Tested with something like:
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
'an_integer' => 1,
'an_empty_array' => [],
'an_array_with_more_than_one_elements' => [1,2],
'a_symbol' => :symbol,
'a_string' => 'string'
}
}
Gives:
{
"key1"=>"value",
"key2"=>{
"sub1"=>"string",
"sub2"=>"string"
},
"shippingInfo"=> {
"shippingType"=>"Calculated",
"shipToLocations"=>"Worldwide",
"expeditedShipping"=>"false",
"oneDayShippingAvailable"=>"false",
"handlingTime"=>"3",
"an_integer"=>1,
"an_empty_array"=>[],
"an_array_with_more_than_one_elements"=>[1, 2],
"a_symbol"=>:symbol,
"a_string"=>"string"
}
}
Following your question in the comments, I guess the logic would change a bit:
class Hash
def deep_transform_values
self.transform_values do |val|
next(val.first) if val.is_a?(Array) && val.length == 1
next(val) unless val.respond_to?(:deep_transform_values)
val.deep_transform_values
end
end
end
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide', 'Web'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
}
}
def recurse(hash)
hash.transform_values do |v|
case v
when Array
v.size == 1 ? v.first : v
when Hash
recurse v
else
# raise exception
end
end
end
recurse hash
#=> {"key1"=>"value",
# "key2"=>{
# "sub1"=>"string",
# "sub2"=>"string"
# },
# "shippingInfo"=>{
# "shippingType"=>"Calculated",
# "shipToLocations"=>["Worldwide", "Web"],
# "expeditedShipping"=>"false",
# "oneDayShippingAvailable"=>"false",
# "handlingTime"=>"3"
# }
# }
As an alternative, consider using an object and allowing the initializer to deconstruct some of the keys for you.
One of the reasons a lot of people like myself started using Ruby in favour of Perl was because of the better expression of objects in place of primitives like arrays and hashes. Use it to your advantage!
class ShippingStuff # You've kept the data vague
def initialize key1:, key2:, shippingInfo:
#blk = -> val {
val.respond_to?(:push) && val.size == 1 ?
val.first :
cleankeys(val)
}
#key1 = cleankeys key1
#key2 = cleankeys key2
#shippingInfo = shippingInfo
end
attr_reader :key1, :key2, :shippingInfo
# basically a cut down version of what
# Sebastian Palma answered with
def cleankeys data
if data.respond_to? :transform_values
data.transform_values &#blk
else
#blk.call(data)
end
end
end
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
}
}
shipper = ShippingStuff.new hash.transform_keys!(&:to_sym)
shipper.key1
# "value"
shipper.key2
# {"sub1"=>"string", "sub2"=>"string"}
shipper.shippingInfo
# {"shippingType"=>["Calculated"], "shipToLocations"=>["Worldwide"], "expeditedShipping"=>["false"], "oneDayShippingAvailable"=>["false"], "handlingTime"=>["3"]}
In the same vein, I'd even make an Info class for the shippingInfo data.
You may run into a different problem if key1 and key2 are dynamic, but there's ways around that too (double splat for one).
I noticed a lot of answers with unnecessary recursion. Current version of Ruby 2.7.x with ActiveSupport (I tested with 6.1.4.4) will allow you to do this:
Input data:
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string']},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide', 'Web'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3']}}
Solution:
hash.deep_transform_values do |value|
# whatever you need to do to any nested value, like:
if value == value.to_i.to_s
value.to_i
else
value
end
end
The example above will return a typecast String to Integer.

Best/cleanest way to grab all the keys that end in url in Ruby

I have a Hash that looks like this:
{
"id" => 108,
"position" => 0,
"attachment_content_type" => "image/jpeg",
"attachment_updated_at" => "2014-11-14T21:50:46.395Z",
"attachment_width" => 1140,
"attachment_height" => 1140,
"alt" => "",
"viewable_type" => "Spree::Variant",
"viewable_id" => 43,
"mini_url" => "xyz,
"small_url" => "http:blahblahblah",
"product_url" => "http:blahblahblah",
"large_url" => "http:blahblahblah",
"xlarge_url" => "http:blahblahblah"
}
How can I use the splat * to grab all the keys that end in url? Is there a way to do that?
This Hash is in a serializer in Rails and I'm trying to nest the urls in a JSON structure that looks like this:
{
urls: {
mini_url: "blaaaah",
(etc.)
}
}
Thoughts?
Not sure what you mean by using the splat operator here but you can easily pull them with code like:
input = {
"id" => 108,
"position" => 0,
"attachment_content_type" => "image/jpeg",
"attachment_file_name" => "Strainer_-_Blue_Apron_Marketplace_213.jpg",
"type" => "Spree::Image",
"attachment_updated_at" => "2014-11-14T21:50:46.395Z",
"attachment_width" => 1140,
"attachment_height" => 1140,
"alt" => "",
"viewable_type" => "Spree::Variant",
"viewable_id" => 43,
"mini_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/mini/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"small_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/small/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"product_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/product/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"large_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/large/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"xlarge_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/xlarge/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846"
}
urls = input.each_with_object({}) do |(key,value),acc|
if key.end_with?("_url")
acc[key] = value
end
end
puts urls.inspect
How can I use the splat * to grab all the keys that end in url? Is there a way to do that?
Splats cannot do that.
You can use Hash#select:
input = {
"id" => 108,
"position" => 0,
"attachment_content_type" => "image/jpeg",
"attachment_file_name" => "Strainer_-_Blue_Apron_Marketplace_213.jpg",
"type" => "Spree::Image",
"attachment_updated_at" => "2014-11-14T21:50:46.395Z",
"attachment_width" => 1140,
"attachment_height" => 1140,
"alt" => "",
"viewable_type" => "Spree::Variant",
"viewable_id" => 43,
"mini_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/mini/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"small_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/small/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"product_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/product/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"large_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/large/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"xlarge_url" => "https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/xlarge/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846"
}
require "pp"
pp input.select { |key, value| key.end_with?("_url") }
Output:
{"mini_url"=>
"https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/mini/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"small_url"=>
"https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/small/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"product_url"=>
"https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/product/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"large_url"=>
"https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/large/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846",
"xlarge_url"=>
"https://s3.amazonaws.com/marketplace-staging.blueapron.com/app/public/spree/products/108/xlarge/Strainer_-_Blue_Apron_Marketplace_213.jpg?1416001846"}
Do you want the hash, or do you want the values. If I only wanted the values I'd start by taking advantage of keys and select:
hash = {
"id" => 108,
"viewable_id" => 43,
"mini_url" => "mini_url_value",
"small_url" => "small_url_value",
}
hash.keys.select{ |k| k[/.*_url$/] } # => ["mini_url", "small_url"]
That uses *, but there's no reason to use it. From experience and testing I know it'd slow the check and waste CPU. Simplifying the pattern would be faster:
hash.keys.select{ |k| k[/_url$/] } # => ["mini_url", "small_url"]
Even faster is:
hash.keys.select{ |k| k.end_with?('_url') } # => ["mini_url", "small_url"]
From there I'd use values_at to get the associated values:
url_keys = hash.keys.select{ |k| k.end_with?('_url') } # => ["mini_url", "small_url"]
hash.values_at(*url_keys) # => ["mini_url_value", "small_url_value"]
It'd be easy to build a new hash:
url_keys = hash.keys.select{ |k| k.end_with?('_url') } # => ["mini_url", "small_url"]
url_keys.zip(hash.values_at(*url_keys)).to_h # => {"mini_url"=>"mini_url_value", "small_url"=>"small_url_value"}
It's also possible to use select with the hash and convert the resulting array-of-arrays back to a new hash:
hash.select{ |k| k.end_with?('_url') } # => {"mini_url"=>"mini_url_value", "small_url"=>"small_url_value"}
I use the first way out of habit; It was the Perl-ish way and I wrote a lot of Perl, and it's compatible with old Rubies. Hash.select didn't return hashes for a long time and to_h wasn't available as part of Array for even longer.

How to combine two arrays of hashes?

I am querying an API to get information on users given their email address. For example:
emails = [{'email' => 'example1#test.com'}, {'email' => 'example2#test.com'}, ... ]
The query hash I pass to the API has to be in this format. The API returns an array of hashes on the information it found for each user. If there was no information it returns an empty hash in that index. The results are returned in the same order as they are queried, i.e. the first index of the response array is the info for example1#test.com. A sample response might look like this:
response = [{'gender' => 'male', 'age' => '24 - 35'}, {'gender' => 'male'}, ... ]
How can I combine my array of email hashes with the response array such that I will get something like the following?
combined = [
{'email' => 'example1#test.com', 'gender' => 'male', 'age' => '24 - 35'},
{'email' => 'example2#test.com', 'gender' => 'male'}, ... ]
The other way to achieve this, basing on #Arup Rakshit's answer:
emails = [{'email' => 'example1#test.com'}, {'email' => 'example2#test.com'} ]
response = [{'gender' => 'male', 'age' => '24 - 35'}, {'gender' => 'male'}]
emails.map.with_index { |hash, i| hash.merge(response[i]) }
How is this?
emails = [{'email' => 'example1#test.com'}, {'email' => 'example2#test.com'} ]
response = [{'gender' => 'male', 'age' => '24 - 35'}, {'gender' => 'male'}]
combined = emails.each_index.map { |i| emails[i].merge(response[i]) }
My version using Array#zip:
emails = [{'email' => 'example1#test.com'}, {'email' => 'example2#test.com'}]
response = [{'gender' => 'male', 'age' => '24 - 35'}, {'gender' => 'male'}]
combined = emails.zip(response).map { |e, r| e.merge(r) }
# => [{"email"=>"example1#test.com", "gender"=>"male", "age"=>"24 - 35"},
# {"email"=>"example2#test.com", "gender"=>"male"}]

Create multi-dimensional array or hash in ruby from JSON

I am using Rhomobile and trying to dynamically build a hash for the id and title of the buttons has of the Alert.show_popup, but am not quite getting it. What I want the end result to be, in effect, is:
Alert.show_popup( {
:message => 'Please Select Appropriate Address:',
:title => 'Get Nearby...',
:icon => :info,
:buttons => [
{'id' => '1', 'title' => 'Address 1'},
{'id' => '2', 'title' => 'Address 2'},
{'id' => '3', 'title' => 'Address 3'},
{'id' => '4', 'title' => 'Address 4'}
],
:callback => url_for(:action => :on_addressidentified_popup)
}
)
I've tried a few methods, but none have worked (build a string that looks like a hash and try_convert, etc.). Here was the latest one I tried which seemed close, but yet still far away:
nearbyaddresses = Rho::JSON.parse(#params['body'])
h = {}
nearbyaddresses.each do |location|
h[intIndex] = {}
h[intIndex][:id] = intIndex.to_s
h[intIndex][:title] = location["Address"].to_s
intIndex = intIndex + 1
end
Alert.show_popup( {
:message => 'Please Select Appropriate Address:',
:title => 'Get Nearby...',
:icon => :info,
:buttons => h,
:callback => url_for(:action => :on_addressidentified_popup)
}
)
Any ruby wizards here that can help me out?
How about
nearby_addresses_list = Rho::JSON.parse(#params['body'])
buttons_list = nearby_addresses_list.collect.each_with_index {|address, i|
{'id' => i, 'title' => address} #not sure if you need to dig into this at all.
}
This should leave buttons_list with this value
[{'id' => 0, 'title' => nearby_addresses_list[0]},
{'id' => 1, 'title' => nearby_addresses_list[1]}
{'id' => 2, 'title' => nearby_addresses_list[2]}
{'id' => 3, 'title' => nearby_addresses_list[3]}]
If you want the id's to start with 1, change the body of the collect statement to {'id' => i+1, 'title' => address}
Then just add buttons_list in as the value for the key :buttons.
Alert.show_popup( {
:message => 'Please Select Appropriate Address:',
:title => 'Get Nearby...',
:icon => :info,
:buttons => buttons_list,
:callback => url_for(:action => :on_addressidentified_popup)
})
If you're seeing weirdness between the desired output you mentioned first and the code you said was close, is it perhaps that you used symbols for the keys in your code (:id), and strings in your desired output ("id") ?
Here's how I addressed the issue. Works like a charm...
intIndex = 0
nearbyaddresses = Rho::JSON.parse(#params['body'])
##nearbyAddresses = nearbyaddresses
button_array = []
nearbyaddresses.each do |location|
opt = {'id' => intIndex.to_s, 'title' => location["Address"] }
button_array << opt
intIndex = intIndex + 1
end
Alert.show_popup( {
:message => 'Please Select Appropriate Address:',
:title => 'Get Nearby...',
:icon => :info,
:buttons => button_array,
:callback => url_for(:action => :getselectedaddress)
}
)

Ruby collection out of order

I have the following:
BB_AREAS = {
:about => {:link => "quem somos", :slug => "quem-somos"},
:expositors => {:link => "expositores",:slug => "expositores"},
:map => {:link => "planta", :slug => "planta"},
:activities => {:link => "atividades",:slug => "atividades"},
:address => {:link => "como chegar",:slug => "como-chegar"},
:support => {:link => "apoio", :slug => "apoio"},
:optin => {:link => "cadastro",:slug => "cadastro"},
:how_expositors => {:link => "como expor",:slug => "como-expor"},
:press => {:link => "imprensa",:slug => "imprensa"},
:contact => {:link => "contato",:slug => "contato"},
}
BB_MENU_AREAS = BB_AREAS.each_with_object({}) { |(k, v), h| h[k] = v[:link]}
BB_MENU_AREAS_SLUG = BB_AREAS.each_with_object({}) { |(k, v), h| h[k] = v[:slug]}
And in the view I have the following:
=render :partial => '/shared/menu', :collection => BB_MENU_AREAS.map {|link, menu| {:link => link, :menu => menu}}, :spacer_template => '/shared/menu_separator'
I want the menu to render in the same order of BB_AREAS, but it is rendered in an arbitrary order.
Hashes are ordered by insertion order in Ruby 1.9+, otherwise they have an internal order.
IMO this data belongs in an array of actual objects, though; roughly:
class Area
attr_accessor :name, :link, :slug
def initialize(name, link, slug)
#name = name
#link = link
#slug = slig
end
end
BB_AREAS = [
Area.new("About", "quem somos", "quem-somos"),
Area.new("Expositors", "expositores", "expositores"),
# etc.
]
If you actually need to extract individual components in order you may.
Ruby Hashs are orderless in Ruby 1.8 and lower. However, in Ruby 1.9 and higher, Hashes are ordered. There is a backward compatible solution though:
BB_AREAS = [
[:about , {:link => "quem somos", :slug => "quem-somos"}],
[:expositors , {:link => "expositores",:slug => "expositores"}],
[:map , {:link => "planta", :slug => "planta"}],
[:activities , {:link => "atividades",:slug => "atividades"}],
[:address , {:link => "como chegar",:slug => "como-chegar"}],
[:support , {:link => "apoio", :slug => "apoio"}],
[:optin , {:link => "cadastro",:slug => "cadastro"}],
[:how_expositors , {:link => "como expor",:slug => "como-expor"}],
[:press , {:link => "imprensa",:slug => "imprensa"}],
[:contact , {:link => "contato",:slug => "contato"}],
]
BB_MENU_AREAS = BB_AREAS.each_with_object({}) { |(k, v), h| h[k] = v[:link]}
BB_MENU_AREAS_SLUG = BB_AREAS.each_with_object({}) { |(k, v), h| h[k] = v[:slug]}
Nothing do change in your view. Additionally, in this data structure, instead of [key], you need to use .assoc(key)[1].

Resources