Home > database >  Rails loop over hash in custom order conditionally
Rails loop over hash in custom order conditionally

Time:04-12

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:

  1. Using Array#index to find where in an array an element is found.
  2. The 'spaceship operator' <=> which is how Array#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.

  • Related