Compare multiple values in a Ruby array - ruby

[
{
"name": "John Doe",
"location": {
"name": "New York, New York",
"id": 12746342329
},
"hometown": {
"name": "Brooklyn, New York",
"id": 43453644
}
},
{
"name": "Jane Doe",
"location": {
"name": "Miami, Florida",
"id": 12746342329
},
"hometown": {
"name": "Queens, New York",
"id": 12746329
}
}
]
Given this piece of JSON, how would I be able to loop through and pull out all of the "hometown" and "location" keys and see which people had the value of New York?
My issue is I can Array.each through these items, but I don't know how to traverse both location && hometown with my criteria ("New York").

people.select {|person|
person.any? {|k, v|
%w[location hometown].include?(k) && /New York/ =~ v['name']
}}
This basically says the following: select all entries in the array for which the following condition is true. The condition is: is it true for any of the key-value pairs that the key is either 'hometown' or 'location' and the name property of the value belonging to that key matches the Regexp /New York/?
However, your object model seems to be in a serious need of refactoring. In fact, the main problem is that your object model isn't even an object model, it's a hash and array model.
Here's what I mean by a proper object model:
class Person
attr_reader :name, :location, :hometown
def initialize(name, location=nil, hometown=nil)
#name, #location, #hometown = name, location, hometown
end
def cities
return #location, #hometown
end
end
class City
attr_reader :id, :name
def initialize(id, name)
#id, #name = id, name
end
def =~(other)
name =~ other
end
end
nyc = City.new(12746342329, 'New York, New York')
brooklyn = City.new(43453644, 'Brooklyn, New York')
miami = City.new(12746342329, 'Miami, Florida')
queens = City.new(12746329, 'Queens, New York')
john = Person.new('John Doe', nyc, brooklyn)
jane = Person.new('Jane Doe', miami, queens)
people = [john, jane]
If you have such a proper object model, your code becomes much cleaner, because instead of teasing apart the nuts of bults of a nested maze of hashes and arrays, you have nice little objects that you can simply ask some questions:
people.select {|person| person.cities.any? {|city| city =~ /New York/ }}
You can almost read this like English: from the array select all people for which any of their cities matches the Regexp /New York/.
If we improve the object model further, it gets even better:
class Person
def lived_in?(pattern)
cities.any? {|city| city =~ pattern }
end
end
people.select {|person| person.lived_in?(/New York/) }
This basically says "From the people, select the ones which at one time lived in New York". That's much better than "from the people select all for which the first element of the key value pair is either the string 'hometown' or the string 'location' and the second element of the key value pair matches the Regexp /New York/".

I think Jörg's solution has a minor bug - 'location' and 'hometown' are not used, so, for example, following "person" would pass the test:
{
'name' => 'Foo Bar',
'favourite movie' => {
name => 'New York, New York!'
}
}
Here's a shot at correcting it, along with comments:
ary.select {|person| # get me every person satisfying following condition
%w[location hometown].any? {|key| # check if for any of the strings 'location' and 'hometown'
# person should have that key, and its 'name' should contain /New York/ regexp
person[key] && person[key]['name'] && /New York/ =~ person[key]['name']
}}

Related

How can ONE Ruby-symbol map to MULTIPLE values?

