Related
I'm stuck trying to safely navigate a hash from json.
The json could have a string, eg:
or it could be further nested:
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h2 = { location: 'Australia' }
h2.dig('location', 'formatted')
Then String does not have #dig method
Basically I'm trying to load the JSON then populate the rails model with the data available which may be optional. It seems backwards to check every nested step with an if.
Hash#dig has no magic. It reduces the arguments recursively calling Hash#[] on what was returned from the previous call.
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h1.dig :location, :code
#⇒ "AU"
It works, because h1[:location] had returned a hash.
h2 = { location: 'Australia' }
h2.dig :location, :code
It raises, because h2[:location] had returned a String.
That said, the solution would be to reimplement Hash#dig, as usually :)
Explicitly taking into account that it’s extremely trivial. Just take a list of keys to dig and (surprise) reduce, returning either the value, or nil.
%i|location code|.reduce(h2) do |acc, e|
acc.is_a?(Hash) ? acc[e] : nil
end
#⇒ nil
%i|location code|.reduce(h1) do |acc, e|
acc.is_a?(Hash) ? acc[e] : nil
end
#⇒ "AU"
Shameless plug. You might find the gem iteraptor I had created for this exact purpose useful.
You can use a simple piece of code like that:
def nested_value(hash, key)
return hash if key == ''
keys = key.split('.')
value = hash[keys.first] || hash[keys.first.to_sym]
return value unless value.is_a?(Hash)
nested_value(value, keys[1..-1].join('.'))
end
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h2 = { 'location' => 'Australia' }
p nested_value(h1, 'location.formatted') # => Australia
p nested_value(h2, 'location.formatted') # => Australia
You can also use that method for getting any nested value of a hash by providing key in format foo.bar.baz.qux. Also the method doesn't worry whether a hash has string keys or symbol keys.
I don't know if this lead to the expected behaviour (see examples below) but you can define a patch for the Hash class as follow:
module MyHashPatch
def safe_dig(params) # ok, call as you like..
tmp = self
res = nil
params.each do |param|
if (tmp.is_a? Hash) && (tmp.has_key? param)
tmp = tmp[param]
res = tmp
else
break
end
end
res
end
end
Hash.include MyHashPatch
Then test on your hashes:
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h2 = { location: 'Australia' }
h1.safe_dig([:location, :formatted]) #=> "Australia"
h2.safe_dig([:location, :formatted]) #=> "Australia"
h1.safe_dig([:location, :code]) #=> "AU"
h2.safe_dig([:location, :code]) #=> "Australia"
I could use any improvements to improve my code. I think most of the method have the same layout but I am not getting the desired output, so any help would be great. If you want to see the exercise online, its called the Bachelor Nested Iteration. I really have no clue why I am not getting my desired output, to me my working out makes sense I guess.
for the get_first_name_of_season_winner method, no matter what arguments I pass through when I call it, I always get "Beth Smalls" as an output when it shouldn't be the case. If I pass "Season 29", the output should be "Ashley Yeats"
for the get_contestant_name method, it's the same thing. It always returns "Beth Smalls" no matter what occupation I pass through. For example if I call it like this
get_contestant_name(thebachelor, "Chiropractic Assistant" )
it should return "Becca Tilley" as an output but it doesn't.
for the count_contestant_by_hometown, it should return the number of contestants which are from the hometown thats passed in the method, however, no matter which argument I pass, I get the number 4 as an output.
for the get_occupation, it should return the name of the person corresponding to the hometown being passed in the method, but I always get "Beth Smalls" no matter which hometown I pass through it.
The final method, I have no idea how to do it. It takes in two arguments––the data hash and a string of a season. Iterate through the hash and return the average age of all of the contestants for that season.
thebachelor = {
"season 30": [
{
"name": "Beth Smalls",
"age": "26",
"hometown": "Great Falls, Virginia",
"occupation": "Nanny/Freelance Journalist",
"status": "Winner"
},
{
"name": "Becca Tilley",
"age": "27",
"hometown": "Shreveport, Louisiana",
"occupation": "Chiropractic Assistant",
"status": "Eliminated Week 8"
}
],
"season 29": [
{
"name": "Ashley Yeats",
"age": "24",
"hometown": "Denver, Colorado",
"occupation": "Dental Assitant",
"status": "Winner"
},
{
"name": "Sam Grover",
"age": "29",
"hometown": "New York, New York",
"occupation": "Entertainer",
"status": "Eliminated Week 6"
}
]
}
Now the methods. get_first_name_of_season_winner is
def get_first_name_of_season_winner(data, season)
#this method returns the first name of that seasons winner
#pass the season of the show, and then it returns only th FIRST NAME of the winner for that season
#iterate through the inital hash to access the season number
#then iterate through the array, to access the hash inside
#acess the "status" to get the output
data.each do |season, contestant_Data|
contestant_Data.each do |a|
a.each do |attribute, value|
if value == "Winner"
return a[:name]
end
end
end
end
end
get_first_name_of_season_winner(thebachelor, "season 29") #returns the full name of only "Beth Smalls"
get_contestant_name is:
def get_contestant_name(data, occupation) #this method takes in the data hash and an occupation string and returns the name of the woman who has that occupation
#iterate through the initial hash to access the seasons
#iterate through the seasons to access the arrays inside
#access the occupation element of the array
#return the person who has the occupation
data.each do |season, contestant_data|
contestant_data.each do |a|
a.each do |attribute, value|
if attribute == :occupation
return a[:name]
end
end
end
end
end
get_contestant_name(thebachelor, "Chiropractic Assistant" ) #returns the full name of only "Beth Smalls"
count_contestant_by_hometown is:
def count_contestant_by_hometown(data, hometown) #this method should return the number of contestants from the hometown passed
#include a counter variable
#iterate through the hash to access the seasons
#access the array
#access the hometown key in the hash
#keep count
counter = 0
data.each do |season, contestant_data|
contestant_data.each do |a|
a.each do |attribute, value|
if attribute == :hometown
counter += 1
end
end
end
end
return counter
end
count_contestant_by_hometown(thebachelor, "Denver, Colorado") #returns the number 4, I have no idea why
get_occupation is:
def get_occupation(data, hometown) #should return the occupation of of the first contestant who hails from the hometown
data.each do |season, contestant_data|
contestant_data.each do |a|
a.each do |attribute, value|
if attribute == :hometown
return a[:name]
end
end
end
end
end
get_occupation(thebachelor, "Denver, Colorado") #returns "Beth Smalls" when it should return "Ashley Yeats"
average_age_for_season is:
def average_age_for_season(data, season) #returns the average age of all contestants for that season
I think a big problem comes from the data you're passing in. Take for example, a working solution for your final issue.
To get the data for a single season, you can use:
def average_age_for(data, season)
contestants = data[season]
contestants.sum { |contestant| contestant[:age].to_f } / contestants.count
end
average_age_for(thebatchelor, :"season 30")
#=> 26.5
Note that you need to pass :"season 30", rather than simply "season 30". That's because your data is is using symbolised strings as keys, rather than just strings.
Replace your data's keys with strings:
thebachelor = {
"season 30" => [
{
"name" => "Beth Smalls",
"age" => "26",
"hometown" => "Great Falls, Virginia",
"occupation" => "Nanny/Freelance Journalist",
"status" => "Winner"
},
{
"name" => "Becca Tilley",
"age" => "27",
"hometown" => "Shreveport, Louisiana",
"occupation" => "Chiropractic Assistant",
"status" => "Eliminated Week 8"
}
],
"season 29" => [
{
"name" => "Ashley Yeats",
"age" => "24",
"hometown" => "Denver, Colorado",
"occupation" => "Dental Assitant",
"status" => "Winner"
},
{
"name" => "Sam Grover",
"age" => "29",
"hometown" => "New York, New York",
"occupation" => "Entertainer",
"status" => "Eliminated Week 6"
}
]
}
Then look for a string in the method:
def average_age_for(data, season)
contestants = data[season]
# vvvvvvv
contestants.sum { |contestant| contestant["age"].to_f } / contestants.count
# ^^^^^^^
end
And this takes shape.
You can then do:
1)
def get_first_name_of_season_winner(data, season)
data[season].detect { |contestant| contestant["status"] == "Winner" }["name"].split.first
end
get_first_name_of_season_winner(thebachelor, "season 29")
#=> "Ashley"
2)
def get_contestant_name(data, occupation)
data.values.flatten.detect { |contestant| contestant["occupation"] == occupation }
end
get_contestant_name(thebachelor, "Chiropractic Assistant")
#=> {"name"=>"Becca Tilley", "age"=>"27", "hometown"=>"Shreveport, Louisiana", "occupation"=>"Chiropractic Assistant", "status"=>"Eliminated Week 8"}
3)
def count_contestant_by_hometown(data, town)
data.values.flatten.select { |contestant| contestant["hometown"] == town }.count
end
count_contestant_by_hometown(thebachelor, "New York, New York")
#=> 1
4)
def get_occupation(data, hometown)
data.values.flatten.detect { |contestant| contestant["hometown"] == hometown }["occupation"]
end
get_occupation(thebachelor, "New York, New York")
#=> "Entertainer"
Generalised && Optimised Solution:
Following method will work to get whatever you need from your hash thebachelor,
def get_information(data, required, season, optional, hash= {})
data = season.nil? ? data.values.flatten : data[season]
selected = data.select { |x| (hash.inject(true) { |m, (k,v)| m &&= (x[k] == v) }) }
required_data = selected.map { |x| x[required] }
if optional == :average && required == :age
(required_data.map(&:to_i).sum / required_data.count.to_f).round(2)
else
(optional == :count) ? required_data.count : required_data
end
end
Data need to be provided as below,
data - hash input from which you want to retrieve data
required - output inner hash attribute you want in output.
season - If you want to retrieve data season specific, provide season, else nil to get from all season.
Optional - It can be set nil, unless you want count or average. Otherwise pass :count or :average as optional argument.
hash - filter option. If you want to filter inner data by attributes like :hometown or :status or :occupation. Just provide it. otherwise it will set empty hash.
See examples below,
# Get name of season winner for season 'season 29'
get_information(thebachelor, :name, :'season 29', nil, status: 'Winner')
# => ["Ashley Yeats"]
# Get name of all winners irrespective of season
get_information(thebachelor, :name, nil, nil, status: 'Winner')
# => ["Beth Smalls", "Ashley Yeats"]
# Get contestant name for occupation "Chiropractic Assistant"
get_information(thebachelor, :name, nil, nil, occupation: "Chiropractic Assistant")
# => ["Becca Tilley"]
# Count contestant by home town "Denver, Colorado"
get_information(thebachelor, :name, nil, :count, hometown: "Denver, Colorado")
# => 1
# Get occupation of contestant who hails from hometown "Denver, Colorado"
get_information(thebachelor, :occupation, nil, nil, hometown: "Denver, Colorado")
# => ["Dental Assitant"]
# Get Average age for season :"season 29"
get_information(thebachelor, :age, :"season 29", :average)
# => 26.5
This method provide moreover than you asked in your question.
First, I would reformat your data so it's easy to select/detect:
data = data.map { |key, value| value.transform_keys(&:to_sym).merge(season: key) }
so now it looks like
[{
season: "season 30",
name: "Beth Smalls",
age: "26",
hometown: "Great Falls, Virginia",
occupation: "Nanny/Freelance Journalist",
status: "Winner"
},...
]
Now it's much easier to filter and detect:
def get_first_name_of_season_winner(data, season)
p = ->(v) { v[:season] == season && v[:status] == 'Winner' }
data.detect(&p)[:name][/\w+/]
end
def get_contestant_name(data, occupation)
p = ->(v) { v[:occupation] == occupation }
data.detect(&p)[:name]
end
def count_contestant_by_hometown(data, hometown)
p = ->(v) { v[:hometown] = hometown }
data.select(&p).count
end
def get_occupation(data, hometown)
p = ->(v) { v[:hometown] = hometown }
data.detect(&p)[:occupation]
end
def average_age_for_season(data, season)
p = ->(v) { v[:season] = season }
ages = data.select(&p).map { |datum| datum[:age] }
ages.sum.fdiv(ages.count) unless ages.empty?
end
In general, all these problems are of two types:
1. Given an array of data, find all the items that satisfy a certain condition
2. Given an array of data, find the first item that satisfies a certain condition
And you always solve them with select/detect and a block/proc.
In Javascript we can do this
var c = {
firstname: "Bob",
lastName: "Smith"
log: function()
{
return "Hey" + this.firstname + " " + this.lastName;
}
};
Can we do anything like this in ruby? I.E where does the usage of "This" come in, and also can we stack functions inside a variable? I'm new to ruby but the feature of "hashes" seems awfully similiar in a sense
Hash items have no access to parent hash and hence hash’s nested siblings. What you are looking for is a class.
class MyClass
def initialize
#first_name = "Bob"
#last_name = "Smith"
end
def log
"Hey #{#first_name} #{#last_name}"
end
end
MyClass.new.log
#⇒ "Hey Bob Smith"
I believe it makes sense to read a book on Ruby syntax and/or basic concepts before trying to play with.
In ruby there is the concept of Proc and Lambda, which is similar.
Example:
def create_lambda
lambda { "You're looking at me?" }
end
some_var = create_lambda
some_var.call #=> "You're looking at me?"
You can also pass parameters:
def create_square
lambda { |base| base * base }
end
square = create_square
square.call(4) # => 16
Be aware, however, that lambdas, nor procs, take optional arguments, you always have to give them exactly the ones you defined.
In your example, the hash can not be referenced before it has been created.
So, for your example, you could do:
my_hash = {
firstname: "Bob",
lastName: "Smith"
}
my_hash['log'] = lambda { "Hey #{my_hash['firstname']} #{my_hash['lastName']}" }
my_hash['log'].call #=> "Hey Bob Smith"
you can try Struct
require 'ostruct'
Person = Struct.new(:firstname, :lastName) do
def log
"Hey" + self.firstname + " " + self.lastName
end
end
person = Person.new('Bob', 'Smith')
puts person.log
I have a list of immutable value objects. The lookup class provides ways to iterate and query that data:
class Banker
Bank = Struct.new(:name, :bic, :codes)
attr_reader :banks
def initialize
#banks = [
Bank.new('Citibank', '1234567', ['1', '2']),
Bank.new('Wells Fargo', '7654321', ['4']), # etc.
]
end
def find_by_bic(bic)
banks.each do |bank|
return bank if bank.bic == bic
end
end
end
#banks is initialized every time Banker is used. What options are there to cache #banks so that it's reused across different instances of the Banker?
I don't think Struct buys you anything here. How about doing it like this?
Code
class Banker
#all_banks = {}
class << self
attr_reader :all_banks
end
attr_reader :banks
def initialize(banks)
#banks = banks.keys
banks.each { |k,v| self.class.all_banks[k] = v }
end
def find_by_bic(bic)
return nil unless #banks.include?(bic)
self.class.all_banks[bic]
end
end
Note self in self.class is needed to distinguish the class of self from the keyword class.
Example
b1 = Banker.new({ '1234567' => { name: 'Citibank', codes: ["1", "2"] },
'7654321' => { name: 'Wells Fargo', codes: ['4'] } })
b1.banks
#=> ["1234567", "7654321"]
Banker.all_banks
#=> {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
# "7654321"=>{:name=>"Wells Fargo", :codes=>["4"]}}
b1.find_by_bic '7654321'
#=> {:name=>"Wells Fargo", :codes=>["4"]}
b1.find_by_bic '1234567'
#=> {:name=>"Citibank", :codes=>["1", "2"]}
b1.find_by_bic '0000000'
#=> nil
b2 = Banker.new({ '6523155' => { name: 'Bank of America', codes: ["3"] },
'1234567' => { name: 'Citibank', codes: ["1", "2"] } })
b2.banks
#=> ["6523155", "1234567"]
Banker.all_banks
#=> {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
# "7654321"=>{:name=>"Wells Fargo", :codes=>["4"]},
# "6523155"=>{:name=>"Bank of America", :codes=>["3"]}}
b2.find_by_bic '6523155'
#=> {:name=>"Bank of America", :codes=>["3"]}
b2.find_by_bic '1234567'
#=> {:name=>"Citibank", :codes=>["1", "2"]}
b2.find_by_bic '7654321'
#=> nil
Alternatives
If you prefer you could instead add the class method:
def self.new(banks)
banks.each { |k,v| all_banks[k] = v }
super
end
and remove the first line in initialize.
Or, if you have a complete list of all banks, you could instead just make all_banks a constant:
ALL_BANKS = {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
"7654321"=>{:name=>"Wells Fargo", :codes=>["4"]},
"6523155"=>{:name=>"Bank of America", :codes=>["3"]}}
def find_by_bic(bic)
return nil unless #banks.include?(bic)
ALL_BANKS[bic]
end
and change initialize to:
def initialize(bics)
#banks = bics
end
where bics is an array of bic values.
To share immutable data between instances you can use frozen class variables: ##banks ||= [...].freeze
Typically, parsing XML or JSON returns a hash, array, or combination of them. Often, parsing through an invalid array leads to all sorts of TypeErrors, NoMethodErrors, unexpected nils, and the like.
For example, I have a response object and want to find the following element:
response['cars'][0]['engine']['5L']
If response is
{ 'foo' => { 'bar' => [1, 2, 3] } }
it will throw a NoMethodError exception, when all I want is to see is nil.
Is there a simple way to look for an element without resorting to lots of nil checks, rescues, or Rails try methods?
Casper was just before me, he used the same idea (don't know where i found it, is a time ago) but i believe my solution is more sturdy
module DeepFetch
def deep_fetch(*keys, &fetch_default)
throw_fetch_default = fetch_default && lambda {|key, coll|
args = [key, coll]
# only provide extra block args if requested
args = args.slice(0, fetch_default.arity) if fetch_default.arity >= 0
# If we need the default, we need to stop processing the loop immediately
throw :df_value, fetch_default.call(*args)
}
catch(:df_value){
keys.inject(self){|value,key|
block = throw_fetch_default && lambda{|*args|
# sneak the current collection in as an extra block arg
args << value
throw_fetch_default.call(*args)
}
value.fetch(key, &block) if value.class.method_defined? :fetch
}
}
end
# Overload [] to work with multiple keys
def [](*keys)
case keys.size
when 1 then super
else deep_fetch(*keys){|key, coll| coll[key]}
end
end
end
response = { 'foo' => { 'bar' => [1, 2, 3] } }
response.extend(DeepFetch)
p response.deep_fetch('cars') { nil } # nil
p response.deep_fetch('cars', 0) { nil } # nil
p response.deep_fetch('foo') { nil } # {"bar"=>[1, 2, 3]}
p response.deep_fetch('foo', 'bar', 0) { nil } # 1
p response.deep_fetch('foo', 'bar', 3) { nil } # nil
p response.deep_fetch('foo', 'bar', 0, 'engine') { nil } # nil
I tried to look through both the Hash documentation and also through Facets, but nothing stood out as far as I could see.
So you might want to implement your own solution. Here's one option:
class Hash
def deep_index(*args)
args.inject(self) { |e,arg|
break nil if e[arg].nil?
e[arg]
}
end
end
h1 = { 'cars' => [{'engine' => {'5L' => 'It worked'}}] }
h2 = { 'foo' => { 'bar' => [1, 2, 3] } }
p h1.deep_index('cars', 0, 'engine', '5L')
p h2.deep_index('cars', 0, 'engine', '5L')
p h2.deep_index('foo', 'bonk')
Output:
"It worked"
nil
nil
If you can live with getting an empty hash instead of nil when there is no key, then you can do it like this:
response.fetch('cars', {}).fetch(0, {}).fetch('engine', {}).fetch('5L', {})
or save some types by defining a method Hash#_:
class Hash; def _ k; fetch(k, {}) end end
response._('cars')._(0)._('engine')._('5L')
or do it at once like this:
["cars", 0, "engine", "5L"].inject(response){|h, k| h.fetch(k, {})}
For the sake of reference, there are several projects i know of that tackle the more general problem of chaining methods in the face of possible nils:
andand
ick
zucker's egonil
methodchain
probably others...
There's also been considerable discussion in the past:
Ruby nil-like object - One of many on SO
Null Objects and Falsiness - Great article by Avdi Grimm
The 28 Bytes of Ruby Joy! - Very interesting discussion following J-_-L's post
More idiomatic way to avoid errors when calling method on variable that may be nil? on ruby-talk
et cetera
Having said that, the answers already provided probably suffice for the more specific problem of chained Hash#[] access.
I would suggest an approach of injecting custom #[] method to instances we are interested in:
def weaken_checks_for_brackets_accessor inst
inst.instance_variable_set(:#original_get_element_method, inst.method(:[])) \
unless inst.instance_variable_get(:#original_get_element_method)
singleton_class = class << inst; self; end
singleton_class.send(:define_method, :[]) do |*keys|
begin
res = (inst.instance_variable_get(:#original_get_element_method).call *keys)
rescue
end
weaken_checks_for_brackets_accessor(res.nil? ? inst.class.new : res)
end
inst
end
Being called on the instance of Hash (Array is OK as all the other classes, having #[] defined), this method stores the original Hash#[] method unless it is already substituted (that’s needed to prevent stack overflow during multiple calls.) Then it injects the custom implementation of #[] method, returning empty class instance instead of nil/exception. To use the safe value retrieval:
a = { 'foo' => { 'bar' => [1, 2, 3] } }
p (weaken_checks_for_brackets_accessor a)['foo']['bar']
p "1 #{a['foo']}"
p "2 #{a['foo']['bar']}"
p "3 #{a['foo']['bar']['ghgh']}"
p "4 #{a['foo']['bar']['ghgh'][0]}"
p "5 #{a['foo']['bar']['ghgh'][0]['olala']}"
Yielding:
#⇒ [1, 2, 3]
#⇒ "1 {\"bar\"=>[1, 2, 3]}"
#⇒ "2 [1, 2, 3]"
#⇒ "3 []"
#⇒ "4 []"
#⇒ "5 []"
Since Ruby 2.3, the answer is dig