How to sort a hierarchical array of hashes - ruby-on-rails

I'm working with an array like the below:
arr = [{
item: "Subject",
id: "16",
parent_id: ""
},
{
item: "Math",
id: "17",
parent_id: "16"
},
{
item: "Geology",
id: "988",
parent_id: "208"
},
{
item: "Biology",
id: "844",
parent_id: "208"
},
{
item: "Botany",
id: "594",
parent_id: "844"
},
{
item: "Science",
id: "208",
parent_id: "16"
}
]
I'm wanting to sort them so and print them out so that they display like this, grouping them and showing their parentage within the hierarchy as indentations:
Subject
Math
Science
Geology
Biology
Botany
I'm fairly stumped on how to accomplish this. I am ultimately wanting to iterate through the array only once, but I get stuck when I realize that a parent item may come after its child. Any help is greatly appreciated.
Edit: eliminated the duplicate item

To sort an array by an attribute of its elements...
arr = arr.sort_by{ |e| e[:parent_id] }
Then a little recursion to walk the tree...
def display_hierarchy(arr, parent_id="", level = 0)
current_indent = 2 * level
arr.select{ |e| e[:parent_id] == parent_id }.each do |elem|
name = elem[:item]
puts name.rjust(name.length + current_indent)
display_hierarchy(arr, elem[:id], level + 1)
end
end
Putting it all together...
arr = arr.sort_by{ |e| e[:parent_id] }
display_hierarchy(arr)
Note, if you have duplicate parents, you will get duplicate branches (hint: your example array has a duplicate "Science" node).
Also note, sorting the array ahead of time doesn't really matter for the tree, since it is a hierarchy, I just added it to show how to do it. You'd probably just sort each set of children like so...
def display_hierarchy(arr, parent_id="", level = 0)
current_indent = 2 * level
arr.select{ |e| e[:parent_id] == parent_id }.sort_by{ |e| e[:item]}.each do |elem|
name = elem[:item]
puts name.rjust(name.length + current_indent)
display_hierarchy(arr, elem[:id], level + 1)
end
end
And you get this...
Subject
Math
Science
Biology
Botany
Geology
Science
Biology
Botany
Geology

I am ultimately wanting to iterate through the array only once
In a way, you can do that: by transforming the array into a different structure that's more fit for the task. You'd still have to traverse the resulting collection again though.
Say, you could partition the array into groups by their :parent_id:
children = arr.group_by { |subject| subject[:parent_id] }
Then it's your typical recursive walk, where indentation is added on every level:
hierarchical_print = proc do |node, indent = ""|
puts(indent + node[:item]) # base
new_indent = indent + " "
children[node[:id]]&.each do |child| # recursion
hierarchical_print.(child, new_indent)
end
end
Notice the use of &. (the "lonely operator") in there that only calls the method if the callee isn't nil. Because getting a value from a hash with a key that isn't there returns nil.
In your case the root is a node with an empty parent id, so it's readily available from the children hash as well:
hierarchical_print.(children[""].first)
Subject
Math
Science
Geology
Biology
Botany
Science
Geology
Biology
Botany
...well, yes, you do have two Sciences in there.

Related

Find various ways of allocating fruit to people

