bogardpd
bogardpd

Reputation: 287

How can I sort a Ruby 2D array by multiple strings descending?

I have an array in Ruby (1.9.3) in which each element describes several parameters of an airport:

@airport_array = Array.new    
@airports.each do |airport|
  @airport_array.push({:id => airport.id, :iata_code => airport.iata_code, :city => airport.city, :country => airport.country})
end

In particular, :city and :country are both strings, and I would like to be able to sort by country in reverse alphabetical order and then city in alphabetical order.

I have been able to sort integers using something like this:

@airport_array = @airport_array.sort_by {|airport| [-airport[:country], airport[:city]]}

However, this syntax (in particular, using the - sign to denote a reverse sort) doesn't seem to work with strings. I get the following error:

undefined method `-@' for "United States":String

If I remove the minus sign, I don't get an error, but as expected, the sort is alphabetical for both parameters.

Is there a way I can sort this array by two strings, with exactly one string in reverse alphabetical order?

As an example, say I have the following array:

[{:id=>1, :iata_code=>"SEA", :city=>"Seattle", :country=>"United States"},
{:id=>2, :iata_code=>"DEN", :city=>"Denver", :country=>"United States"},
{:id=>3, :iata_code=>"YVR", :city=>"Vancouver", :country=>"Canada"},
{:id=>4, :iata_code=>"HNL", :city=>"Honolulu", :country=>"United States"},
{:id=>5, :iata_code=>"YOW", :city=>"Ottawa", :country=>"Canada"},
{:id=>6, :iata_code=>"YHZ", :city=>"Halifax", :country=>"Canada"}]

After I sort it, I would like to have the following array:

[{:id=>2, :iata_code=>"DEN", :city=>"Denver", :country=>"United States"},
{:id=>4, :iata_code=>"HNL", :city=>"Honolulu", :country=>"United States"},
{:id=>1, :iata_code=>"SEA", :city=>"Seattle", :country=>"United States"},
{:id=>6, :iata_code=>"YHZ", :city=>"Halifax", :country=>"Canada"},
{:id=>5, :iata_code=>"YOW", :city=>"Ottawa", :country=>"Canada"},
{:id=>3, :iata_code=>"YVR", :city=>"Vancouver", :country=>"Canada"}]

So the countries are in reverse alphabetical order (U, C), and then within a country, the cities are in alphabetical order (D, H, S and H, O, V).

Upvotes: 3

Views: 219

Answers (3)

dgilperez
dgilperez

Reputation: 10796

You can use the ASCII value of the first letter of your country - using String#ord which outputs an Integer - for the reverse sort requirement like this:

@airport_array.sort_by {|airport| [-airport[:country][0].ord, airport[:city]]}

EDIT: This option will not guarantee correct sort of different countries starting by the same letter, such as 'Cambodia' and 'Canada', so let's build upon this technique to consider all the letters in the name.

I'd came up with this function to obtain the transposed word from a given string, defining transposed word as the sum of the transposed letters across the ascii table (ie. 'A' becomes 'Z', 'C' becomes 'X' and so on), effectively obtaining a word you can use in your reversed alphabetical order.

# For each upcased letter, transpose the letter according to the ascii table 
# considering that 'A'.ord => 65 and 'Z'.ord => 90, 
# using 65 - X + 90 to obtain the ascii value for the transposed letter.
#
# Usage examples:
#   ascii_inverse('A') => 'Z'
#   ascii_inverse('Denver') => "WVMEVI"
#   ascii_inverse('DENVER') => "WVMEVI"

def ascii_inverse(text)
  text.upcase.chars.map{ |char| (155 - char.ord).abs }.map(&:chr).join
end

Now you can use this method into your sort_by statement:

@airport_array.sort_by {|airport| [ascii_inverse(airport[:country]), airport[:city]]}

At last, I'd say that I consider this just an exercise to see how far I'd go down this road. Although it works, I'd be reluctant myself of using this approach, unless there is clear performance benefits and I'd need that and I'd probably fall to simpler @Anthony's sort approach.

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110675

This answer should be regarded as a curiosity. It assumes that all country names are a single word.

arr = [
  {:id=>1, :iata_code=>"SEA", :city=>"Seattle",   :country=>"United States"},
  {:id=>2, :iata_code=>"DEN", :city=>"Denver",    :country=>"United States"},
  {:id=>3, :iata_code=>"YVR", :city=>"Vancouver", :country=>"Canada"},
  {:id=>4, :iata_code=>"HNL", :city=>"Honolulu",  :country=>"United States"},
  {:id=>5, :iata_code=>"YOW", :city=>"Ottawa",    :country=>"Canada"},
  {:id=>6, :iata_code=>"YHZ", :city=>"Halifax",   :country=>"Canada"}]

arr.sort_by { |h| [-h[:country].downcase.to_i(36), h[:city]] }
  #=> [{:id=>2, :iata_code=>"DEN", :city=>"Denver",    :country=>"United States"},
  #    {:id=>4, :iata_code=>"HNL", :city=>"Honolulu",  :country=>"United States"},
  #    {:id=>1, :iata_code=>"SEA", :city=>"Seattle",   :country=>"United States"},
  #    {:id=>6, :iata_code=>"YHZ", :city=>"Halifax",   :country=>"Canada"},
  #    {:id=>5, :iata_code=>"YOW", :city=>"Ottawa",    :country=>"Canada"},
  #    {:id=>3, :iata_code=>"YVR", :city=>"Vancouver", :country=>"Canada"}] 

Upvotes: 1

Anthony
Anthony

Reputation: 15957

I thought I was going to get this to work using Enumerable#sort_by but I ran into the same issue you did so I used sort with a block. Enumerable#sort with a block is known to be slower than sort_by so I'm curious how others might answer this.

I got it working using:

arr.sort { |a, b| [b[:country], a[:city]] <=> [a[:country], b[:city]] }

It looks like this:

[76] pry(main)> arr
=> [{:id=>1, :iata_code=>"SEA", :city=>"Seattle", :country=>"United States"},
 {:id=>2, :iata_code=>"DEN", :city=>"Denver", :country=>"United States"},
 {:id=>3, :iata_code=>"YVR", :city=>"Vancouver", :country=>"Canada"},
 {:id=>4, :iata_code=>"HNL", :city=>"Honolulu", :country=>"United States"},
 {:id=>5, :iata_code=>"YOW", :city=>"Ottawa", :country=>"Canada"},
 {:id=>6, :iata_code=>"YHZ", :city=>"Halifax", :country=>"Canada"},
 {:id=>2, :iata_code=>"DOV", :city=>"Dover", :country=>"United States"}]

[77] pry(main)> arr.sort { |a, b| [b[:country], a[:city]] <=> [a[:country], b[:city]]  }
=> [{:id=>2, :iata_code=>"DEN", :city=>"Denver", :country=>"United States"},
 {:id=>2, :iata_code=>"DOV", :city=>"Dover", :country=>"United States"},
 {:id=>4, :iata_code=>"HNL", :city=>"Honolulu", :country=>"United States"},
 {:id=>1, :iata_code=>"SEA", :city=>"Seattle", :country=>"United States"},
 {:id=>6, :iata_code=>"YHZ", :city=>"Halifax", :country=>"Canada"},
 {:id=>5, :iata_code=>"YOW", :city=>"Ottawa", :country=>"Canada"},
 {:id=>3, :iata_code=>"YVR", :city=>"Vancouver", :country=>"Canada"}]

Upvotes: 3

Related Questions