3

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).

1
  • I've now added an example. Commented Mar 1, 2015 at 22:56

3 Answers 3

3

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"}]
Sign up to request clarification or add additional context in comments.

2 Comments

I'd definitely go for this option, since it's the most intention-revealing from the current 3.
I think the question is a good one but I'm willing to bet a few other great solutions will come about
1

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"}] 

1 Comment

That's much simpler than mine! Well played.
0

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.

4 Comments

What if the data contains 'Canada' and 'Cambodia'?
I went down this route too but this will only give you the ord for the first value of the string, you can't guarantee that Algeria and Albania will work correctly.
I was already working on this on my console. Wait a min please.
Updated. Working. Too ugly.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.