I'm working on a RoR web application.
I have to loop over an array of hashes in a custom order based on the value of the hash.
In this example, the "Pending" status needs to appear first in the list.
Is it possible to customize the order of the array in the each loop?
<% example_statuses = [{ :status => "Active", :job_count => 0 }, { :status => "Pending", :job_count => 1 }, { :status => "Complete", :job_count => 3 }] %>
<% example_statuses.each do |es| %>
<h3><%= es[:status] %></h3>
<% end %>
UPDATE: I have a simple working example that involves removing and prepending the pending status hash back into the array. HOWEVER is there a way to create a custom order instead of manually replacing values one by one?
<% example_statuses = [{ :status => "Active", :job_count => 0 }, { :status => "Pending", :job_count => 1 }, { :status => "Complete", :job_count => 3 }] %>
<%= pending = example_statuses.find { |x| x[:status] == "Pending"} %>
<% example_statuses.delete(pending) %>
<% example_statuses.prepend(pending) %>
<% example_statuses.each do |es| %>
<h3><%= es[:status] %></h3>
<% end %>
CodePudding user response:
There are two important concepts here:
- Using
Array#index
to find where in an array an element is found. - The 'spaceship operator'
<=>
which is howArray#sort
works. You can investigate it here
In your case solution would be:
example_statuses = [{ :status => "Active", :job_count => 0 }, { :status => "Pending", :job_count => 1 }, { :status => "Complete", :job_count => 3 }]
order = ["Pending", "Active", "Complete"]
sorted_array = example_statuses.sort do |a,b|
order.index(a[:status]) <=> order.index(b[:status])
end
# => [{:status=>"Pending", :job_count=>1}, {:status=>"Active", :job_count=>0}, {:status=>"Complete", :job_count=>3}]
CodePudding user response:
you can try it with single line like this example_statuses.sort_by{|x| x[:status] == 'Pending' ? '' : x[:status] }
CodePudding user response:
As I understand you are given an array order
that can be reordered to equal
example_statuses.map { |h| h[:status] }
#=> ["Active", "Pending", "Complete"]
and that you wish to sort example_statuses
to produce an array sorted
such that
sorted.map { |h| h[:status] } == order
#=> true
You do not need to actually sort example_statues
, which would have a computational complexity of O(n*log(n)
) where n
equals the number of elements in example_statuses
.
Instead, do the following, which has a computational complexity close to O(n
). Suppose
order = ["Pending", "Active", "Complete"]
Case 1: g[:status] != h[:status]
for all pairs of distinct elements g
and h
of example_statuses
In this case compute
example_statuses.each_with_object({}) { |g,h| h[g[:status]] = g }
.values_at(*order)
#=> [{:status=>"Pending", :job_count=>1},
# {:status=>"Active", :job_count=>0},
# {:status=>"Complete", :job_count=>3}]
The receiver of Hash#values_at is seen to be the following.
example_statuses.each_with_object({}) { |g,h| h[g[:status]] = g }
#=> {"Active"=>{:status=>"Active", :job_count=>0},
# "Pending"=>{:status=>"Pending", :job_count=>1},
# "Complete"=>{:status=>"Complete", :job_count=>3}}
I earlier claimed that the computational complexity was "almost" O(n
). Building the hash example_statuses.each_with_object({}) { |g,h| h[g[:status]] = g }
is O(n
) but the key lookup for each element or order
is only "almost" O(1). Were is a constant time the complexity would be O(n
).
Case 2: g[:status] == h[:status]
for at least one pair of distinct elements g
and h
of example_statuses
Of course this case may not be permitted but that has not been made clear by the question.
Suppose order
is as before but example_statuses
is as follows.
example_statuses = [
{ :status => "Active", :job_count => 0 },
{ :status => "Pending", :job_count => 1},
{ :status => "Complete", :job_count => 3 },
{ :status => "Active", :job_count => 7 }
]
Note that example_statuses[0][:status]
and example_statuses[3][:status]
both equal "Active"
.
We need just a slight modification of the Case 1 calculation (which does not affect computational complexity).
example_statuses.each_with_object(Hash.new { |h,k| h[k] = [] }) do |g,h|
h[g[:status]] << g
end.values_at(*order).flatten
#=> [{:status=>"Pending", :job_count=>1},
# {:status=>"Active", :job_count=>0},
# {:status=>"Active", :job_count=>7},
# {:status=>"Complete", :job_count=>3}]
The receivers of values_at
is seen to equal the following.
{"Active"=>[{:status=>"Active", :job_count=>0},
{:status=>"Active", :job_count=>7}],
"Pending"=>[{:status=>"Pending", :job_count=>1}],
"Complete"=>[{:status=>"Complete", :job_count=>3}]}
See the form of Hash::new that takes a block and no argument. When h
does not have a key [g[:status]
this causes h[g[:status]]
to be assigned to an empty array before executing g[:status]<< g
.