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.10.0'
Class Method Summary collapse
-
.between?(value, min, max, case_sensitive: false) ⇒ Boolean
Returns true if value falls within the natural sort range [min, max] inclusive.
-
.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!(array, case_sensitive: false, reverse: false) ⇒ Array<String, nil>
Sorts an array of strings in place using natural ordering.
-
.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_index(array, case_sensitive: false, reverse: false) ⇒ Array<Integer>
Returns an array of original indices representing the natural-sort permutation.
-
.sort_paths(paths, separator: '/', case_sensitive: false, reverse: false) ⇒ Array<String>
Natural sort for separator-delimited paths.
-
.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.
-
.uniq(array, case_sensitive: false) ⇒ Array<String, nil>
Deduplicates an array preserving first-occurrence order, treating elements as equal when natural comparison returns 0.
Class Method Details
.between?(value, min, max, case_sensitive: false) ⇒ Boolean
Returns true if value falls within the natural sort range [min, max] inclusive.
205 206 207 208 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 205 def self.between?(value, min, max, case_sensitive: false) compare(min, value, case_sensitive: case_sensitive) <= 0 && compare(value, max, case_sensitive: case_sensitive) <= 0 end |
.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) }
194 195 196 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 194 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.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 216 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.
147 148 149 150 151 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 147 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.
136 137 138 139 140 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 136 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).
161 162 163 164 165 166 167 168 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 161 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!(array, case_sensitive: false, reverse: false) ⇒ Array<String, nil>
Sorts an array of strings in place using natural ordering.
Mirrors Ruby’s ‘Array#sort!` for callers who want to mutate the receiver rather than copy. Returns the same array.
97 98 99 100 101 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 97 def self.sort!(array, case_sensitive: false, reverse: false) array.sort! { |a, b| compare(a, b, case_sensitive: case_sensitive) } array.reverse! if reverse array 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.
111 112 113 114 115 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 111 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.
176 177 178 179 180 181 182 183 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 176 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_index(array, case_sensitive: false, reverse: false) ⇒ Array<Integer>
Returns an array of original indices representing the natural-sort permutation.
241 242 243 244 245 246 247 248 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 241 def self.sort_index(array, case_sensitive: false, reverse: false) return [] if array.empty? result = array.each_with_index .sort { |(a, _), (b, _)| compare(a, b, case_sensitive: case_sensitive) } .map(&:last) reverse ? result.reverse : result end |
.sort_paths(paths, separator: '/', case_sensitive: false, reverse: false) ⇒ Array<String>
Natural sort for separator-delimited paths.
Splits each path on separator, naturally sorts the resulting segment arrays lexicographically, then returns the original path strings in sorted order. Trailing separators are preserved in the output; the split segments (with an empty trailing element) are used for comparison.
278 279 280 281 282 283 284 285 286 287 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 278 def self.sort_paths(paths, separator: '/', case_sensitive: false, reverse: false) decorated = paths.map do |path| segments = path.to_s.split(separator, -1) key = segments.map { |seg| natural_key(seg, case_sensitive: case_sensitive) } [key, path] end sorted = decorated.sort_by(&:first).map(&:last) reverse ? sorted.reverse : sorted end |
.sort_stable(array, case_sensitive: false) ⇒ Array<String, nil>
Stable sort that preserves original order for equal elements.
122 123 124 125 126 127 128 129 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 122 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 |
.uniq(array, case_sensitive: false) ⇒ Array<String, nil>
Deduplicates an array preserving first-occurrence order, treating elements as equal when natural comparison returns 0.
256 257 258 259 260 261 262 263 264 |
# File 'lib/philiprehberger/natural_sort/comparator.rb', line 256 def self.uniq(array, case_sensitive: false) result = [] array.each do |element| next if result.any? { |kept| compare(kept, element, case_sensitive: case_sensitive).zero? } result << element end result end |