KBouldin9
KBouldin9

Reputation: 404

Sort array of string with digits and characters in ruby

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

Answers (3)

Cary Swoveland
Cary Swoveland

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

mu is too short
mu is too short

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

mrzasa
mrzasa

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

Related Questions