Extract Hash values using Hash#dig - ruby

h = {
users: {
u_548912: {
name: "John",
age: 30
},
u_598715: {
name: "Doe",
age: 30
}
}
}
Given a hash like above, say I want to get user John, I can do
h[:users].values.first[:name] # => "John"
In Ruby 2.3 use Hash#dig can do the same thing:
h.dig(:users, :u_548912, :name) # => "John"
But given that the u_548912 is just a random number(no way to know it before hand), is there a way to get the information still using Hash#dig?

You can, of course, pass an expression as an argument to #dig:
h.dig(:users, h.dig(:users)&.keys&.first, :name)
#=> John
Extract the key if you want more legibility, at the cost of lines of code:
first_user_id = h.dig(:users)&.keys&.first
h.dig(:users, first_user_id, :name)
#=> John
Another option would be to chain your #dig method calls. This is shorter, but a bit less legible.
h.dig(:users)&.values&.dig(0, :name)
#=> John
I'm afraid there is no "neater" way of doing this while still having safe navigation.

Related

Is there any way to check if hashes in an array contains similar key value pairs in ruby?

For example, I have
array = [ {name: 'robert', nationality: 'asian', age: 10},
{name: 'robert', nationality: 'asian', age: 5},
{name: 'sira', nationality: 'african', age: 15} ]
I want to get the result as
array = [ {name: 'robert', nationality: 'asian', age: 15},
{name: 'sira', nationality: 'african', age: 15} ]
since there are 2 Robert's with the same nationality.
Any help would be much appreciated.
I have tried Array.uniq! {|e| e[:name] && e[:nationality] } but I want to add both numbers in the two hashes which is 10 + 5
P.S: Array can have n number of hashes.
I would start with something like this:
array = [
{ name: 'robert', nationality: 'asian', age: 10 },
{ name: 'robert', nationality: 'asian', age: 5 },
{ name: 'sira', nationality: 'african', age: 15 }
]
array.group_by { |e| e.values_at(:name, :nationality) }
.map { |_, vs| vs.first.merge(age: vs.sum { |v| v[:age] }) }
#=> [
# {
# :name => "robert",
# :nationality => "asian",
# :age => 15
# }, {
# :name => "sira",
# :nationality => "african",
# :age => 15
# }
# ]
Let's take a look at what you want to accomplish and go from there. You have a list of some objects, and you want to merge certain objects together if they have the same ethnicity and name. So we have a key by which we will merge. Let's put that in programming terms.
key = proc { |x| [x[:name], x[:nationality]] }
We've defined a procedure which takes a hash and returns its "key" value. If this procedure returns the same value (according to eql?) for two hashes, then those two hashes need to be merged together. Now, what do we mean by "merge"? You want to add the ages together, so let's write a merge function.
merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }
If we have two values x and y such that key[x] and key[y] are the same, we want to merge them by making a copy of x and adding y's age to it. That's exactly what this procedure does. Now that we have our building blocks, we can write the algorithm.
We want to produce an array at the end, after merging using the key procedure we've written. Fortunately, Ruby has a handy function called each_with_object which will do something very nice for us. The method each_with_object will execute its block for each element of the array, passing in a predetermined value as the other argument. This will come in handy here.
result = array.each_with_object({}) do |x, hsh|
# ...
end.values
Since we're using keys and values to do the merge, the most efficient way to do this is going to be with a hash. Hence, we pass in an empty hash as the extra object, which we'll modify to accumulate the merge results. At the end, we don't care about the keys anymore, so we write .values to get just the objects themselves. Now for the final pieces.
if hsh.include? key[x]
hsh[ key[x] ] = merge.call hsh[ key[x] ], x
else
hsh[ key[x] ] = x
end
Let's break this down. If the hash already includes key[x], which is the key for the object x that we're looking at, then we want to merge x with the value that is currently at key[x]. This is where we add the ages together. This approach only works if the merge function is what mathematicians call a semigroup, which is a fancy way of saying that the operation is associative. You don't need to worry too much about that; addition is a very good example of a semigroup, so it works here.
Anyway, if the key doesn't exist in the hash, we want to put the current value in the hash at the key position. The resulting hash from merging is returned, and then we can get the values out of it to get the result you wanted.
key = proc { |x| [x[:name], x[:nationality]] }
merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }
result = array.each_with_object({}) do |x, hsh|
if hsh.include? key[x]
hsh[ key[x] ] = merge.call hsh[ key[x] ], x
else
hsh[ key[x] ] = x
end
end.values
Now, my complexity theory is a bit rusty, but if Ruby implements its hash type efficiently (which I'm fairly certain it does), then this merge algorithm is O(n), which means it will take a linear amount of time to finish, given the problem size as input.
array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }.
map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }
[{ :name=>"robert", :nationality=>"asian", :age=>15 },
{ :name=>"sira", :nationality=>"african", :age=>15 }]
The two steps are as follows.
a = array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }
#=> { ["robert", "asian"]=>15, ["sira", "african"]=>15 }
This uses the class method Hash::new to create a hash with a default value of zero (represented by the block variable h). Once this hash heen obtained it is a simple matter to construct the desired hash:
a.map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }

