How can ONE Ruby-symbol map to MULTIPLE values? - ruby

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'?

Related

compare different objects of array with same value in ruby

I have a usecase, where I have two different array of object which consist of same attributes with different/same values.
Ex:
x = [#<User _id: 32864efe, question: "comments", answer: "testing">]
y = [#<ActionController::Parameters {"question"=>"comments", "answer"=>"testing"} permitted: true>}>]
Now, When I am trying to get the difference between these two objects I am expecting difference to be nil when object consists of same value, however it is returning the below response.
x - y => [#<User _id: 32864efe, question: "comments", answer: "testing">]
In some cases, I may have more than one User object in x object. In that case, it should return the difference.
Can you please suggest how we can handle this. Any help would be appreciated.
Is this what you're looking for?
def compare_arrays(array1, array2)
result = []
# Iterate through each object in array1
array1.each do |obj1|
# Check if the object exists in array2
matching_obj = array2.find { |obj2| obj1 == obj2 }
# If no matching object was found, add the object to the result array
result << obj1 unless matching_obj
end
result
end
This function iterates through each object in array1 and checks if there is a matching object in array2. If no matching object is found, the object is added to the result array.
You can use this function like this:
array1 = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
array2 = [{ id: 1, name: 'John' }]
result = compare_arrays(array1, array2)
# result is [{ id: 2, name: 'Jane' }]

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

Passing sort_by an array of sort fields

If I have a array of hashes
collection = [
{ first_name: 'john', last_name: 'smith', middle: 'c'},
{ first_name: 'john', last_name: 'foo', middle: 'a'}
]
And an array of keys I want to sort by:
sort_keys = ['first_name', 'last_name']
How can I pass these keys to sort_by given that the keys will always match the keys in the collection?
I've tried
collection.sort_by { |v| sort_keys.map(&:v) }
but this doesn't work. I believe I'll need to use a proc but I'm not sure how to implement it. Would appreciate any help!
Using Ruby 2.2.1
If you change your sort_keys to contain symbols:
sort_keys = [:first_name, :last_name]
You can use values_at to retrieve the values:
collection.sort_by { |h| h.values_at(*sort_keys) }
#=> [{:first_name=>"john", :last_name=>"foo", :middle=>"a"}, {:first_name=>"john", :last_name=>"smith", :middle=>"c"}]
The array that is used to sort the hashes looks like this:
collection.map { |h| h.values_at(*sort_keys) }
#=> [["john", "smith"], ["john", "foo"]]

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.

Most succinct method that takes a single-level hash argument and returns a copy with nil values

Help me write the most succinct method that takes one argument (a single-level hash) and returns a copy with the values set to nil.
example input hash
{
email: 'hans#moleman.com',
first_name: 'Hans',
last_name: 'Moleman'
}
returned value
{
email: nil,
first_name: nil,
last_name: nil
}
What about this?
new_hash = Hash[original_hash.keys.zip([])]
Take the keys of the hash, zip with an empty array to get pairs of keys with nil, and use Hash[] to convert it back to a hash.
Or, as #mu_is_too_short pointed out in the comments, another way to do this that might be less tricky to read is:
new_hash = Hash[original_hash.keys.map { |k| [k, nil] }]
This is a good alternative, credit to #mu.
h= {
email: 'hans#moleman.com',
first_name: 'Hans',
last_name: 'Moleman'
}
Hash[*h.flat_map{|k,_| [k,nil]}]
#=> {:email=>nil, :first_name=>nil, :last_name=>nil}
or simply
Hash[h.map { |k,_| [k,nil] }]
#=> {:email=>nil, :first_name=>nil, :last_name=>nil}
The answer to the question before it was supplemented with its comments
h = {
email: 'hans#moleman.com',
first_name: 'Hans',
last_name: 'Moleman'
}
h.keys.each{|k| h[k] = nil}
But don't forget that there is a more straightforward way to clear a hash:
h.clear
h # => {}
The answer to the question supplemented with its comments
original = {
email: 'hans#moleman.com',
first_name: 'Hans',
last_name: 'Moleman'
}
h = {}
original.keys.each{|k| h[k] = nil}
h # The hash you want
The best solution I could come up with:
def clear_values(single_level_hash)
single_level_hash
.keys
.reduce({}) do |hash, key|
hash[key] = nil
hash
end
end
There must be a better way!

Resources