From what I have read so far I know, that a symbol is pointing always to the same value. Unlike a string, which is mutable in Ruby. Means a string might not have the same value all the time.
But if I create a list which hashes like e.g. this one:
persons = [
{first_name: "John", last_name: "Doe"},
{first_name: "Lisa", last_name: "Meyer"}
]
If I now do:
persons[1][:first_name]
Then I get the correct value "Lisa".
But how does that work?
If a symbol can point to only one exact value:
How can it differentiate between the "first_name"-symbol of the first hash and the "first_name"-symbol of the second, third ... hash?
persons = [
{ first_name: "John", last_name: "Doe" },
{ first_name: "Lisa", last_name: "Meyer"}
]
persons is an array of hashes. Arrays can be accessed by their indexes.
So, the index 0 in persons is { first_name: "John", last_name: "Doe" }.
The index 1 in persons is { first_name: "Lisa", last_name: "Meyer"} and so on:
p persons[0] # {:first_name=>"John", :last_name=>"Doe"}
p persons[1] # {:first_name=>"Lisa", :last_name=>"Meyer"}
Don't get confused in mutability here. It's just you're referring to the wrong data type.
If you want, you can check every hash key from persons and see that they have the same object_id (first_name and last_name). That's because symbols are immutables, allowing Ruby to create only one instance of them as long as your code is running:
persons.map { |e| e.map { |key, _| [key, key.object_id] }.to_h }
# [{:first_name=>1016988, :last_name=>1017308}, {:first_name=>1016988, :last_name=>1017308}]
As per the comment question; no, it doesn't create a new object depending on the scope, e.g.:
p :first_name.object_id
# 1016668
def persons
[{ first_name: "John", last_name: "Doe" },
{ first_name: "Lisa", last_name: "Meyer"}]
end
p persons.map { |e| e.map { |key, _| [key, key.object_id] }.to_h }
# [{:first_name=>1016668, :last_name=>1017308}, {:first_name=>1016668, :last_name=>1017308}]
:first_name.object_id is defined outside the scope of the persons method, but while inside the method, it keeps pointing to the same object.
In your example you select the object at index 1 of your array.
{first_name: "Lisa", last_name: "Meyer"}
How could the value of the first_name key be different than 'Lisa'?

Ruby function to get specific item in array and dynamically get nested data

If I have an array of hashes that looks like this
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some street',
postcode: '98877',
#...
}
}, {
#...
}]
can you write a function to get a specific item in an array, iterate over it and dynamically retrieve subsequently nested data?
This example doesn't work, but hopefully better explains my question:
def getDataFromFirstItem(someVal)
array(0).each{ |k, v| v["#{ someVal }"] }
end
puts getDataFromFirstItem('name')
# Expected output: 'Stan'
For context, I'm trying to create a Middleman helper so that I don't have to loop through a specific array that only has one item each time I use it in my template. The item (a hash) contains a load of global site variables. The data is coming from Contentful, within which everything is an array of entries.
Starting in ruby 2.3 and greater, you can use Array#dig and Hash#dig which both
Extracts the nested value specified by the sequence of idx objects by calling dig at each step, returning nil if any intermediate step is nil.
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some Street',
postcode: '98877'
}
}, {
}]
array.dig(0, :name) # => "Stan"
array.dig(0, :address, :postcode) # => "98877"
array.dig(0, :address, :city) # => nil
array.dig(1, :address, :postcode) # => nil
array.dig(2, :address, :postcode) # => nil
Please try this
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some street',
postcode: '98877',
#...
}
},
{
name: 'Nimish',
surname: 'Gupta',
address: {
street: 'Some street',
postcode: '98877',
#...
}
}
]
def getDataFromFirstItem(array, someVal)
array[0][someVal]
end
#Run this command
getDataFromFirstItem(array, :name) # => 'Stan'
#Please notice I send name as a symbol instead of string because the hash you declared consists of symbol keys
#Also if you want to make a dynamic program that works on all indexes of an array and not on a specific index then you can try this
def getDataFromItem(array, index, someVal)
if array[index]
array[index][someVal]
end
end
getDataFromItem(array, 0, :name) # => Stan
getDataFromItem(array, 1, :name) # => Nimish
getDataFromItem(array, 2, :name) # => nil
Hope this works, Please let me know if you still faces any issues

Iterating over an array to create a nested hash

