Module: Philiprehberger::NaturalSort
- Defined in:
- lib/philiprehberger/natural_sort.rb,
lib/philiprehberger/natural_sort/version.rb,
lib/philiprehberger/natural_sort/comparator.rb
Defined Under Namespace
Modules: ArrayRefinement Classes: Error
Constant Summary collapse
- VERSION =
'0.5.0'
Class Method Summary collapse
-
.collate(a, b, case_sensitive: false) ⇒ Integer
Spaceship-style comparator returning -1, 0, or 1.
-
.comparator(case_sensitive: false) ⇒ Proc
Returns a Proc suitable for use with Array#sort.
-
.compare(a, b, case_sensitive: false) ⇒ Integer
Compares two strings using natural sort order.
-
.group_by_prefix(array, case_sensitive: false) ⇒ Hash<String, Array<String>>
Splits each string at the first digit boundary, groups by the non-numeric prefix.
-
.max(array, case_sensitive: false) ⇒ String?
Finds the naturally largest element without full sort.
-
.min(array, case_sensitive: false) ⇒ String?
Finds the naturally smallest element without full sort.
-
.natural_key(str, case_sensitive: false) ⇒ Array
Returns a sort key array usable with Ruby’s built-in sort_by, min_by, max_by, etc.
-
.sort(array, case_sensitive: false, reverse: false, ignore_case: false) ⇒ Array<String, nil>
Sorts an array of strings in natural order.
-
.sort_by(array, case_sensitive: false, reverse: false, ignore_case: false) {|element| ... } ⇒ Array
Sorts an array by the natural order of values returned by the block.
-
.sort_by_stable(array, case_sensitive: false) {|element| ... } ⇒ Array
Stable sort by block result, preserving original order for equal elements.
-
.sort_stable(array, case_sensitive: false) ⇒ Array<String, nil>
Stable sort that preserves original order for equal elements.
-
.tokenize(str, case_sensitive: false) ⇒ Array<String, Integer>
Splits a string into chunks of text and numbers for natural comparison.
Class Method Details
.collate(a, b, case_sensitive: false) ⇒ Integer
Spaceship-style comparator returning -1, 0, or 1.
Suitable for use with Array#sort:
array.sort { |a, b| NaturalSort.collate(a, b) }
179 180 181 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 179 def self.collate(a, b, case_sensitive: false) compare(a, b, case_sensitive: case_sensitive) end |
.comparator(case_sensitive: false) ⇒ Proc
Returns a Proc suitable for use with Array#sort.
71 72 73 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 71 def self.comparator(case_sensitive: false) ->(a, b) { compare(a, b, case_sensitive: case_sensitive) } end |
.compare(a, b, case_sensitive: false) ⇒ Integer
Compares two strings using natural sort order.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 23 def self.compare(a, b, case_sensitive: false) return 0 if a.nil? && b.nil? return -1 if a.nil? return 1 if b.nil? a_str = a.to_s b_str = b.to_s tokens_a = tokenize(a_str, case_sensitive: case_sensitive) tokens_b = tokenize(b_str, case_sensitive: case_sensitive) max_len = [tokens_a.length, tokens_b.length].max max_len.times do |i| chunk_a = tokens_a[i] chunk_b = tokens_b[i] # Shorter token list comes first return -1 if chunk_a.nil? return 1 if chunk_b.nil? # Both numeric if chunk_a.is_a?(Integer) && chunk_b.is_a?(Integer) cmp = chunk_a <=> chunk_b return cmp unless cmp.zero? next end # Both strings if chunk_a.is_a?(String) && chunk_b.is_a?(String) cmp = chunk_a <=> chunk_b return cmp unless cmp.zero? next end # Mixed: numbers sort before strings return chunk_a.is_a?(Integer) ? -1 : 1 end 0 end |
.group_by_prefix(array, case_sensitive: false) ⇒ Hash<String, Array<String>>
Splits each string at the first digit boundary, groups by the non-numeric prefix. Each group’s values are naturally sorted.
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 189 def self.group_by_prefix(array, case_sensitive: false) groups = {} array.each do |str| s = str.to_s match = s.match(/\A([^\d]*)/) prefix = match ? match[1] : '' groups[prefix] ||= [] groups[prefix] << str end groups.each_value do |values| values.replace(sort(values, case_sensitive: case_sensitive)) end groups end |
.max(array, case_sensitive: false) ⇒ String?
Finds the naturally largest element without full sort.
132 133 134 135 136 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 132 def self.max(array, case_sensitive: false) return nil if array.empty? array.max { |a, b| compare(a, b, case_sensitive: case_sensitive) } end |
.min(array, case_sensitive: false) ⇒ String?
Finds the naturally smallest element without full sort.
121 122 123 124 125 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 121 def self.min(array, case_sensitive: false) return nil if array.empty? array.min { |a, b| compare(a, b, case_sensitive: case_sensitive) } end |
.natural_key(str, case_sensitive: false) ⇒ Array
Returns a sort key array usable with Ruby’s built-in sort_by, min_by, max_by, etc.
The key is an array of [type_flag, value] pairs where type_flag ensures correct ordering between numeric and string chunks (numbers sort before strings).
146 147 148 149 150 151 152 153 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 146 def self.natural_key(str, case_sensitive: false) return [[-1, '']] if str.nil? tokens = tokenize(str.to_s, case_sensitive: case_sensitive) tokens.map do |chunk| chunk.is_a?(Integer) ? [0, chunk] : [1, chunk] end end |
.sort(array, case_sensitive: false, reverse: false, ignore_case: false) ⇒ Array<String, nil>
Sorts an array of strings in natural order.
82 83 84 85 86 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 82 def self.sort(array, case_sensitive: false, reverse: false, ignore_case: false) effective_case_sensitive = ignore_case ? false : case_sensitive result = array.sort { |a, b| compare(a, b, case_sensitive: effective_case_sensitive) } reverse ? result.reverse : result end |
.sort_by(array, case_sensitive: false, reverse: false, ignore_case: false) {|element| ... } ⇒ Array
Sorts an array by the natural order of values returned by the block.
96 97 98 99 100 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 96 def self.sort_by(array, case_sensitive: false, reverse: false, ignore_case: false, &block) effective_case_sensitive = ignore_case ? false : case_sensitive result = array.sort { |a, b| compare(block.call(a), block.call(b), case_sensitive: effective_case_sensitive) } reverse ? result.reverse : result end |
.sort_by_stable(array, case_sensitive: false) {|element| ... } ⇒ Array
Stable sort by block result, preserving original order for equal elements.
161 162 163 164 165 166 167 168 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 161 def self.sort_by_stable(array, case_sensitive: false, &block) array.each_with_index .sort_by do |element, index| key_str = block.call(element) [natural_key(key_str, case_sensitive: case_sensitive), index] end .map(&:first) end |
.sort_stable(array, case_sensitive: false) ⇒ Array<String, nil>
Stable sort that preserves original order for equal elements.
107 108 109 110 111 112 113 114 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 107 def self.sort_stable(array, case_sensitive: false) array.each_with_index .sort_by do |element, index| [tokenize(element.nil? ? '' : element.to_s, case_sensitive: case_sensitive), element.nil? ? 0 : 1, index] end .map(&:first) end |
.tokenize(str, case_sensitive: false) ⇒ Array<String, Integer>
Splits a string into chunks of text and numbers for natural comparison.
10 11 12 13 14 15 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 10 def self.tokenize(str, case_sensitive: false) normalized = case_sensitive ? str : str.downcase normalized.scan(/\d+|[^\d]+/).map do |chunk| chunk.match?(/\A\d+\z/) ? chunk.to_i : chunk end end |