Kobius
Kobius

Reputation: 714

Custom Sort array of strings by another array of strings - Ruby

I have an array that is currently sorted alphabetically, and I'm trying to sort it by a manual order of strings.

Current code:

list = ["gold","silver","bronze","steel","copper"]

list = list.sort { |a, b| a <=> b }

What I'm trying to achieve: (With a blank entry as a separator)

list = ["gold","silver","bronze","steel","copper"]

sort_order = ["bronze","silver","gold","","copper","steel"]

list = list.sort_by sort_order

Output: bronze | silver | gold | - | copper | steel

Is this possible? Currently stuck with these error messages:

comparison of Integer with nil failed
comparison of String with String failed

Upvotes: 1

Views: 1364

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110675

I assume that:

  • every element of list is in sort_order;
  • sort_order may contain elements that are not in list;
  • list may contain duplicates; and
  • sort_order contains no duplicates.

If sort_order initially contains duplicates the temporary array sort_order.uniq can be used in the calculations.

Observe that if, as in the example, list contains no duplicates and sort_order contains no elements other than those in list, sorting list by the order of its elements in sort_order is trivial, as it merely returns sort_order.

The following is more efficient than methods that use sort or sort_by (O(n) versus O(n*log(n)) computational complexity.)

list = ["gold", "copper", "silver", "copper", "steel", "gold"]
sort_order = ["bronze", "silver", "tin", "gold", "copper", "steel"]

count = list.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
  #=> {"gold"=>2, "copper"=>2, "silver"=>1, "steel"=>1} 
sort_order.flat_map { |e| [e]*count[e] }.reject(&:empty?)
  #=> ["silver", "gold", "gold", "copper", "copper", "steel"] 

Upvotes: 3

baseballlover723
baseballlover723

Reputation: 683

you may want to just make a medal class or something, with a numerical rank and a string name, and then just sort by the rank. like so.

class Medal
  attr_accessor :name, :rank

  def initialize(name, rank)
    @name = name
    @rank = rank
  end
end

list = [Medal.new("gold", 0), Medal.new("silver", 1), Medal.new("bronze", 2), Medal.new("steel", 4), Medal.new("copper", 3)]

list = list.sort_by &:rank

you could take this a big farther if you want and define a map of medal names to ranks to drop the rank from the initialize. You could also just define each medal as a constant if you want, since you'll probably only have a fixed list.

But if you don't want to do that then you could probably also just have a list of the order and sort by the index, like this

list = ["gold","silver","bronze","steel","copper"]

sort_order = ["bronze","silver","gold","","copper","steel"]

list = list.sort_by {|m| sort_order.index m}

Upvotes: 1

Related Questions