# Example 1
People = ["Terry", "Merry"]
Fruit = ["Apple","Grape","Peach"]
# Possible solutions:
[
{"Terry"=>"Apple","Merry"=>"Grape"},
{"Terry"=>"Apple","Merry"=>"Peach"},
{"Terry"=>"Grape","Merry"=>"Apple"},
{"Terry"=>"Grape","Merry"=>"Peach"},
{"Terry"=>"Peach","Merry"=>"Apple"},
{"Terry"=>"Peach","Merry"=>"Grape"},
]
# Example 2
People = ["Terry", "Merry", "Perry"]
Fruit = ["Apple","Grape"]
# Possible solutions:
[
{"Terry"=>"Apple","Merry"=>"Grape","Perry"=>nil},
{"Terry"=>"Apple","Merry"=>nil,"Perry"=>"Grape"},
{"Terry"=>"Grape","Merry"=>"Apple","Perry"=>nil},
{"Terry"=>"Grape","Merry"=>nil,"Perry"=>"Apple"},
{"Terry"=>nil,"Merry"=>"Apple","Perry"=>"Grape"},
{"Terry"=>nil,"Merry"=>"Grape","Perry"=>"Apple"},
]
Stuck trying to solve this recursively (necessary for this exercise, though let me know if you don't think recursion is possible).
I feel like basically I start by assigning a random person a fruit, and then add that to all possible solutions that arise from the smaller subset of assigning remaining people remaining fruit.
E.g., for Example 1, I assign Terry an Apple, and then aggregate that with the remaining possible options of what Merry can get (either Grape or Peach).
Then just repeat changing up the fruit assigned to the first random person (e.g., with Terry getting Grape then Peach, in Example 1).
I feel like this sounds so straightforward but I'm struggling.
It can be done recursively as follows.
def hmmm(people, fruit)
adj_fruit = fruit + [nil]*([people.size-fruit.size, 0].max)
recurse(adj_fruit).map { |a| people.zip(a).to_h }
end
def recurse(fruit_left, fruit_selected = [])
return [fruit_selected + fruit_left] if fruit_left.size == 1
fruit_left.each_with_object([]) do |f,a|
recurse(fruit_left - [f], fruit_selected + [f]).each { |e| a << e }
end
end
hmmm(["Terry", "Merry"], ["Apple", "Grape", "Peach"])
#=> [{"Terry"=>"Apple", "Merry"=>"Grape"}, {"Terry"=>"Apple", "Merry"=>"Peach"},
# {"Terry"=>"Grape", "Merry"=>"Apple"}, {"Terry"=>"Grape", "Merry"=>"Peach"},
# {"Terry"=>"Peach", "Merry"=>"Apple"}, {"Terry"=>"Peach", "Merry"=>"Grape"}]
Here adj_fruit #=> ["Apple", "Grape", "Peach"]
hmmm(["Terry", "Merry", "Perry"], ["Apple", "Grape"])
#=> [{"Terry"=>"Apple", "Merry"=>"Grape", "Perry"=>nil},
# {"Terry"=>"Apple", "Merry"=>nil, "Perry"=>"Grape"},
# {"Terry"=>"Grape", "Merry"=>"Apple", "Perry"=>nil},
# {"Terry"=>"Grape", "Merry"=>nil, "Perry"=>"Apple"},
# {"Terry"=>nil, "Merry"=>"Apple", "Perry"=>"Grape"},
# {"Terry"=>nil, "Merry"=>"Grape", "Perry"=>"Apple"}]
Here adj_fruit #=> ["Apple", "Grape", nil].
We can see map's receiver in hmmm by removing .map { |a| people.zip(a).to_h } from its last line.
def hmmm(people, fruit)
adj_fruit = fruit + [nil]*([people.size-fruit.size, 0].max)
recurse(adj_fruit)
end
hmmm(["Terry", "Merry"], ["Apple","Grape","Peach"])
#=> [["Apple", "Grape", "Peach"], ["Apple", "Peach", "Grape"],
# ["Grape", "Apple", "Peach"], ["Grape", "Peach", "Apple"],
# ["Peach", "Apple", "Grape"], ["Peach", "Grape", "Apple"]]
A more conventional solution, such as the one following, would not employ recursion.
def hmmm(people, fruit)
(fruit + [nil]*[people.size - fruit.size, 0].max).
permutation(people.size).
map { |a| people.zip(a).to_h }
end
This produces the same return values as those shown above for the recursive solution.
See Array#permutation and Enumerable#zip.
If len(people) <= len(fruit), then you can use
for pieces in itertools.permutations(fruit, len(people)):
assign the pieces of fruit to the people in order
If len(people) > len(fruit), then use
for eaters in itertools.permutations(people, len(fruit))
assign the eaters to the fruit in order, and the others get nothing
I don't know how to combine the two separate cases into a single case
I now see that this was supposed to be solve recursively. Misread the original.
Let's look the possibilities for
assignment(people, fruit):
If len(people) == 0, then you're done, with the empty solution. (Not to be confused with no solution.)
If len(fruit) == 0, then no one gets any fruit. Again, this is an actual solution.
If len(people) <= len(fruit), then the first person gets some piece of fruit, appended onto all possible results of the remainder of the people getting the remainder of the fruit.
If len(people) > len(fruit), then either the first person does or doesn't get a piece of fruit, and recursively the rest of the people get whatever's left.
It's left as an exercise to you how to code this.
For anyone's future reference, this was my answer using recursion.
NOTE that "nil" overcounts; since "nil" is treated as a unique entry, the code reads {"Terry"=>"apple","Merry"=>"nil","Perry"=>"nil"} and {"Terry"=>"apple","Perry"=>"nil","Merry"=>"nil"} as 2 distinct solutions. I did not investigate further because this isn't super realistic for the exercise that this is a part of.
I also didn't investigate further for same reason, but using string "nil" versus nil yielded different results
def pure5(people, fruit, solution = [])
people_count = people.size
fruit_count = fruit.size
diff = people_count - fruit_count
diff.times { fruit << "nil" } if diff > 0
people.each do |p|
fruit.each do |f|
if people.size == 1
obj = {}
obj[p] = f
solution << obj
else
partial_solution = pure5(people - [p], fruit - [f])
partial_solution.each do |s|
s[p] = f
end
solution = solution + partial_solution
end
end
return solution
end
end

Fastest way to remove a key from array and make it last

I have to remove the 'Other' Category from the array, which is originally sorted alphabetically, and just make it the last index. I created this little helper but believe there could be a faster way of accomplishing this.
The array is something like this [#<Category id: 17, title: "Books">, #<Category id: 18, title: "Children's Clothing">,
Here is what I've done. It works. Although, I was wonder if theres a more efficient way.
<%
#options = []
#other_option = []
#free_item_options.each do |category|
if category.title.downcase == "other"
#other_option << category
else
#options << category
end
end
#options << #other_option[0]
%>
In cases like this, I usually reach for multi-parameter sorting.
#free_item_options.sort_by do |option|
[
option.title.casecmp?('other') ? 1 : 0,
option.title,
]
end
"Other" category will have 1 and will sort last. Everything else will have 0 and will sort between themselves by ascending title.
Another approach is to just use SQL.
#free_item_options = Category.select("categories.*, (LOWER(title) = 'other') as is_other").order('is_other', :title).to_a
There is Enumerable#partition which is designed to split a collection up in two partitions.
#other_option, #options = #free_item_options.partition { |category| category.title.casecmp?('other') }
#options.concat(#other_options)
If you are certain there is a maximum of one "other" category (which seems to be the case based upon #options << #other_option[0]). You could also use find_index in combination with delete_at and <<. find_index stops iterating upon the first match.
index = #free_item_options.find_index { |category| category.title.casecmp?('other') }
#free_item_options << #free_item_options.delete_at(index) if index
Keep in mind the above does mutate #free_item_options.

rails array of hashes calculate one column

I have an array and it has many columns and I want to change one value of my one column.
My array is:
[
{
id: 1,
Districts: "Lakhisarai",
Area: 15.87,
Production: 67.77,
Productivity: 4271,
Year: 2015,
Area_Colour: "Red",
Production_Colour: "Orange",
Productivity_Colour: "Dark_Green",
created_at: "2018-07-24T11:24:13.000Z",
updated_at: "2018-07-24T11:24:13.000Z"
},
{
id: 29,
Districts: "Begusarai",
Area: 18.53,
Production: 29.35,
Productivity: 1584,
Year: 2015,
Area_Colour: "Red",
Production_Colour: "Red",
Productivity_Colour: "Orange",
created_at: "2018-07-24T11:24:13.000Z",
updated_at: "2018-07-24T11:24:13.000Z"
},
...
]
This is my sample array and I want my Productivity to be divided by 100 for that I am using one empty array and pushing these hashes to my array like:
j = []
b.map do |k|
if k.Productivity
u = k.Productivity/100
j.push({id: k.id, Productivity: u })
else
j.push({id: k.id, Productivity: k.Productivity })
end
Is there any simple way where I can generate this kind of array and reflect my changes to to one column. Is there any way where I don't need to push name of column one by one in push method.
I want to generate exact same array with one modification in productivity
let's say your array is e, then:
e.each { |item| item[:Productivity] = item[:Productivity]/100}
Example:
e = [{p: 12, d: 13}, {p:14, d:70}]
e.each { |item| item[:p] = item[:p]/10}
output: [{:p=>1, :d=>13}, {:p=>1, :d=>70}]
You could take help of map method here to create a new array from your original array, but with the mentioned changes.
ary.map do |elem|
h = elem.slice(:id)
h[:productivity] = elem[:Productivity] / 100 if elem[:Productivity]
h
end
=> [{:id=>1, :productivity=>42}, {:id=>29, :productivity=>15}]
Note, Hash#slice returns a new hash with only the key-value pairs for the keys passed in argument e.g. here, it returns { id: 1 } for first element.
Also, we are assigning the calculated productivity to the output only when it is set on original hash. Hence, the if condition there.

combine two arrays of possibly different sizes into hash

How can I combine these two arrays in to a hash. They may be of equal sizes or not.
#status_array = ["ready", "required", "processing", "approval", "live"]
#part_milestones = [#<Milestone id: 657707, data_type: "ready">, #<Milestone id: 657708, data_type: "required">, #<Milestone id: 657709, data_type: "approval">]
They are sorted already. I just need the hash to deal with the "blanks" properly like so:
{"ready"=>#<Milestone id: 657707, data_type: "ready">, "required"=>#<Milestone id: 657708, data_type: "required">, "processing"=>nil, "approval"=>#<Milestone id: 657709, data_type: "approval">, "live"=>nil}
The cleanest way I know to do this is:
hash = #status_array.inject({}) do |result_hash, status|
result_hash[status] = #part_milestones.select { |milestone| milestone.data_type == status }.first
result_hash
end
You can use zip to merge the arrays into two dimensional and then use the following to convert into hash
Hash[#status_array.zip(#part_milestones)]
Documentation for Hash[]
UPDATE:
just realized that its not a one to one mapping
hash = {}
#status_array.each do |status|
hash[status] = #part_milestone.find{|milestone| milestone.data_type == status}
end
#part_milestones.sort_by &:data_type should do the trick

ruby on rails looping through a list of key value elements

I have a list of key value pairs like this.
PERSON_SUMMARY = {
first_names: %w(Mike Tim Jim kevin Alan Sara John Sammy t'Renée),
last_names: %w(Robinson Jackson Fox Terry Ali Brits Tyson Willis-St.\ Paul),
offenses: [
{ offense_name:'Speeding',
penalties: [
{ penalty_name: 'Prison', severity: 'Medium' },
{ penalty_name: 'Ticket', severity: 'Low' }
]
},
{ offense_name:'Shoplifting',
penalties: [
{ penalty_name: 'Prison', severity: 'Medium' },
{ penalty_name: 'Fine', severity: 'Low' }
]
}
]
}
I want to store and print only offense_name,**penalty_name** and severity one by one , but I am not able to get the right syntax.
Here is what I have tried so far:
PERSON_SUMMARY[:offenses].each do |offense|
offense_name = offense[:offense_name]
offense[:penalties].each do |penalty|
penalty_name = penalty[:penalty_name]
severity_val = penalty[:severity]
end
end
EDIT: Eventually I need to insert it into the database table through this function:
PersonOffense.where(person_id: person.id).first_or_create(
name: offense_name,
penalty: penalty_name ,
severity: severity_val
end
But I notice an issue, there are multiple penalty names above. Not sure how to insert them.
For example,
I need to insert offense_name twice in my table so that there are 2 entries in the table.
Speeding Prison Medium
Speeding Ticket Low
EDIT: I like Jesse's answer below. How can I use it to insert it in the same order to my method above (inserting offense_name,penalty_name and severity with the result of the answer given below by Jesse.
Rather than just using each, if you map over the offenses, and then flatten them in the end, you'll get what you want:
offenses = PERSON_SUMMARY[:offenses].map do |offense|
offense[:penalties].map do |penalty|
penalty.merge(name: offense[:offense_name])
end
end.flatten
=> [{:penalty_name=>"Prison", :severity=>"Medium", :name=>"Speeding"}, {:penalty_name=>"Ticket", :severity=>"Low", :name=>"Speeding"}, {:penalty_name=>"Prison", :severity=>"Medium", :name=>"Shoplifting"}, {:penalty_name=>"Fine", :severity=>"Low", :name=>"Shoplifting"}]
UPDATE
In ruby, the following is a hash, and you already have a hash
So you can just loop through your new array and create your PersonOffense:
offenses.each do |hash|
PersonOffense.where(person_id: person.id).first_or_create( hash )
end

Resources