I am trying to create a nested hash from an array that has several elements saved to it. I've tried experimenting with each_with_object, each_with_index, each and map.
class Person
attr_reader :name, :city, :state, :zip, :hobby
def initialize(name, hobby, city, state, zip)
#name = name
#hobby = hobby
#city = city
#state = state
#zip = zip
end
end
steve = Person.new("Steve", "basketball","Dallas", "Texas", 75444)
chris = Person.new("Chris", "piano","Phoenix", "Arizona", 75218)
larry = Person.new("Larry", "hunting","Austin", "Texas", 78735)
adam = Person.new("Adam", "swimming","Waco", "Texas", 76715)
people = [steve, chris, larry, adam]
people_array = people.map do |person|
person = person.name, person.hobby, person.city, person.state, person.zip
end
Now I just need to turn it into a hash. One issue I am having is, when I'm experimenting with other methods, I can turn it into a hash, but the array is still inside the hash. The expected output is just a nested hash with no arrays inside of it.
# Expected output ... create the following hash from the peeps array:
#
# people_hash = {
# "Steve" => {
# "hobby" => "golf",
# "address" => {
# "city" => "Dallas",
# "state" => "Texas",
# "zip" => 75444
# }
# # etc, etc
Any hints on making sure the hash is a nested hash with no arrays?
This works:
person_hash = Hash[peeps_array.map do |user|
[user[0], Hash['hobby', user[1], 'address', Hash['city', user[2], 'state', user[3], 'zip', user[4]]]]
end]
Basically just use the ruby Hash [] method to convert each of the sub-arrays into an hash
Why not just pass people?
people.each_with_object({}) do |instance, h|
h[instance.name] = { "hobby" => instance.hobby,
"address" => { "city" => instance.city,
"state" => instance.state,
"zip" => instance.zip } }
end

categorize by hash value

