natural_sort 0.2.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 +5 -5
- data/CHANGELOG.md +46 -0
- data/LICENSE.txt +1 -1
- data/README.md +129 -46
- data/lib/natural_sort/kernel.rb +14 -0
- data/lib/natural_sort/key.rb +94 -0
- data/lib/natural_sort/refinements.rb +7 -5
- data/lib/natural_sort/version.rb +3 -1
- data/lib/natural_sort.rb +39 -9
- metadata +22 -12
- data/lib/natural_sort/segment.rb +0 -36
- data/lib/natural_sort/segmented_string.rb +0 -43
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 354422d90d16ba088c221c5d69e708a0abe72c3fcc693016144b7b36237cd106
|
|
4
|
+
data.tar.gz: c05143277661784e8de74e8acf05100eeaee1bc53566bc0ff4b61e8e8ef4bcc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/README.md
CHANGED
|
@@ -1,92 +1,175 @@
|
|
|
1
1
|
# Natural Sort
|
|
2
2
|
|
|
3
|
-
[][codeclimate]
|
|
3
|
+
[][ci]
|
|
4
|
+
[][gem]
|
|
6
5
|
|
|
7
|
-
[
|
|
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
|
-
|
|
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
|
|
19
|
+
Add it to your Gemfile:
|
|
16
20
|
|
|
17
21
|
```ruby
|
|
18
22
|
gem "natural_sort"
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
…then `bundle install`. Or grab it directly:
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
```
|
|
28
|
+
$ gem install natural_sort
|
|
29
|
+
```
|
|
26
30
|
|
|
27
|
-
|
|
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 = [
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
#
|
|
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
|
-
|
|
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/
|
|
153
|
+
require "natural_sort/refinements"
|
|
74
154
|
|
|
75
|
-
|
|
76
|
-
using NatualSort
|
|
155
|
+
using NaturalSort
|
|
77
156
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
92
|
-
License](
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/natural_sort/version.rb
CHANGED
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 :
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
48
|
+
Key.new(a) <=> Key.new(b)
|
|
23
49
|
end
|
|
24
|
-
end
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
47
|
-
|
|
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: []
|
data/lib/natural_sort/segment.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
module NaturalSort
|
|
2
|
-
class Segment
|
|
3
|
-
include Comparable
|
|
4
|
-
|
|
5
|
-
NUMERIC = /\A\d+(?:\.\d+)?\z/
|
|
6
|
-
private_constant :NUMERIC
|
|
7
|
-
|
|
8
|
-
attr_reader :input
|
|
9
|
-
|
|
10
|
-
def initialize(input)
|
|
11
|
-
@input = input.to_s
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def to_s
|
|
15
|
-
@input
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def <=>(other)
|
|
19
|
-
if numeric? && other.numeric?
|
|
20
|
-
Rational(input) <=> Rational(other.to_s)
|
|
21
|
-
else
|
|
22
|
-
compare_chars(input, other.to_s)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def numeric?
|
|
27
|
-
NUMERIC === input
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
def compare_chars(a, b)
|
|
33
|
-
a == b.swapcase ? a <=> b : a.downcase <=> b.downcase
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
module NaturalSort
|
|
2
|
-
class SegmentedString
|
|
3
|
-
include Comparable
|
|
4
|
-
|
|
5
|
-
TOKENIZER = /\d+(?:\.\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
|