I have two arrays of hashes. Here's the structure of the arrays:
array_1 = [{:address=>"123 Main St", :locality=>"New York",
:neighbourhood=>"Main", :lot_front=>18.0, :lot_depth=>37.0,
:property_type=>"Detached", :asking_price=>156000}]
array_2 = [{:address=>"121 Orchard Heights Dr", :locality=>"Clarington",
:neighbourhood=>"Newcastle", :lot_front=>19.0, :lot_depth=>46.0,
:property_type=>"Detached", :closed_price=>270000,
:sold_date=>"2013-04-02"}]
My goal is to do the following:
For every item in array_1, find all items in array_2 that have the same values for :locality and the :neighbourhood. Then....find the average of all the values of :closed_price of the items from the search.
I'd like the code to loop through all the items in array_1 and execute the above logic.
I'm a beginner and have tried everything I know. Nothing has worked.
Thanks for your help.
Try this :)
I tried to be as simple and readable as possible on this solution.
def average(array)
array.sum.to_f / array.size.to_f
end
array_1.map do |a|
closed_prices = array_2.select do |b|
a[:locality] == b[:locality] and a[:neighbourhood] == b[:neighbourhood]
end.map do |matched|
matched[:closed_price]
end
average(closed_prices)
end
I used the following hashes to test this code:
I removed the extra keys from the hashes to simplify, removing noises.
array_1 = [
{
:locality => "New York",
:neighbourhood => "Main"
},
{
:locality => "Clarington",
:neighbourhood => "Newcastle"
},
]
array_2 = [
{
:locality => "Clarington",
:neighbourhood => "Newcastle",
:closed_price => 270000
},
{
:locality => "New York",
:neighbourhood => "Main",
:closed_price => 100000
},
{
:locality => "New York",
:neighbourhood => "Main",
:closed_price => 200000
}
]
This would return the array
[150000.0, 270000.0]
Which is the average of the :closed_price values for all the items of the array_2 that have the same :locality and :neighbourhood for each item of the array_1. This resulting array has two elements on this case because the array_1 has two elements.
You can also monkey patch the Array class to implement the avg() method right there if you want on this solution!
This will make the code much more simple.
Like this:
array_1.map do |a|
array_2.select do |b|
a[:locality] == b[:locality] and a[:neighbourhood] == b[:neighbourhood]
end.map do |matched|
matched[:closed_price]
end.avg
end
I explained more about monkey patching right here if you desire: How do I create an average from a Ruby array?
Try the following:
array_1.each do |el|
close_addresses = array_2.filter { |sub_el| el[:locality] == sub_el[:locality] && el[:neighbourhood] == sub_el[:neighbourhood] }
if close_addresses.any?
el[:expected_price] = close_addresses.instance_eval { reduce(0) { |sum, sub_el| sum + sub_el[:closed_price] } / size }
end
end
Added the :expected_price key to the array_1 elements to save the average :closed_price values.
array_1 = [{:address=>"123 Main St", :locality=>"New York",
:neighbourhood=>"Main", :lot_front=>18.0, :lot_depth=>37.0,
:property_type=>"Detached", :asking_price=>156000},
{:address=>"321 Niam St", :locality=>"New York",
:neighbourhood=>"Niam", :lot_front=>18.0, :lot_depth=>37.0,
:property_type=>"Unattached", :asking_price=>100000}]
array_2 = [{:address=>"121 Orchard Heights Dr", :locality=>"New York",
:neighbourhood=>"Main", :lot_front=>19.0, :lot_depth=>46.0,
:property_type=>"Detached", :closed_price=>270000,
:sold_date=>"2013-04-02"},
{:address=>"121 Orchard Heights Dr", :locality=>"New York",
:neighbourhood=>"Niam", :lot_front=>19.0, :lot_depth=>46.0,
:property_type=>"Detached", :closed_price=>600000,
:sold_date=>"2013-04-02"},
{:address=>"121 Orchard Heights Dr", :locality=>"New York",
:neighbourhood=>"Main", :lot_front=>19.0, :lot_depth=>46.0,
:property_type=>"Detached", :closed_price=>400000,
:sold_date=>"2013-04-02"}]
First construct a hash h2 whose keys are distinct pairs of the values of :locality and neighborhourhood in array_2 and whose values are the elements of h2 (hashes) whose values for those keys correspond to the elements of the key:
h2 = array_2.each_with_object(Hash.new { |h,k| h[k] = [] }) do |g,h|
h[[g[:locality], g[:neighbourhood]]] << g
end
#=> {["New York", "Main"]=>[
# {:address=>"121 Orchard Heights Dr", :locality=>"New York",
# :neighbourhood=>"Main", :lot_front=>19.0, :lot_depth=>46.0,
# :property_type=>"Detached", :closed_price=>270000,
# :sold_date=>"2013-04-02"},
# {:address=>"121 Orchard Heights Dr", :locality=>"New York",
# :neighbourhood=>"Main", :lot_front=>19.0, :lot_depth=>46.0,
# :property_type=>"Detached", :closed_price=>400000,
# :sold_date=>"2013-04-02"}
# ],
# ["New York", "Niam"]=>[
# {:address=>"121 Orchard Heights Dr", :locality=>"New York",
# :neighbourhood=>"Niam", :lot_front=>19.0, :lot_depth=>46.0,
# :property_type=>"Detached", :closed_price=>600000,
# :sold_date=>"2013-04-02"}
# ]
# }
See the form of Hash::new that takes a block.
Now merge each element (hash) of array_1 with a hash having the single key :closed_price whose value is the average of the values of :close_price taken over all elements in h2 for which the values of :locality and neighborhourhood are the same as those for the element of array_1 being matched:
array_1.map do |g|
a2s = h2[ [g[:locality], g[:neighbourhood]] ]
g.merge( closed_avg: a2s.sum { |g| g[:closed_price] }.fdiv(a2s.size) )
end
[{:address=>"123 Main St", :locality=>"New York",
:neighbourhood=>"Main", :lot_front=>18.0, :lot_depth=>37.0,
:property_type=>"Detached", :asking_price=>156000, :closed_avg=>335000.0},
{:address=>"321 Niam St", :locality=>"New York",
:neighbourhood=>"Niam", :lot_front=>18.0, :lot_depth=>37.0,
:property_type=>"Unattached", :asking_price=>100000, :closed_avg=>600000.0}]
See Hash#merge and Numeric#fdiv. merge does not mutate (modify) any of the elements (hashes) of array_1.
The construction of h2 is effectively the same as the following:
h2 = {}
array_2.each do |g|
arr = [g[:locality], g[:neighbourhood]]
h2[arr] = [] unless h2.key?(arr)
h2[arr] << g
end
h2
Related
I want to loop through a hash that contains a mix of nested object types. E.g.
{
"a" => "1",
"b" => ["1", "2"],
"c" => {"d" => "1", "e" => {"f" => ["1", "2", {"g" => ["x", "y", "1"]}]}}
}
and transform all string values to integers, given that they consist of numbers only. So they should match /^\d+$/.
But the important part is the iteration part.
How do I find all the values of key/value pairs as well as all values inside arrays, so I can manipulate them?
It's a bit similar to the deep_symbolize_keys method, only here I'm interested in everything else that hash keys.
My own implementation goes like this, but it's long and I may not have catched all edge cases:
def all_to_i(x)
if x.is_a?(Hash)
x.each do |k, v|
if v.is_a?(Hash)
all_to_i(v)
elsif v.is_a?(Array)
x[k] = v.map { |v2| all_to_i(v2) }
elsif v.is_a?(String)
if v.scan(/^\d+$/) != []
x[k] = v.to_i
else
x[k] = v
end
end
end
elsif x.is_a?(Array)
x.map { |x2| all_to_i(x2) }
elsif x.is_a?(String)
if x.scan(/^\d+$/) != []
x.to_i
else
x
end
end
end
# => {"a"=>1, "b"=>[1, 2], "c"=>{"d"=>1, "e"=>{"f"=>[1, 2, {"g"=>["x", "y", 1]}]}}}
i need to do an array of hashes inside of a hash, something like this:
merit_hash => {
students => [
{
"id": id,
"name": name,
subjects => [
{
"id": id,
"grade": grade
},
{
"id": id,
"grade": grade
}
]
},
{
"id": id,
"name": name,
subjects => [
{
"id": id,
"grade": grade
},
{
"id": id,
"grade": grade
}
]
}
]
}
Right now, i just have the array of student hashes, but i dont exactly know how to put the subject array inside of it, im doing this:
merit = {}
merit["students"] = []
students.each do |students|
student_subjects = Array.new
merit["students"].push(
{
"id" => students.id,
"name" => students.name.to_s
selected_batch_subjects.each do |subjects|
grade = FinalGrades.where(batch_subject_id:subjects.id, period_id: period.id, student_id: student.id).first.value
student_subjects.push(
{
"id" => subjects.id,
"grade"=> grade
}
)
end
}
)
end
but throws this error
unexpected '}', expecting keyword_end
when i try to close the student hash... what can i do to make this work? or, whats the best way of implementing this?
Thanks!
Something like this should work:
merit = {}
merit["students"] = []
students.each do |student|
student_information = {"id" => student.id, "name" => student.name.to_s}
student_subjects = []
selected_batch_subjects.each do |subjects|
grade = FinalGrades.where(batch_subject_id:subjects.id, period_id: period.id, student_id: student.id).first.value
student_subjects.push({"id" => subjects.id, "grade" => grade})
end
student_information[:subjects] = student_subjects
merit["students"].push(student_information)
end
The important part is adding each student's subjects to the already existing hash.
Your iterations are not very clear to me but for current loop and array push you could do like this:
merit = {}
merit["students"] = []
students.each do |students|
student_subjects = []
merit["students"] << {
"id" => students.id,
"name" => students.name.to_s
}
selected_batch_subjects.each do |subjects|
grade = FinalGrades.where(batch_subject_id:subjects.id, period_id: period.id, student_id: student.id).first.value
student_subjects << {"id" => subjects.id,"grade"=> grade}
end
end
The original array likes this:
[{:age=>28, :name=>"John", :id=>1}, {:name=>"David", :age=>20, :id=>2, :sex=>"male"}]
Order of existing keys:
[:id, :name, :age] or ['id', 'name', 'age']
The result should be:
[[1, "John", 28], [2, "David", 20]]
Thank for teaching me.
P/s: I am using Ruby 1.8.7 and Rails 2.3.5
Thanks
Here is a nice way using #values_at :
records = [
{:age=>28, :name=>"John", :id=>1},
{:name=>"David", :age=>20, :id=>2, :sex=>"male"}
]
attributes = [:id, :name, :age]
records.collect { |h| h.values_at(*attributes) }
# => [[1, "John", 28], [2, "David", 20]]
Map all the records and then map the attributes in the order given to return the attributes' values in the specified order.
records = [
{:age=>28, :name=>"John", :id=>1},
{:name=>"David", :age=>20, :id=>2, :sex=>"male"}
]
attributes = [:id, :name, :age]
records.map do |record|
attributes.map { |attr| record[attr] }
end
I have a hash in ruby which looks something like this:
{
"admin_milestones"=>"1",
"users_milestones"=>"0",
"admin_goals"=>"1",
"users_goals"=>"0",
"admin_tasks"=>"1",
"users_tasks"=>"0",
"admin_messages"=>"1",
"users_messages"=>"0",
"admin_meetings"=>"1",
"users_meetings"=>"0"
}
I am trying to lookout for a solutions which can cut this hash in to two parts, one with value as 1 and other hash with value as 0.
You can group hash by its value:
h1 = {
"admin_milestones"=>"1",
"users_milestones"=>"0",
"admin_goals"=>"1",
"users_goals"=>"0",
"admin_tasks"=>"1",
"users_tasks"=>"0",
"admin_messages"=>"1",
"users_messages"=>"0",
"admin_meetings"=>"1",
"users_meetings"=>"0"
}
h2 = h1.group_by{|k,v| v}
It will produce a hash grouped by its values like this:
h2 = {"1"=>[["admin_milestones", "1"], ["admin_goals", "1"], ["admin_tasks", "1"], ["admin_messages", "1"], ["admin_meetings", "1"]],
"0"=>[["users_milestones", "0"], ["users_goals", "0"], ["users_tasks", "0"], ["users_messages", "0"], ["users_meetings", "0"]]}
If you want an array as answer the cleanest solution is the partition method.
zeros, ones = my_hash.partition{|key, val| val == '0'}
You should use group_by on the keys arrays and use the value as the grouping element:
h1 = {
"admin_milestones"=>"1",
"users_milestones"=>"0",
"admin_goals"=>"1",
"users_goals"=>"0",
"admin_tasks"=>"1",
"users_tasks"=>"0",
"admin_messages"=>"1",
"users_messages"=>"0",
"admin_meetings"=>"1",
"users_meetings"=>"0"
}
# group_by on the keys, then use the value from the hash as bucket
h2 = h1.keys.group_by { |k| h1[k] }
puts h2.inspect
Returns a hash from value to array of keys:
{
"1" => [
[0] "admin_milestones",
[1] "admin_goals",
[2] "admin_tasks",
[3] "admin_messages",
[4] "admin_meetings"
],
"0" => [
[0] "users_milestones",
[1] "users_goals",
[2] "users_tasks",
[3] "users_messages",
[4] "users_meetings"
]
}
Just Hash.select:
h1.select { |key, value| value == '0' } #=> {"users_milestones"=>"0", "users_goals"=>"0", ...}
h1.select { |key, value| value == '1' } #=> {"admin_milestones"=>"1", "admin_goals"=>"1", ...}
The return value depends on your Ruby version. Ruby 1.8 returns a array of arrays, whereas Ruby 1.9 returns a hash like in the example above.
Similar with https://stackoverflow.com/a/56164608/14718545 you can use group_by but with then, in this case, you will avoid instantiating an extra variable.
{
"admin_milestones" => "1",
"users_milestones" => "0",
"admin_goals" => "1",
"users_goals" => "0",
"admin_tasks" => "1",
"users_tasks" => "0",
"admin_messages" => "1",
"users_messages" => "0",
"admin_meetings" => "1",
"users_meetings" => "0"
}.then { |h| h.keys.group_by { |k| h[k] } }
{"1"=>["admin_milestones", "admin_goals", "admin_tasks", "admin_messages", "admin_meetings"],
"0"=>["users_milestones", "users_goals", "users_tasks", "users_messages", "users_meetings"]}
I want to group a collection of objects by their has many relations... like this
s.inventoryitems.group_by{|i| i.locations}
For the sake of simplicity this returns me something like this:
{[1, 2, 3]=>["a"], [2]=>["b", "c"], []=>["d"]}
I'm looking for a result like this though:
{[1] => ["a"], [2] => ["a","b","c"], [3] => ["a"], [] => ["d"]}
I am working on restructuring things so this can all be done in a more intuitive DB & model association oriented way, but in the meantime I need implement this immediately and need to wrangle it with some Ruby and am not sure. Thanks for any help!
You need to expand this, invert it, and re-group it if you want to flip the structure like that. You could do this simply by iterating over it and regrouping manually:
h = { [ 1, 2, 3 ] => [ "a" ], [ 2 ] => [ "b", "c" ], [ ] => [ "d" ] }
s = { }
h.each do |keys, values|
keys.each do |key|
values.each do |value|
s[[ key ]] ||= [ ]
s[[ key ]] << value
end
end
if (keys.empty?)
s[[ ]] = values
end
end
puts s.inspect
# => {[1]=>["a"], [2]=>["a", "b", "c"], [3]=>["a"], []=>["d"]}