I have an array of hashes with values like:
by_person = [{ :person => "Jane Smith", :filenames => ["Report.pdf", "File2.pdf"]}, {:person => "John Doe", :filenames => ["Report.pdf] }]
I would like to end up with another array of hashes (by_file) that has each unique value from the filenames key as a key in the by_file array:
by_file = [{ :filename => "Report.pdf", :people => ["Jane Smith", "John Doe"] }, { :filename => "File2.pdf", :people => [Jane Smith] }]
I have tried:
by_file = []
by_person.each do |person|
person[:filenames].each do |file|
unless by_file.include?(file)
# list people that are included in file
by_person_each_file = by_person.select{|person| person[:filenames].include?(file)}
by_person_each_file.each do |person|
by_file << {
:file => file,
:people => person[:person]
}
end
end
end
end
as well as:
by_file.map(&:to_a).reduce({}) {|h,(k,v)| (h[k] ||= []) << v; h}
Any feedback is appreciated, thanks!
Doesn't seem too tricky, but the way you're compiling it isn't very efficient:
by_person = [{ :person => "Jane Smith", :filenames => ["Report.pdf", "File2.pdf"]}, {:person => "John Doe", :filenames => ["Report.pdf"] }]
by_file = by_person.each_with_object({ }) do |entry, index|
entry[:filenames].each do |filename|
set = index[filename] ||= [ ]
set << entry[:person]
end
end.collect do |filename, people|
{
filename: filename,
people: people
}
end
puts by_file.inspect
# => [{:filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"]}, {:filename=>"File2.pdf", :people=>["Jane Smith"]}]
This makes use of a hash to group the people by filename, essentially inverting your structure, and then converts that into the final format in a second pass. This is more efficient than working with the final format during compilation as that's not indexed and requires an expensive linear search to find the correct container to insert into.
An alternate method is to create a default hash constructor that makes the structure you're looking for:
by_file_hash = Hash.new do |h,k|
h[k] = {
filename: k,
people: [ ]
}
end
by_person.each do |entry|
entry[:filenames].each do |filename|
by_file_hash[filename][:people] << entry[:person]
end
end
by_file = by_file_hash.values
puts by_file.inspect
# => [{:filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"]}, {:filename=>"File2.pdf", :people=>["Jane Smith"]}]
This may or may not be easier to understand.
This is one way to do it.
Code
def convert(by_person)
by_person.each_with_object({}) do |hf,hp|
hf[:filenames].each do |fname|
hp.update({ fname=>[hf[:person]] }) { |_,oh,nh| oh+nh }
end
end.map { |fname,people| { :filename => fname, :people=>people } }
end
Example
by_person = [{:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]},
{:person=>"John Doe", :filenames=>["Report.pdf"]}]
convert(by_person)
#=> [{:filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"]},
# {:filename=>"File2.pdf", :people=>["Jane Smith"]}]
Explanation
For by_person in the example:
enum1 = by_person.each_with_object({})
#=>[{:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]},
{:person=>"John Doe", :filenames=>["Report.pdf"]}]:each_with_object({})>
Let's see what values the enumerator enum will pass into the block:
enum1.to_a
#=> [[{:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]}, {}],
# [{:person=>"John Doe", :filenames=>["Report.pdf"]}, {}]]
As will be shown below, the empty hash in the first element of the enumerator will no longer be empty with the second element is passed into the block.
The first element is assigned to the block variables as follows (I've indented to indicate the block level):
hf = {:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]}
hp = {}
enum2 = hf[:filenames].each
#=> #<Enumerator: ["Report.pdf", "File2.pdf"]:each>
enum2.to_a
#=> ["Report.pdf", "File2.pdf"]
"Report.pdf" is passed to the inner block, assigned to the block variable:
fname = "Report.pdf"
and
hp.update({ "Report.pdf"=>["Jane Smith"] }) { |_,oh,nh| oh+nh }
#=> {"Report.pdf"=>["Jane Smith"]}
is executed, returning the updated value of hp.
Here the block for Hash#update (aka Hash#merge!) is not consulted. It is only needed when the hash hp and the merging hash (here { fname=>["Jane Smith"] }) have one or more common keys. For each common key, the key and the corresponding values from the two hashes are passed to the block. This is elaborated below.
Next, enum2 passes "File2.pdf" into the block and assigns it to the block variable:
fname = "File2.pdf"
and executes
hp.update({ "File2.pdf"=>["Jane Smith"] }) { |_,oh,nh| oh+nh }
#=> {"Report.pdf"=>["Jane Smith"], "File2.pdf"=>["Jane Smith"]}
which returns the updated value of hp. Again, update's block was not consulted. We're now finished with Jane, so enum1 next passes its second and last value into the block and assigns the block variables as follows:
hf = {:person=>"John Doe", :filenames=>["Report.pdf"]}
hp = {"Report.pdf"=>["Jane Smith"], "File2.pdf"=>["Jane Smith"]}
Note that hp has now been updated. We then have:
enum2 = hf[:filenames].each
#=> #<Enumerator: ["Report.pdf"]:each>
enum2.to_a
#=> ["Report.pdf"]
enum2 assigns
fname = "Report.pdf"
and executes:
hp.update({ "Report.pdf"=>["John Doe"] }) { |_,oh,nv| oh+nv }
#=> {"Report.pdf"=>["Jane Smith", "John Doe"], "File2.pdf"=>["Jane Smith"]}
In making this update, hp and the hash being merged both have the key "Report.pdf". The following values are therefore passed to the block variables |k,ov,nv|:
k = "Report.pdf"
oh = ["Jane Smith"]
nh = ["John Doe"]
We don't need the key, so I've replaced it with an underscore. The block returns
["Jane Smith"]+["John Doe"] #=> ["Jane Smith", "John Doe"]
which becomes the new value for the key "Report.pdf".
Before turning to the final step, I'd like to suggest that you consider stopping here. That is, rather than constructing an array of hashes, one for each file, just leave it as a hash with the files as keys and arrays of persons the values:
{ "Report.pdf"=>["Jane Smith", "John Doe"], "File2.pdf"=>["Jane Smith"] }
The final step is straightforward:
hp.map { |fname,people| { :filename => fname, :people=>people } }
#=> [{ :filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"] },
# { :filename=>"File2.pdf", :people=>["Jane Smith"] }]

Sorting multiple values by ascending and descending

I'm trying to sort an array of objects based upon different attributes. Some of those attributes I would like to sort in ascending order and some in descending order. I have been able to sort by ascending or descending but have been unable to combine the two.
Here is the simple class I am working with:
class Dog
attr_reader :name, :gender
DOGS = []
def initialize(name, gender)
#name = name
#gender = gender
DOGS << self
end
def self.all
DOGS
end
def self.sort_all_by_gender_then_name
self.all.sort_by { |d| [d.gender, d.name] }
end
end
I can then instantiate some dogs to be sorted later.
#rover = Dog.new("Rover", "Male")
#max = Dog.new("Max", "Male")
#fluffy = Dog.new("Fluffy", "Female")
#cocoa = Dog.new("Cocoa", "Female")
I can then use the sort_all_by_gender_then_name method.
Dog.sort_all_by_gender_then_name
=> [#cocoa, #fluffy, #max, #rover]
The array it returns includes females first, then males, all sorted by name in ascending order.
But what if I want to have gender be descending, and then name ascending, so that it would be males first and then sorted by name ascending. In this case:
=> [#max, #rover, #cocoa, #fluffy]
Or, if I wanted it by gender ascending, but name descending:
=> [#fluffy, #cocoa, #rover, #max]
When sorting numerical values, you can prepend a - to make it sort in reverse. However, I have been unable to find a way to do this with strings. Any help or ideas would be appreciated. Thanks.
Here's one way to do it using .sort instead of .sort_by:
dogs = [
{ name: "Rover", gender: "Male" },
{ name: "Max", gender: "Male" },
{ name: "Fluffy", gender: "Female" },
{ name: "Cocoa", gender: "Female" }
]
# gender asc, name asc
p(dogs.sort do |a, b|
[a[:gender], a[:name]] <=> [b[:gender], b[:name]]
end)
# gender desc, name asc
p(dogs.sort do |a, b|
[b[:gender], a[:name]] <=> [a[:gender], b[:name]]
end)
# gender asc, name desc
p(dogs.sort do |a, b|
[a[:gender], b[:name]] <=> [b[:gender], a[:name]]
end)
Output:
[{:name=>"Cocoa", :gender=>"Female"}, {:name=>"Fluffy", :gender=>"Female"}, {:name=>"Max", :gender=>"Male"}, {:name=>"Rover", :gender=>"Male"}]
[{:name=>"Max", :gender=>"Male"}, {:name=>"Rover", :gender=>"Male"}, {:name=>"Cocoa", :gender=>"Female"}, {:name=>"Fluffy", :gender=>"Female"}]
[{:name=>"Fluffy", :gender=>"Female"}, {:name=>"Cocoa", :gender=>"Female"}, {:name=>"Rover", :gender=>"Male"}, {:name=>"Max", :gender=>"Male"}]
Basically, this is doing something similar to negating numbers (as you mentioned in the question), by swapping the property to the other element if it needs to be sorted in descending order.
This ReversedOrder mixin can help you accomplish the mixed direction sorts on seperate attributes, using sort_by:
module ReversedOrder
def <=>(other)
- super
end
end
Use example:
dogs = [
{ name: "Rover", gender: "Male" },
{ name: "Max", gender: "Male" },
{ name: "Fluffy", gender: "Female" },
{ name: "Cocoa", gender: "Female" }
]
dogs.sort_by {|e| [e[:gender], e[:name]] }
=> [{:name=>"Cocoa", :gender=>"Female"},
{:name=>"Fluffy", :gender=>"Female"},
{:name=>"Max", :gender=>"Male"},
{:name=>"Rover", :gender=>"Male"}]
dogs.sort_by {|e| [e[:gender].dup.extend(ReversedOrder), e[:name]] }
=> [{:name=>"Max", :gender=>"Male"},
{:name=>"Rover", :gender=>"Male"},
{:name=>"Cocoa", :gender=>"Female"},
{:name=>"Fluffy", :gender=>"Female"}]
Note: Be careful to dup the reversed element. Without that, you will mixin the comparison inverter to the actual object instead of just the key being made for sort_by and it will forever produce reversed comparisons.

Resources