简体   繁体   中英

Sorting strings before numbers

For scoping purposes, I'm looking for an unusual sorting mechanism.
Let's suppose we have the following array:

arr = ["33", "a30", "b333", "44", "22", "a15"]

How can I sort it so that non-numeric strings sort before numeric ones?
This is the result I wish to achieve

sorted_arr = ["a15", "a30", "b333", "22", "33", "44"]

I have tried various combinations of sort_by and sort but I've not achieved the correct ordering yet... So, any help will be appreciated. Thanks.

PS: I know I've written "for scoping purposes", but the answer does not need to be for using in a default_scope block. I'll try to adjust it later, once I've found the right sorting technique.

arr.sort_by{|e| [e =~ /\A\d/ ? 1 : 0, e.your_original_sort_condition]}

Here's another way to do it.

Code

def sort_em(arr)
  a = arr.sort
  return a if a.first =~ /\D/ || a.last !~ /\D/  
  a.rotate(a.index(a.find { |e| e =~ /\D/ }))
end

Example

arr = ["33", "a30", "b333", "44", "22", "a15"]
sort_em(arr)
  #=> ["a15", "a30", "b333", "22", "33", "44"]

Notes

  • This will not work if, before separating the numeric and non-numeric entries, you perform some specialized sort that intermixes numeric and non-numeric entries.
  • If " 1" or "1 " may be present and are to be treated as "numeric", add .strip before =~ and ~=~ .
  • If arr is empty, [].first =~ /^\\d+$/ #=> nil

Gentlemen, start your engines

require 'benchmark'

n = 10 # Example
arr = (Array.new(n) {rand(n).to_s } +
       Array.new(n) { (97+rand(26)).chr + rand(n).to_s }).shuffle
  #=> ["9", "5", "a4", "u6", "u3", "g6", "5", "l0", "9", "9",
  #    "3", "8", "t1", "3", "o5", "l6", "3", "i6", "l1", "0"]

n = 200_000
arr = (Array.new(n) {rand(n).to_s } +
       Array.new(n) { (97+rand(26)).chr + rand(n).to_s }).shuffle

def sawa(arr)  arr.sort_by{ |e| [e =~ /\A\d/ ? 1 : 0, e] } end
def darse(arr) arr.partition { |x| x =~ /\D/ }.flat_map(&:sort) end    
def uri(arr)   arr.sort.partition { |x| x[/^[^\d]/] }.inject(:+) end
def cary(arr)
  a = arr.sort
  return a if a.first =~ /\D/ || a.last !~ /\D/  
  a.rotate(a.index(a.find { |e| e =~ /\D/ }))
end

Benchmark.bm(12) do |bm|
  bm.report('@sawa'     ) { sawa(arr)  }
  bm.report('@theDarse' ) { darse(arr) }
  bm.report('@UriAgassi') { uri(arr)   }
  bm.report('@Cary'     ) { cary(arr)  }
end
                   user     system      total        real
@sawa          3.340000   0.020000   3.360000 (  3.364740)
@theDarse      0.530000   0.020000   0.550000 (  0.547846)
@UriAgassi     0.700000   0.010000   0.710000 (  0.711513)
@Cary          0.430000   0.010000   0.440000 (  0.438975)

Admittedly, it's a very limited benchmark.

It's not the most elegant solution but one way to do this would be to create two arrays, one holding all the elements starting with a non-numeric, one holding all the elements starting with a numeric item. Sort the two arrays and then concatenate the arrays

You can do this:

arr.sort.partition { |x| x[/^[^\d]/] }.inject(:+)
#  => ["a15", "a30", "b333", "22", "33", "44"] 

This sorts the array, then it groups all the elements not starting with a digit, and all the elements that do:

arr.sort.partition { |x| x[/^[^\d]/] }
# => [["a15", "a30", "b333"], ["22", "33", "44"]]

and then it joins the two arrays together.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM