natural_sort 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c65073bdbc533210318de532342d680a5bcf1005e02133df2a06deb9541eeb93
4
- data.tar.gz: e80ef3969d51b113b88e2d880ecc85c35fbe960876dd61747f28a3ed8825f3da
3
+ metadata.gz: 354422d90d16ba088c221c5d69e708a0abe72c3fcc693016144b7b36237cd106
4
+ data.tar.gz: c05143277661784e8de74e8acf05100eeaee1bc53566bc0ff4b61e8e8ef4bcc6
5
5
  SHA512:
6
- metadata.gz: 942436f6dd57b0234880b9e719c9803ff4c4d8a4f7e7f0419297775d27fe2c2adcaa9b059d3eb9c0ec567865bf7d2a96bdcedc28bb07d2feaf321f8d52d3b1eb
7
- data.tar.gz: 506dda46929bfcc7682844f21aef3d051ae53c1f797711986405aa4b8605618aa6d553843defda251699b07eecba7b43f0f6b9f72d30a57660bf311071211ad2
6
+ metadata.gz: c5f16a6eeeb80cc07bb97891d8c77ce4d4a063b1fd276d760b4c5d4fc0bfd06729ce870bc82132cbd534ebc9fe1b25bbe0137ba9d0dca2eacaf35ffa48a125de
7
+ data.tar.gz: e441af4a1085b92b999f9e8f5da4cf1e71b20aa4e74fd35f135b1122fed609e9c8d74a852522731a3ef1a36853e7199add5558f5df134cb60e3263aa6fded225
data/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The sort
6
+ order itself is part of the public API — a change to how strings are ordered is
7
+ a breaking change.
8
+
9
+ ## [1.0.0] - 2026-06-14
10
+
11
+ First stable release.
12
+
13
+ Ordering is a faithful port of Martin Pool's natural-order string comparison
14
+ (the algorithm PHP's `strnatcmp` uses), so results are reference-defined.
15
+ Ordering may differ from the 0.x series — review your sort output before
16
+ upgrading, and pin with `~> 1.0`.
17
+
18
+ - Faithful `strnatcmp` ordering: leading-zero digit runs compare as text,
19
+ whitespace is insignificant on its own, non-digit bytes compare by byte value
20
+ (case-sensitive), and arbitrarily large integers compare exactly.
21
+ - Plugs into Ruby's own sort methods: `NaturalSort.sort`, `.sort!`, `.compare`,
22
+ `.key`, and `&NaturalSort` as a comparison block.
23
+ - `NaturalSort::Key` is a frozen, immutable comparison key.
24
+ - Tolerates malformed or ASCII-incompatible encodings, sorting by byte value
25
+ instead of raising.
26
+ - Opt-in extras kept out of the default require: the `NaturalSort()` Kernel
27
+ helper (`require "natural_sort/kernel"`) and `Array`/`Hash`/`Set` refinements
28
+ (`require "natural_sort/refinements"`).
29
+ - Requires Ruby 3.3 or newer.
30
+
31
+ ## [0.3.0] - 2018-09-27
32
+
33
+ - Handle additional edge cases for multi-segment numbers.
34
+
35
+ ## [0.2.0] - 2016-11-14
36
+
37
+ - Add `NaturalSort.sort!` and expand the usage examples.
38
+
39
+ ## [0.1.0] - 2016-01-03
40
+
41
+ - Initial release.
42
+
43
+ [1.0.0]: https://github.com/rwz/natural_sort/compare/v0.3.0...v1.0.0
44
+ [0.3.0]: https://github.com/rwz/natural_sort/compare/v0.2.0...v0.3.0
45
+ [0.2.0]: https://github.com/rwz/natural_sort/compare/v0.1.0...v0.2.0
46
+ [0.1.0]: https://github.com/rwz/natural_sort/releases/tag/v0.1.0
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015 Pavel Pravosud
3
+ Copyright (c) 2015-2026 Pavel Pravosud
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,92 +1,175 @@
1
1
  # Natural Sort
2
2
 
3
- [![Build Status](https://api.travis-ci.org/rwz/natural_sort.svg?branch=master)][travis]
4
- [![Gem Version](http://img.shields.io/gem/v/natural_sort.svg)][gem]
5
- [![Code Climate](http://img.shields.io/codeclimate/github/rwz/natural_sort.svg)][codeclimate]
3
+ [![CI](https://github.com/rwz/natural_sort/actions/workflows/ci.yml/badge.svg)][ci]
4
+ [![Gem Version](https://img.shields.io/gem/v/natural_sort.svg)][gem]
6
5
 
7
- [travis]: https://travis-ci.org/rwz/natural_sort
6
+ [ci]: https://github.com/rwz/natural_sort/actions/workflows/ci.yml
8
7
  [gem]: https://rubygems.org/gems/natural_sort
9
- [codeclimate]: https://codeclimate.com/github/rwz/natural_sort
10
8
 
11
- Natual sorting implementation in Ruby.
9
+ Natural-sort ordering for Ruby — sort strings the way people read them, so
10
+ `"a2"` comes before `"a10"` instead of after it.
11
+
12
+ ```ruby
13
+ %w[a1 a10 a2].sort # => ["a1", "a10", "a2"] # lexical: a10 before a2
14
+ NaturalSort.sort(%w[a1 a10 a2]) # => ["a1", "a2", "a10"] # natural
15
+ ```
12
16
 
13
17
  ## Installation
14
18
 
15
- Add this line to your application's Gemfile:
19
+ Add it to your Gemfile:
16
20
 
17
21
  ```ruby
18
22
  gem "natural_sort"
19
23
  ```
20
24
 
21
- And then execute:
25
+ then `bundle install`. Or grab it directly:
22
26
 
23
- $ bundle
24
-
25
- Or install it yourself as:
27
+ ```
28
+ $ gem install natural_sort
29
+ ```
26
30
 
27
- $ gem install natural_sort
31
+ Requires Ruby 3.3 or newer.
28
32
 
29
33
  ## Usage
30
34
 
35
+ `NaturalSort` is a comparator that plugs into Ruby's own sort methods — it
36
+ doesn't replace them.
37
+
31
38
  ```ruby
32
- list = ["a10", "a", "a20", "a1b", "a1a", "a2", "a0", "a1"]
33
- list.sort(&NaturalSort) # => ["a", "a0", "a1", "a1a", "a1b", "a2", "a10", "a20"]
39
+ list = %w[a10 a a20 a1b a1a a2 a0 a1]
40
+
41
+ NaturalSort.sort(list) # => ["a", "a0", "a1", "a1a", "a1b", "a2", "a10", "a20"]
42
+ NaturalSort.sort!(list) # same, but sorts `list` in place and returns it
43
+ list.sort(&NaturalSort) # NaturalSort works directly as the comparison block
34
44
  ```
35
45
 
46
+ `sort(&NaturalSort)` works because the module is a *comparator*. To sort by a
47
+ *derived* value you want a *key* instead — `NaturalSort.key(x)`, or the
48
+ `NaturalSort()` helper — for `sort_by`, `min_by`, and friends:
49
+
36
50
  ```ruby
37
- list = ["a10", "a", "a20", "a1b", "a1a", "a2", "a0", "a1"]
38
- NaturalSort.sort(list) # => ["a", "a0", "a1", "a1a", "a1b", "a2", "a10", "a20"]
51
+ require "natural_sort/kernel"
52
+
53
+ UbuntuRelease = Struct.new(:number, :name)
54
+
55
+ releases = [
56
+ UbuntuRelease.new("9.04", "Jaunty Jackalope"),
57
+ UbuntuRelease.new("10.10", "Maverick Meerkat"),
58
+ UbuntuRelease.new("8.10", "Intrepid Ibex"),
59
+ UbuntuRelease.new("10.04.4", "Lucid Lynx"),
60
+ UbuntuRelease.new("9.10", "Karmic Koala"),
61
+ ]
62
+
63
+ releases.sort_by { |release| NaturalSort(release.number) }
64
+ # => 8.10, 9.04, 9.10, 10.04.4, 10.10
39
65
  ```
40
66
 
67
+ `NaturalSort()` is a global helper — a `Kernel` method in the spirit of
68
+ `Integer()` or `Array()`. It lives in a separate file so that requiring the gem
69
+ (or its refinements) never adds a method to every object unless you explicitly
70
+ ask for it with `require "natural_sort/kernel"`. If you'd rather not add a
71
+ global method, `NaturalSort.key(value)` does the same thing:
72
+
41
73
  ```ruby
42
- list = ["a10", "a", "a20", "a1b", "a1a", "a2", "a0", "a1"]
43
- NaturalSort.sort! list # => ["a", "a0", "a1", "a1a", "a1b", "a2", "a10", "a20"]
44
- list # => ["a", "a0", "a1", "a1a", "a1b", "a2", "a10", "a20"]
74
+ releases.sort_by { |release| NaturalSort.key(release.number) }
45
75
  ```
46
76
 
77
+ **Performance.** `NaturalSort.sort` and `sort_by` with a `NaturalSort.key` build
78
+ one key per element; `&NaturalSort` re-splits both strings on every comparison
79
+ (so roughly `n log n` key builds instead of `n`). For large arrays, prefer the
80
+ key-based forms.
81
+
82
+ Keys are immutable and safe to share across threads, so you can build one once
83
+ and reuse it — e.g. cache keys when sorting the same data repeatedly.
84
+
85
+ ## How it sorts
86
+
87
+ `NaturalSort` is a faithful port of [Martin Pool's natural-order string
88
+ comparison][natsort] — the same algorithm PHP's `strnatcmp` uses. When an
89
+ ordering looks ambiguous, that implementation is the source of truth.
90
+
91
+ Each string is split into runs of digits and runs of non-digits, then compared
92
+ segment by segment:
93
+
94
+ - **Numbers compare numerically** — `"a2"` sorts before `"a10"`, and arbitrarily
95
+ large integers compare exactly (no float rounding or overflow).
96
+ - **Everything else compares by byte value** (case-sensitive ASCII), so every
97
+ uppercase letter sorts before every lowercase one.
98
+ - **A digit run with a leading zero is treated as text**, so fraction- and
99
+ version-like strings order the way you'd expect:
100
+
101
+ ```ruby
102
+ NaturalSort.sort(%w[1.1 1.02 1.002]) # => ["1.002", "1.02", "1.1"]
103
+ ```
104
+ - **Whitespace is skipped** — it never affects ordering on its own, though it
105
+ still separates adjacent digit runs.
106
+
107
+ Comparison is byte-based and not locale-aware: non-ASCII bytes sort by byte
108
+ value (for valid UTF-8, that's the same as codepoint order), and malformed or
109
+ non-ASCII-compatible input — UTF-16, stray bytes — is ordered by byte rather
110
+ than raising.
111
+
112
+ [natsort]: https://github.com/sourcefrog/natsort
113
+
114
+ ## Surprising cases
115
+
116
+ Because this matches `strnatcmp` exactly, it inherits a few results that catch
117
+ people off guard — all consequences of the rules above:
118
+
47
119
  ```ruby
48
- UbuntuRelease = Struct.new(:number, :name)
120
+ # A leading zero makes a number sort like a fraction, so "08" and "09" land
121
+ # BEFORE "1" — not where you'd put the eighth and ninth items.
122
+ NaturalSort.sort(%w[10 08 1 09 2]) # => ["08", "09", "1", "2", "10"]
123
+ NaturalSort.sort(%w[1.5 1.50 1.05]) # => ["1.05", "1.5", "1.50"]
124
+
125
+ # Among themselves, leading-zero numbers compare as text, so "01333" sorts
126
+ # BEFORE "0400" and "0401" — '1' beats '4' even though 1333 > 400.
127
+ NaturalSort.sort(%w[0400 01333 0401]) # => ["01333", "0400", "0401"]
128
+
129
+ # Whitespace is insignificant, so these compare equal...
130
+ NaturalSort.compare("a b", "ab") # => 0
131
+ # ...but it still splits a number in two, so "1 0" is [1, 0], not 10:
132
+ NaturalSort.compare("1 0", "10") # => -1
133
+
134
+ # Case-sensitive byte order: every uppercase letter sorts before every
135
+ # lowercase one (so "Z" sorts before "a").
136
+ NaturalSort.sort(%w[banana Apple apple Banana])
137
+ # => ["Apple", "Banana", "apple", "banana"]
138
+ ```
49
139
 
50
- ubuntu_releases = [
51
- UbuntuRelease.new("9.04", "Jaunty Jackalope"),
52
- UbuntuRelease.new("10.10", "Maverick Meerkat"),
53
- UbuntuRelease.new("8.10", "Intrepid Ibex"),
54
- UbuntuRelease.new("10.04.4", "Lucid Lynx"),
55
- UbuntuRelease.new("9.10", "Karmic Koala"),
56
- ]
140
+ Want case-insensitive ordering? Normalize your keys:
57
141
 
58
- ubuntu_releases.sort_by { |v| NaturalSort(v.number) }
59
- # => [
60
- # UbuntuRelease.new("8.10", "Intrepid Ibex"),
61
- # UbuntuRelease.new("9.04", "Jaunty Jackalope"),
62
- # UbuntuRelease.new("9.10", "Karmic Koala"),
63
- # UbuntuRelease.new("10.04.4", "Lucid Lynx"),
64
- # UbuntuRelease.new("10.10", "Maverick Meerkat")
65
- # ]
142
+ ```ruby
143
+ %w[img10 IMG2 img1].sort_by { |s| NaturalSort.key(s.downcase) }
144
+ # => ["img1", "IMG2", "img10"]
66
145
  ```
67
146
 
68
147
  ## Refinements
69
148
 
70
- If you're running ruby 2.1 or newer, you can use refinements.
149
+ Prefer calling methods directly? Opt into `natural_sort` and `natural_sort_by`
150
+ on `Array`, `Hash`, and `Set`:
71
151
 
72
152
  ```ruby
73
- require "natural_sort/refinments"
153
+ require "natural_sort/refinements"
74
154
 
75
- class MyClass
76
- using NaturalSort
155
+ using NaturalSort
77
156
 
78
- list.natural_sort # => ["a", "a0", "a1", "a1a"...
79
- ubuntu_releases.natural_sort_by(&:number) # => [ UbuntuRelease.new("8.10"...
80
- end
157
+ %w[a1 a10 a2].natural_sort # => ["a1", "a2", "a10"]
158
+ releases.natural_sort_by(&:number) # => sorted by version number
81
159
  ```
82
160
 
161
+ ## Versioning
162
+
163
+ This project follows [Semantic Versioning](https://semver.org). The sort order
164
+ itself is part of the public API: any change to how strings are ordered is a
165
+ breaking change and ships only in a major release.
166
+
83
167
  ## Contributing
84
168
 
85
- Bug reports and pull requests are welcome on GitHub at
169
+ Bug reports and pull requests are welcome at
86
170
  https://github.com/rwz/natural_sort.
87
171
 
88
-
89
172
  ## License
90
173
 
91
- The gem is available as open source under the terms of the [MIT
92
- License](http://opensource.org/licenses/MIT).
174
+ Available as open source under the terms of the
175
+ [MIT License](https://github.com/rwz/natural_sort/blob/main/LICENSE.txt).
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "natural_sort"
4
+
5
+ # Conversion-style helper (in the spirit of Integer()/Array()): wraps +input+
6
+ # in a NaturalSort comparison key for use as a sort_by key, e.g.
7
+ # +list.sort_by { |x| NaturalSort(x) }+. Opt-in — requiring this file defines
8
+ # it at the top level, so it lands on Kernel and is callable on every object.
9
+ #
10
+ # @param input [#to_s]
11
+ # @return [NaturalSort::Key]
12
+ def NaturalSort(input)
13
+ NaturalSort.key(input)
14
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NaturalSort
4
+ # A precomputed, comparable sort key. Wrap a value with NaturalSort.key (or
5
+ # the NaturalSort() helper) and use it as a sort_by key:
6
+ #
7
+ # array.sort_by { |string| NaturalSort.key(string) }
8
+ #
9
+ # The string is split once, on construction: digit runs with no leading zero
10
+ # become Integers (compared by value); everything else — text, and digit runs
11
+ # with a leading zero — stays a String (compared by byte value). Whitespace is
12
+ # skipped. This reproduces Martin Pool's strnatcmp ordering.
13
+ #
14
+ # Splitting runs over the raw bytes, so any input sorts by byte value rather
15
+ # than raising — including malformed encodings (e.g. Latin-1 bytes mislabeled
16
+ # UTF-8) and ASCII-incompatible ones (UTF-16/UTF-32). For valid UTF-8, byte
17
+ # order and codepoint order agree, so this changes ordering for no one.
18
+ class Key
19
+ include Comparable
20
+
21
+ TOKENIZER = /\d+|\D/
22
+ NUMERIC = /\A[1-9]\d*\z/
23
+ WHITESPACE = /\A\s+\z/
24
+ private_constant :TOKENIZER, :NUMERIC, :WHITESPACE
25
+
26
+ # Internal: the token list. Protected so #<=> can read another Key's
27
+ # segments without exposing the (changeable) token format as public API.
28
+ protected attr_reader :segments
29
+
30
+ def initialize(input)
31
+ @input = input.to_s.dup.freeze
32
+ @segments = @input.b.scan(TOKENIZER).filter_map do |token|
33
+ if token.match?(WHITESPACE)
34
+ nil
35
+ elsif NUMERIC.match?(token)
36
+ Integer(token)
37
+ else
38
+ token
39
+ end
40
+ end.freeze
41
+ freeze
42
+ end
43
+
44
+ def to_s
45
+ @input
46
+ end
47
+
48
+ # Three-way comparison. Numeric segments (Integers) compare by value when
49
+ # paired with another numeric segment; in every other pairing both sides
50
+ # compare by byte value. A non-zero-leading integer's #to_s is its original
51
+ # digits, so the cross-type byte comparison stays exact.
52
+ #
53
+ # @return [Integer, nil] -1, 0, or 1, or nil when +other+ is not a Key
54
+ def <=>(other)
55
+ return nil unless other.is_a?(Key)
56
+
57
+ mine = segments
58
+ theirs = other.segments
59
+ index = 0
60
+
61
+ while index < mine.length
62
+ right = theirs[index]
63
+ return 1 if right.nil?
64
+
65
+ left = mine[index]
66
+ result =
67
+ if left.is_a?(Integer)
68
+ right.is_a?(Integer) ? left <=> right : left.to_s <=> right
69
+ else
70
+ right.is_a?(Integer) ? left <=> right.to_s : left <=> right
71
+ end
72
+ return result unless result.zero?
73
+
74
+ index += 1
75
+ end
76
+
77
+ theirs.length > mine.length ? -1 : 0
78
+ end
79
+
80
+ # Equal keys (same token list) hash alike and collapse in a Hash, Set, or
81
+ # #uniq — consistent with #== / #<=>, since segments are equal exactly when
82
+ # the comparison is 0.
83
+ #
84
+ # @return [Boolean]
85
+ def eql?(other)
86
+ other.is_a?(Key) && segments == other.segments
87
+ end
88
+
89
+ # @return [Integer]
90
+ def hash
91
+ segments.hash
92
+ end
93
+ end
94
+ end
@@ -1,16 +1,18 @@
1
- require "set"
1
+ # frozen_string_literal: true
2
+
3
+ require "natural_sort"
2
4
 
3
5
  module NaturalSort
4
6
  [Array, Hash, Set].each do |klass|
5
7
  refine klass do
6
8
  def natural_sort
7
- to_a.sort(&NaturalSort)
9
+ # For a Hash, +to_a+ yields [key, value] pairs; key on the key alone so
10
+ # the value never sways ordering. For Array/Set the element is taken whole.
11
+ to_a.sort_by { |element, _| NaturalSort.key(element) }
8
12
  end
9
13
 
10
14
  def natural_sort_by
11
- to_a.sort_by do |element|
12
- NaturalSort(yield(element))
13
- end
15
+ to_a.sort_by { |element| NaturalSort.key(yield(element)) }
14
16
  end
15
17
  end
16
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NaturalSort
2
- VERSION = "0.3.0".freeze
4
+ VERSION = "1.0.0"
3
5
  end
data/lib/natural_sort.rb CHANGED
@@ -1,28 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "natural_sort/version"
2
4
 
3
5
  module NaturalSort
4
6
  module_function
5
7
 
6
- autoload :Segment, "natural_sort/segment"
7
- autoload :SegmentedString, "natural_sort/segmented_string"
8
+ autoload :Key, "natural_sort/key"
8
9
 
10
+ # Comparator proc, so the module itself works as a sort block:
11
+ # +list.sort(&NaturalSort)+. Prefer +sort+ or +sort_by { NaturalSort.key(x) }+
12
+ # when speed matters — those build one key per element instead of one per
13
+ # comparison.
14
+ #
15
+ # @return [Proc] a two-argument comparator returning -1, 0, or 1
9
16
  def to_proc
10
- lambda(&method(:compare))
17
+ method(:compare).to_proc
11
18
  end
12
19
 
20
+ # Natural-sorts +input+ into a new array. Not stable: elements whose
21
+ # natural-order keys are equal may be reordered relative to each other.
22
+ #
23
+ # @param input [Enumerable] strings (or any +#to_s+-able values)
24
+ # @return [Array] a new array in natural order
13
25
  def sort(input)
14
- input.sort(&self)
26
+ input.sort_by { |element| Key.new(element) }
15
27
  end
16
28
 
29
+ # Natural-sorts +input+ in place. Like {sort}, not stable for equal keys.
30
+ #
31
+ # @param input [Array] (or anything with +#sort_by!+)
32
+ # @return [Array] +input+ itself, sorted
33
+ # @raise [ArgumentError] when +input+ cannot be sorted in place
17
34
  def sort!(input)
18
- input.sort!(&self)
35
+ unless input.respond_to?(:sort_by!)
36
+ raise ArgumentError, "sort! needs an Array (or anything with #sort_by!); use sort for other enumerables"
37
+ end
38
+
39
+ input.sort_by! { |element| Key.new(element) }
19
40
  end
20
41
 
42
+ # Three-way natural-order comparison of two values (each coerced via +#to_s+).
43
+ #
44
+ # @param a [#to_s]
45
+ # @param b [#to_s]
46
+ # @return [Integer] -1, 0, or 1
21
47
  def compare(a, b)
22
- SegmentedString.new(a) <=> SegmentedString.new(b)
48
+ Key.new(a) <=> Key.new(b)
23
49
  end
24
- end
25
50
 
26
- def NaturalSort(input)
27
- NaturalSort::SegmentedString.new(input)
51
+ # The comparable sort key for +value+, for use as a +sort_by+ key.
52
+ #
53
+ # @param value [#to_s]
54
+ # @return [NaturalSort::Key]
55
+ def key(value)
56
+ Key.new(value)
57
+ end
28
58
  end
metadata CHANGED
@@ -1,34 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: natural_sort
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Pravosud
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-27 00:00:00.000000000 Z
11
+ date: 2026-06-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description: |
14
+ Natural-sort ordering for Ruby: split strings into digit and non-digit
15
+ runs and compare numerically, so "a2" sorts before "a10". Plugs into
16
+ Ruby's own sort methods, with optional Array/Hash/Set refinements and an
17
+ opt-in NaturalSort() helper.
14
18
  email:
15
19
  - pavel@pravosud.com
16
20
  executables: []
17
21
  extensions: []
18
22
  extra_rdoc_files: []
19
23
  files:
24
+ - CHANGELOG.md
20
25
  - LICENSE.txt
21
26
  - README.md
22
27
  - lib/natural_sort.rb
28
+ - lib/natural_sort/kernel.rb
29
+ - lib/natural_sort/key.rb
23
30
  - lib/natural_sort/refinements.rb
24
- - lib/natural_sort/segment.rb
25
- - lib/natural_sort/segmented_string.rb
26
31
  - lib/natural_sort/version.rb
27
32
  homepage: https://github.com/rwz/natural_sort
28
33
  licenses:
29
34
  - MIT
30
- metadata: {}
31
- post_install_message:
35
+ metadata:
36
+ homepage_uri: https://github.com/rwz/natural_sort
37
+ source_code_uri: https://github.com/rwz/natural_sort/tree/v1.0.0
38
+ bug_tracker_uri: https://github.com/rwz/natural_sort/issues
39
+ changelog_uri: https://github.com/rwz/natural_sort/blob/main/CHANGELOG.md
40
+ documentation_uri: https://rubydoc.info/gems/natural_sort
41
+ rubygems_mfa_required: 'true'
42
+ post_install_message:
32
43
  rdoc_options: []
33
44
  require_paths:
34
45
  - lib
@@ -36,16 +47,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
47
  requirements:
37
48
  - - ">="
38
49
  - !ruby/object:Gem::Version
39
- version: '0'
50
+ version: '3.3'
40
51
  required_rubygems_version: !ruby/object:Gem::Requirement
41
52
  requirements:
42
53
  - - ">="
43
54
  - !ruby/object:Gem::Version
44
55
  version: '0'
45
56
  requirements: []
46
- rubyforge_project:
47
- rubygems_version: 2.7.7
48
- signing_key:
57
+ rubygems_version: 3.5.22
58
+ signing_key:
49
59
  specification_version: 4
50
60
  summary: Natural sorting support for Ruby
51
61
  test_files: []
@@ -1,38 +0,0 @@
1
- module NaturalSort
2
- class Segment
3
- include Comparable
4
-
5
- # Segments that look like numbers, but start with 0 should be treated as
6
- # strings, so that we don't asumme that 1.020 is more than 1.1 cause 20 > 1
7
- NUMERIC = /\A[1-9]\d*\z/
8
- private_constant :NUMERIC
9
-
10
- attr_reader :input
11
-
12
- def initialize(input)
13
- @input = input.to_s
14
- end
15
-
16
- def to_s
17
- @input
18
- end
19
-
20
- def <=>(other)
21
- if numeric? && other.numeric?
22
- Rational(input) <=> Rational(other.to_s)
23
- else
24
- compare_chars(input, other.to_s)
25
- end
26
- end
27
-
28
- def numeric?
29
- NUMERIC === input
30
- end
31
-
32
- private
33
-
34
- def compare_chars(a, b)
35
- a == b.swapcase ? a <=> b : a.downcase <=> b.downcase
36
- end
37
- end
38
- end
@@ -1,43 +0,0 @@
1
- module NaturalSort
2
- class SegmentedString
3
- include Comparable
4
-
5
- TOKENIZER = /\d+|\D/
6
- private_constant :TOKENIZER
7
-
8
- attr_reader :input
9
-
10
- def initialize(input)
11
- @input = input.to_s
12
- end
13
-
14
- def segments
15
- @segments ||= tokens.map { |token| Segment.new(token) }
16
- end
17
-
18
- def <=>(other)
19
- raise ArgumentError unless SegmentedString === other
20
-
21
- other_segments = other.segments
22
-
23
- segments.each_with_index do |segment, index|
24
- other_segment = other_segments[index]
25
- return 1 if other_segment.nil?
26
- result = compare_segments(segment, other_segment)
27
- return result unless result.zero?
28
- end
29
-
30
- other_segments.length > segments.length ? -1 : 0
31
- end
32
-
33
- private
34
-
35
- def tokens
36
- @tokens ||= input.scan(TOKENIZER)
37
- end
38
-
39
- def compare_segments(segment, other)
40
- segment <=> other
41
- end
42
- end
43
- end