How to sort an alphanumeric array in ruby

后端 未结 8 1928
没有蜡笔的小新
没有蜡笔的小新 2021-01-19 16:01

How I can sort array data alphanumerically in ruby?

Suppose my array is a = [test_0_1, test_0_2, test_0_3, test_0_4, test_0_5, test_0_6, test_0_7, test_0_8, te

8条回答
  •  青春惊慌失措
    2021-01-19 16:44

    Posting here a more general way to perform a natural decimal sort in Ruby. The following is inspired by my code for sorting "like Xcode" from https://github.com/CocoaPods/Xcodeproj/blob/ca7b41deb38f43c14d066f62a55edcd53876cd07/lib/xcodeproj/project/object/helpers/sort_helper.rb, itself loosely inspired by https://rosettacode.org/wiki/Natural_sorting#Ruby.

    Even if it's clear that we want "10" to be after "2" for a natural decimal sort, there are other aspects to consider with multiple possible alternative behaviors wanted:

    • How do we treat equality like "001"/"01": do we keep the original array order or do we have a fallback logic? (Below, choice is made to have a second pass with a strict ordering logic in case of equality during first pass)
    • Do we ignore consecutive spaces for sorting, or does each space character count? (Below, choice is made to ignore consecutive spaces on first pass, and have a strict comparison on the equality pass)
    • Same question for other special characters. (Below, choice is made to make any non-space and non-digit character count individually)
    • Do we ignore case or not; is "a" before or after "A"? (Below, choice is made to ignore case on first pass, and we have "a" before "A" on the equality pass)

    With those considerations:

    • It means that we should almost certainly use scan instead of split, because we're going to have potentially three kinds of substrings to compare (digits, spaces, all-the-rest).
    • It means that we should almost certainly work with a Comparable class and with def <=>(other) because it's not possible to simply map each substring to something else that would have two distinct behaviors depending on context (the first pass and the equality pass).

    This results in a bit lengthy implementation, but it works nicely for edge situations:

      # Wrapper for a string that performs a natural decimal sort (alphanumeric).
      # @example
      #   arrayOfFilenames.sort_by { |s| NaturalSortString.new(s) }
      class NaturalSortString
        include Comparable
        attr_reader :str_fallback, :ints_and_strings, :ints_and_strings_fallback, :str_pattern
    
        def initialize(str)
          # fallback pass: case is inverted
          @str_fallback = str.swapcase
          # first pass: digits are used as integers, spaces are compacted, case is ignored
          @ints_and_strings = str.scan(/\d+|\s+|[^\d\s]+/).map do |s|
            case s
            when /\d/ then Integer(s, 10)
            when /\s/ then ' '
            else s.downcase
            end
          end
          # second pass: digits are inverted, case is inverted
          @ints_and_strings_fallback = @str_fallback.scan(/\d+|\D+/).map do |s|
            case s
            when /\d/ then Integer(s.reverse, 10)
            else s
            end
          end
          # comparing patterns
          @str_pattern = @ints_and_strings.map { |el| el.is_a?(Integer) ? :i : :s }.join
        end
    
        def <=>(other)
          if str_pattern.start_with?(other.str_pattern) || other.str_pattern.start_with?(str_pattern)
            compare = ints_and_strings <=> other.ints_and_strings
            if compare != 0
              # we sort naturally (literal ints, spaces simplified, case ignored)
              compare
            else
              # natural equality, we use the fallback sort (int reversed, case swapped)
              ints_and_strings_fallback <=> other.ints_and_strings_fallback
            end
          else
            # type mismatch, we sort alphabetically (case swapped)
            str_fallback <=> other.str_fallback
          end
        end
      end
    

    Usage

    Example 1:

    arrayOfFilenames.sort_by { |s| NaturalSortString.new(s) }
    

    Example 2:

    arrayOfFilenames.sort! do |x, y|
      NaturalSortString.new(x) <=> NaturalSortString.new(y)
    end
    

    You may find my test case at https://github.com/CocoaPods/Xcodeproj/blob/ca7b41deb38f43c14d066f62a55edcd53876cd07/spec/project/object/helpers/sort_helper_spec.rb, where I used this reference for ordering: [ ' a', ' a', '0.1.1', '0.1.01', '0.1.2', '0.1.10', '1', '01', '1a', '2', '2 a', '10', 'a', 'A', 'a ', 'a 2', 'a1', 'A1B001', 'A01B1', ]

    Of course, feel free to customize your own sorting logic now.

提交回复
热议问题