Ruby string to hash values

I just started to learn Ruby!
I have the following string:
"Mark Smith, 29"
and I want to convert it to hash, so it looks like this:
{:name=>"Mark", :surname=>"Smith", :age=>29}
I've written the following code, to cut the input:
a1 = string.scan(/\w+|\d+/)
Now I have an array of strings. Is there an elegant way to convert this to hash? I know I can make three iterations like this:
pers = Hash.new
pers[:name] = a1[0]
pers[:surname] = a1[1]
pers[:age] = a1[2]
But maybe there is a way to do it using .each method or something like this? Or maybe it is possible to define a class Person, with predefined keys (:name, :surname, :age), and then just "throw" my string to an instance of this class?
Yes, you can do like,
%i(name surname age)
.zip(string.scan(/\w+|\d+/))
.to_h
# => {:name=>"Mark", :surname=>"Smith", :age=>"29"}
Or, you can take the benefit of Struct, like:
Person = Struct.new(:name, :surname, :age )
person = Person.new( *string.scan(/\w+|\d+/) )
person.age # => "29"
person.name # => "Mark"
For simplicity, precision and clarity I'd do it like so:
"Mark Smith, 29" =~ /(\w+)\s+(\w+),\s+(\d+)/
#=> 0
{ :name=> $1, :surname=> $2, :age=> $3.to_i }
#=> {:name=>"Mark", :surname=>"Smith", :age=>29}
Unlike /\w+|\d+/, this regex requires "Mark" and "Smith" to be strings and "29" to be digits.

How to test with Rspec that a key exists inside a hash that contains an array of hashes

I have a Model method that returns the following when executed.
{"data" => [
{"product" => "PRODUCTA", "orders" => 3, "ordered" => 6, "revenue" => 600.0},
{"product" => "PRODUCTB", "orders" => 1, "ordered" => 5, "revenue" => 100.0}]}
I would like to test to make sure that "revenue" in the first hash is there and then test that the value is equal to 600.
subject { described_class.order_items_by_revenue }
it "includes revenue key" do
expect(subject).to include(:revenue)
end
I am completely lost on how to test this with Rspec.
RSpec allows you to use the have_key predicate matcher to validate the presence of a key via has_key?, like so:
subject { described_class.order_items_by_revenue }
it "includes revenue key" do
expect(subject.first).to have_key(:revenue)
end
This issue can be solved via testing of each hash by key existing with appropriate type of value. For example:
describe 'GetCatsService' do
subject { [{ name: 'Felix', age: 25 }, { name: 'Garfield', age: 40 }] }
it { is_expected.to include(include(name: a_kind_of(String), age: a_kind_of(Integer)))}
end
# GetCatsService
# should include (include {:name => (a kind of String), :age => (a kind of Integer)})
If you explicitly want to test only the first hash in your array mapped to :data, here are your expects given what you wrote above:
data = subject[:data].first
expect(data).not_to be_nil
expect(data.has_key?(:revenue)).to be_truthy
expect(data[:revenue]).to eq 600
Alternatively, for the second expectation, you could use expect(data).to have_key(:revenue) as Chris Heald pointed out in his answer which has a much nicer failure message as seen in the comments.
The first "expectation" test if the subject has the first hash. (You could alternately test if the array is empty?)
The next expectation is testing if the first hash has the key :revenue
The last expectation tests if the first hash :revenue value is equal to 600
You should read up on RSpec, it's a very powerful and usefull testing framework.
this works for me in 'rspec-rails', '~> 3.6'
array_of_hashes = [
{
id: "1356786826",
contact_name: 'John Doe'
}
]
expect(array_of_hashes).to include(have_key(:id))

