Reputation: 404
I have an array with the given strings
array = [
"1mo-30-super",
"1mo-40-classic",
"1mo-30-classic",
"1mo-110-super",
"1mo-20-extra",
"6mo-21-super",
"6mo-11-super",
"12mo-21-classic",
"12mo-21-super"
]
How can I sort the array so that it goes in numerical order, then alphabetical order so the array displays like so:
array = [
"1mo-20-extra",
"1mo-30-classic",
"1mo-30-super",
"1mo-40-classic",
"1mo-110-super",
"6mo-11-super",
"6mo-21-super",
"12mo-21-classic",
"12mo-21-super"
]
Upvotes: 0
Views: 1047
Reputation: 110685
array.sort_by { |s| [s.to_i, s[/(?<=-)\d+/].to_i, s.gsub(/\A.+-/,'')] }
#=> ["1mo-20-extra", "1mo-30-classic", "1mo-30-super", "1mo-40-classic", "1mo-110-super",
# "6mo-11-super", "6mo-21-super", "12mo-21-classic", "12mo-21-super"]
When sorting arrays the method Arrays#<=> is used to order pairs of arrays. See the third paragraph of the doc for an explanation of how that is done.
The arrays used for the sort ordering are as follows.
array.each do |s|
puts "%-15s -> [%2d, %3d, %s]" % [s, s.to_i, s[/(?<=-)\d+/].to_i, s.gsub(/\A.+-/,'')]
end
1mo-30-super -> [ 1, 30, super]
1mo-40-classic -> [ 1, 40, classic]
1mo-30-classic -> [ 1, 30, classic]
1mo-110-super -> [ 1, 110, super]
1mo-20-extra -> [ 1, 20, extra]
6mo-21-super -> [ 6, 21, super]
6mo-11-super -> [ 6, 11, super]
12mo-21-classic -> [12, 21, classic]
12mo-21-super -> [12, 21, super]
(?<=-)
is a positive lookbehind. It requires that the match be immediately preceded by a hyphen. /\A.+-/
matches the beginning of the string followed by one or more characters followed by a hyphen. Because regular expressions are by default greedy, it concludes the match on the second hyphen.
Note that it is not necessary to use regular expressions:
array.sort_by { |s| [s.to_i, s[s.index('-')+1..-1].to_i, s[s.rindex('-')+1..-1]] }
Upvotes: 1
Reputation: 434675
You're looking for a "natural" sort where the numeric substrings will be compared as numbers as the non-numeric parts will be compared like strings. Conveniently enough, arrays in Ruby compare element-by-element and your format is fairly regular so you can get away with a #sort_by
call and a bit of mangling to convert "12mo-21-classic"
to [12, 'mo-', 21, '-classic']
. Something like this for example:
# This is a bit complicated so we'll give the logic a name.
natural_parts = ->(s) { s.match(/(\d+)(\D+)(\d+)(\D+)/).to_a.drop(1).map.with_index { |e, i| i.even?? e.to_i : e } }
array.sort_by(&natural_parts)
Upvotes: 1
Reputation: 23327
You can chain several #sort
method calls, each sorting by a different part of a string (starting with one with smallest priority):
array.sort { |a,b| a.match(/-(.*)$/)[1] <=> b.match(/-(.*)-/)[1] } # sort by last element ('classic', 'super')
.sort { |a,b| a.match(/-(\d+)-/)[1].to_i <=> b.match(/-(\d+)-/)[1].to_i } # sort by the number between dashes
.sort { |a,b| a.to_i <=> b.to_i } # sort by the initial number
=> ["1mo-20-extra",
"1mo-30-classic",
"1mo-30-super",
"1mo-40-classic",
"1mo-110-super",
"6mo-11-super",
"6mo-21-super",
"12mo-21-super",
"12mo-21-classic"]
Upvotes: 0