user2101699
user2101699

Reputation: 257

Ruby sort_by keep original order of not sorted

I have a hash that looks something like:

{ "3g3dsd3" => {"price"=>0.12, "avg"=>81, "top"=>true}, "1sf3af" => {"price"=>0.14, "avg"=>121, "top"=>false}...}

I wanna reorder it so that items with "top"=>true would be on top but besides that the items would keep the previous order, meaning items with the same top value wont change the order between when.

I could not find evidence in the original doc that sort_by keeps the order of the attributes that are not sorted.

How can I do that?

Upvotes: 1

Views: 729

Answers (4)

3limin4t0r
3limin4t0r

Reputation: 21130

sort_by specifically states:

The result is not guaranteed to be stable. When two keys are equal, the order of the corresponding elements is unpredictable.

Meaning your question is well founded.

In this scenario you could use partition to split the collection up into two. Then paste them back together in the way you desire.

top, non_top = hash.partition { |key, hash| hash['top'] }
result = (top + non_top).to_h

partition keeps the order of the of the original array.


If you are a fan of one liners, the following does the same in this scenario.

result = hash.partition { |key, hash| hash['top'] }.flatten(1).to_h

Upvotes: 1

Stefan
Stefan

Reputation: 114188

You can make sort_by stable by incorporating with_index.

Instead of:

collection.sort_by { |...| ... }

You write:

collection.sort_by.with_index { |(...), i| [..., i] }

Applied to your problem:

hash.sort_by { |_k, v| v['top'] ? 0 : 1 }                      # unstable

hash.sort_by.with_index { |(_k, v), i| [v['top'] ? 0 : 1, i] } # stable

Upvotes: 3

Cary Swoveland
Cary Swoveland

Reputation: 110685

Here is a way to reorder the hash's key-value pairs without converting the hash to one or more arrays and then converting those arrays, after modification, back to a hash.

Suppose

h = { "2rh4abc" => {"price"=>0.18, "avg"=>130, "top"=>false },
      "3g3dsd3" => {"price"=>0.12, "avg"=>81,  "top"=>true  },
      "1sf3af"  => {"price"=>0.14, "avg"=>121, "top"=>false } }

then

top_key = h.find { |_,v| v["top"] == true }.first
  #=> "3g3dsd3"
{ top_key=>h[top_key] }.merge(h.reject { |k,_| k == top_key })
  #=> {"3g3dsd3"=>{"price"=>0.12, "avg"=>81, "top"=>true},
  #    "2rh4abc"=>{"price"=>0.18, "avg"=>130, "top"=>false},
  #    "1sf3af"=>{"price"=>0.14, "avg"=>121, "top"=>false}}

If h can be mutated (modified in place) this can be simplified to:

{ top_key=>h.delete(top_key) }.merge(h)
  #=> {"3g3dsd3"=>{"price"=>0.12, "avg"=>81, "top"=>true},
  #    "2rh4abc"=>{"price"=>0.18, "avg"=>130, "top"=>false},    
  #    "1sf3af"=>{"price"=>0.14, "avg"=>121, "top"=>false}} 

Upvotes: 0

dssjoblom
dssjoblom

Reputation: 124

Ruby sort / sort_by are not stable. You can however use the index in the original input as a tiebreaker: Is sort in Ruby stable?

Upvotes: 0

Related Questions