How to get the right csv format from hash in ruby

Hash to csv
hash :
{
"employee" => [
{
"name" => "Claude",
"lastname"=> "David",
"profile" => [
"age" => "43",
"jobs" => [
{
"name" => "Ingeneer",
"year" => "5"
}
],
"graduate" => [
{
"place" => "Oxford",
"year" => "1990"
},
],
"kids" => [
{
"name" => "Viktor",
"age" => "18",
}
]
}
}]
this is an example of an hash I would work on. So, as you can see, there is many level of array in it.
My question is, how do I put it properly in a CSV file?
I tried this :
column_names = hash['employee'].first.keys
s=CSV.generate do |csv|
csv << column_names
hash['scrap'].each do |x|
csv << x.values
end
end
File.write('myCSV.csv', s)
but I only get name, lastname and profile as keys, when I would catch all of them (age, jobs, name , year, graduate, place...).
Beside, how can I associate one value per case?
Because I actually have all employee[x] which take a cell alone. Is there any parameters I have missed?
Ps: This could be the following of this post
A valid CSV output has a fixed number of columns, your hash has a variable number of values. The keys jobs, graduate and kids could all have multiple values.
If your only goal is to make a CSV output that can be read in Excel for example, you could enumerate your Hash, take the maximum number of key/value pairs per key, total it and then write your CSV output, filling the blank values with "".
There are plenty of examples here on Stack Overflow, search for "deep hash" to start with.
Your result would have a different number of columns with each Hash you provide it.
That's too much work if you ask me.
If you just want to present a readable result, your best and easiest option is to convert the Hash to YAML which is created for readability:
require 'yaml'
hash = {.....}
puts hash.to_yaml
employee:
- name: Claude
lastname: David
profile:
- age: '43'
jobs:
- name: Ingeneer
year: '5'
graduate:
- place: Oxford
year: '1990'
kids:
- name: Viktor
age: '18'
If you want to convert the hash to a CSV file or record, you'll need to get a 'flat' representation of your keys and values. Something like the following:
h = {
a: 1,
b: {
c: 3,
d: 4,
e: {
f: 5
},
g: 6
}
}
def flat_keys(h)
h.keys.reject{|k| h[k].is_a?(Hash)} + h.values.select{|v| v.is_a?(Hash)}.flat_map{|v| flat_keys(v)}
end
flat_keys(h)
# [:a, :c, :d, :g, :f]
def flat_values(h)
h.values.flat_map{|v| v.is_a?(Hash) ? flat_values(v) : v}
end
flat_values(h)
# [1, 3, 4, 5, 6]
Then you can apply that to create a CSV output.
It depends on how those fields are represented in the database.
For example, your jobs has a hash with name key and your kids also has a hash with name key, so you can't just 'flatten' them, because keys have to be unique.
jobs is probably another model (database table), so you probably would have to (depending on the database) write it separately, including things like the id of the related object and so on.
Are you sure you're not in over your head? Judging from your last question and because you seem to treat csv's as simple key-values pair omitting all the database representation and relations.

How do I get a hash from an array based on a value in the hash?

How do I grab a hash from an array based on a value in the hash? In this case I want to select the hash that has the lowest score, being potato. I use Ruby 1.9.
[
{ name: "tomato", score: 9 },
{ name: "potato", score: 3 },
{ name: "carrot", score: 6 }
]
You can use Enumerable's min_by method:
ary.min_by {|h| h[:score] }
#=> { name: "potato", score: "3" }
I think your intention is to compare by the number rather than as strings.
array.min_by{|h| h[:score].to_i}
Edit Since the OP changed the question, the answer becomes
array.min_by{|h| h[:score]}
which now makes no difference from Zach Kemp's answer.
Ruby's Enumerable#min_by is definitely the way to go; however, just for kicks, here is a solution based on Enumerable#reduce:
array.reduce({}) do |memo, x|
min_score = memo[:score]
(!min_score || (min_score > x[:score])) ? x : memo
end

Resources