put 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -1
- data/Gemfile.lock +2 -1
- data/README.md +217 -5
- data/lib/put/debug.rb +45 -0
- data/lib/put/puts_thing/anywhere.rb +13 -0
- data/lib/put/puts_thing/ascending.rb +6 -0
- data/lib/put/puts_thing/descending.rb +9 -0
- data/lib/put/puts_thing/first.rb +9 -0
- data/lib/put/puts_thing/in_order.rb +16 -0
- data/lib/put/puts_thing/last.rb +13 -0
- data/lib/put/puts_thing/nil_order.rb +17 -0
- data/lib/put/puts_thing/nils_first.rb +9 -0
- data/lib/put/puts_thing/nils_last.rb +9 -0
- data/lib/put/puts_thing.rb +0 -38
- data/lib/put/version.rb +1 -1
- data/lib/put.rb +26 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91d380401f71b040bc67875987d94a474c6075e28e25197acf9eeea15bfa7d24
|
4
|
+
data.tar.gz: 1f2c56fcb6258bb2d6762eff8aacebad67c83f405d0fc2bf9b863e8355daad8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0048ac2c7ff377dc9f3944ff540786c8b352a6475e01c20db38839fe159ba109960e40c2ca6aa92b2d81b26097dc2c627d5270bdefa36f8f6d5858ed01493a1
|
7
|
+
data.tar.gz: c11d0add1538cf3457d29b8a9a6d03ad11e8107f0e910af80a82c6699553852abb4e43180370e768dda8e8fbcc60d2518e3c93e4760a55d7f492fe3b1adb43ff
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,11 +1,223 @@
|
|
1
|
-
# Put
|
1
|
+
# Put - put your things in order 💎
|
2
2
|
|
3
|
-
Put
|
4
|
-
[Enumerable#sort_by](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-sort_by)
|
3
|
+
Put pairs with
|
4
|
+
[Enumerable#sort_by](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-sort_by)
|
5
|
+
to provide a more expressive, fault-tolerant, and configurable approach to
|
6
|
+
sorting Ruby objects with multiple criteria.
|
5
7
|
|
6
|
-
|
7
|
-
array of arrays of comparable elements in priority-order.
|
8
|
+
# First, put Put in your Gemfile
|
8
9
|
|
10
|
+
You've probably already put a few gems in there, so why not put Put, too:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem "put"
|
14
|
+
```
|
15
|
+
|
16
|
+
Of course after you push Put, your colleagues will wonder why you put Put there.
|
17
|
+
|
18
|
+
## Before you tell me where to put it
|
19
|
+
|
20
|
+
A neat trick when applying complex sorting rules to a collection is to map them
|
21
|
+
to an array of arrays of comparable values in priority order. It's a common
|
22
|
+
approach (and a special subtype of what's called a [Schwartzian
|
23
|
+
transform](https://en.wikipedia.org/wiki/Schwartzian_transform)), but this
|
24
|
+
pattern doesn't have an widely-accepted name yet, so let's use code to explain.
|
25
|
+
|
26
|
+
Suppose you have some people:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
Person = Struct.new(:name, :age, :rubyist?, keyword_init: true)
|
30
|
+
|
31
|
+
people = [
|
32
|
+
Person.new(name: "Tam", age: 22),
|
33
|
+
Person.new(name: "Zak", age: 33),
|
34
|
+
Person.new(name: "Axe", age: 33),
|
35
|
+
Person.new(name: "Qin", age: 18, rubyist?: true),
|
36
|
+
Person.new(name: "Zoe", age: 28, rubyist?: true)
|
37
|
+
]
|
38
|
+
```
|
39
|
+
|
40
|
+
And you want to sort these people in the following priority order:
|
41
|
+
|
42
|
+
1. Put any Rubyists at the _top_ of the list, as is right and good
|
43
|
+
2. If both are (or are not) Rubyists, break the tie by sorting by age descending
|
44
|
+
3. Finally, break any remaining ties by sorting by name ascending
|
45
|
+
|
46
|
+
Here's what the aforementioned pattern to accomplish this usually looks like
|
47
|
+
using
|
48
|
+
[Enumerable#sort_by](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-sort_by):
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
people.sort_by { |person|
|
52
|
+
[
|
53
|
+
person.rubyist? ? 0 : 1,
|
54
|
+
person.age * -1,
|
55
|
+
person.name
|
56
|
+
]
|
57
|
+
} # => Zoe, Qin, Axe, Zak, Tam
|
58
|
+
```
|
59
|
+
|
60
|
+
The above will return everyone in the right order. This has a few drawbacks,
|
61
|
+
though:
|
62
|
+
|
63
|
+
* Unless you're already familiar with this pattern that nobody's bothered to
|
64
|
+
give a name before, this code isn't very expressive. As a result, each line
|
65
|
+
is almost begging for a code comment above it to explain its intent
|
66
|
+
* Ternary operators are confusing, especially with predicate methods like
|
67
|
+
`rubyist?` and especially when returning [magic
|
68
|
+
number](https://en.wikipedia.org/wiki/Magic_number_(programming))'s like `1` and
|
69
|
+
`0`.
|
70
|
+
* Any `nil` values will result in a bad time. If a person's `age` is nil, you'll
|
71
|
+
get "_undefined method `*' for nil:NilClass_" `NoMethodError`
|
72
|
+
* Relatedly, if any two items aren't comparable (e.g. `<=>` returns nil), you'll
|
73
|
+
be greeted with an inscrutable `ArgumentError` that just says "_comparison of
|
74
|
+
Array with Array failed_"
|
75
|
+
|
76
|
+
Here's the same code example if you put Put in there:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
people.sort_by { |person|
|
80
|
+
[
|
81
|
+
(Put.first if person.rubyist?),
|
82
|
+
Put.desc(person.age),
|
83
|
+
Put.asc(person.name)
|
84
|
+
]
|
85
|
+
} # => Zoe, Qin, Axe, Zak, Tam
|
86
|
+
```
|
87
|
+
|
88
|
+
The Put gem solves every one of the above issues:
|
89
|
+
|
90
|
+
* Put's methods have actual names. In fact, let's just call this the "Put
|
91
|
+
pattern" while we're at it
|
92
|
+
* No ternaries necessary
|
93
|
+
* It's quite `nil` friendly
|
94
|
+
* It ships with a `Put.debug` method that helps you introspect those
|
95
|
+
impenetrable `ArgumentError` messages whenever any two values turn out not to
|
96
|
+
be comparable
|
97
|
+
|
98
|
+
After reading this, your teammates are sure be glad they put you in charge of
|
99
|
+
putting little gems like Put in the Gemfile.
|
100
|
+
|
101
|
+
## When you Put it that way
|
102
|
+
|
103
|
+
Put's API is short and sweet. In fact, you've already put up with most of it.
|
104
|
+
|
105
|
+
### Put.first
|
106
|
+
|
107
|
+
When a particular condition indicates an item should go to the top of a list,
|
108
|
+
you'll want to designate a position in your mapped `sort_by` arrays to return
|
109
|
+
either `Put.first` or `nil`, like this:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
[42, 12, 65, 99, 49].sort_by { |n|
|
113
|
+
[(Put.first if n.odd?)]
|
114
|
+
} # => 65, 99, 49, 42, 12
|
115
|
+
```
|
116
|
+
|
117
|
+
### Put.last
|
118
|
+
|
119
|
+
When items that meet a certain condition should go to the bottom of the list,
|
120
|
+
you can do the same sort of conditional expression with `Put.last`:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
%w[Jin drinks Gin on Gym day].sort_by { |s|
|
124
|
+
[(Put.last unless s.match?(/[A-Z]/))]
|
125
|
+
} # => ["Jin", "Gin", "Gym", "drinks", "on", "day"]
|
126
|
+
```
|
127
|
+
|
128
|
+
### Put.asc(value, nils_first: false)
|
129
|
+
|
130
|
+
The `Put.asc` method provides a nil-safe way to sort a value in ascending order:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
%w[The quick brown fox].sort_by { |s|
|
134
|
+
[Put.asc(s)]
|
135
|
+
} # => ["The", "brown", "fox", "quick"]
|
136
|
+
```
|
137
|
+
|
138
|
+
It also supports an optional `nils_first` keyword argument that defaults to
|
139
|
+
false (translation: nils are sorted last by default), which looks like this:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
[3, nil, 1, 5].sort_by { |n|
|
143
|
+
[Put.asc(n, nils_first: true)]
|
144
|
+
} # => [nil, 1, 3, 5]
|
145
|
+
```
|
146
|
+
|
147
|
+
### Put.desc(value, nils_first: false)
|
148
|
+
|
149
|
+
The opposite of `Put.asc` is `Put.desc`, and it works as you might suspect:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
%w[Aardvark Zebra].sort_by { |s|
|
153
|
+
[Put.desc(s)]
|
154
|
+
} # => ["Zebra", "Aardvark"]
|
155
|
+
```
|
156
|
+
|
157
|
+
And also like `Put.asc`, `Put.desc` has an optional `nils_first` keyword
|
158
|
+
argument when you want nils on top:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
[1, nil, 2, 3].sort_by { |n|
|
162
|
+
[Put.desc(n, nils_first: true)]
|
163
|
+
} # => [nil, 3, 2, 1]
|
164
|
+
```
|
165
|
+
|
166
|
+
### Put.anywhere
|
167
|
+
|
168
|
+
You're sorting stuff, so naturally _order matters_. But when building a compound
|
169
|
+
`sort_by` expression, order matters less as you add more and more tiebreaking
|
170
|
+
criteria. In fact, sometimes shuffling items is the more appropriate than
|
171
|
+
leaving things in their original order. Enter `Put.anywhere`, which can be
|
172
|
+
called without any argument at any index in the mapped sorting array:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
[1, 3, 4, 7, 8, 9].sort_by { |n|
|
176
|
+
[
|
177
|
+
(Put.first if n.even?),
|
178
|
+
Put.anywhere
|
179
|
+
]
|
180
|
+
} # => [8, 4, 1, 7, 9, 3]
|
181
|
+
```
|
182
|
+
|
183
|
+
### Put.nils_first(value)
|
184
|
+
|
185
|
+
If you're sorting items and you know some not-comparable `nil` values are going
|
186
|
+
to appear, you can put all the nils on top with `Put.nil_first(value)`. Note
|
187
|
+
that _unlike_ `Put.asc` and `Put.desc`, it won't actually sort the values—it'll
|
188
|
+
just pull all the nils up!
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
[:fun, :stuff, nil, :here].sort_by { |val|
|
192
|
+
[Put.nils_first(val)]
|
193
|
+
} # => [nil, :fun, :stuff, :here]
|
194
|
+
```
|
195
|
+
|
196
|
+
### Put.nils_last(value)
|
197
|
+
|
198
|
+
As you might be able to guess, `Put.nils_last` puts the nils last:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
[:every, nil, :counts].sort_by { |val|
|
202
|
+
[Put.nils_last(val)]
|
203
|
+
} # => [:every, :counts, nil]
|
204
|
+
```
|
205
|
+
|
206
|
+
### Put.debug(sorting_arrays)
|
207
|
+
|
208
|
+
If you see "comparison of Array with Array failed" and you don't have any idea
|
209
|
+
what is going on, try debugging by changing `sort_by` to `map` and passing it
|
210
|
+
to `Put.debug`.
|
211
|
+
|
212
|
+
For an interactive example of how to debug this issue with `Put.debug`, take a
|
213
|
+
look [at this test case](/test/put_test.rb#L53-L98).
|
214
|
+
|
215
|
+
## Put your hands together! 👏
|
216
|
+
|
217
|
+
Many thanks to [Matt Jones](https://github.com/al2o3cr) and [Matthew
|
218
|
+
Draper](https://github.com/matthewd) for answering a bunch of obscure questions
|
219
|
+
about comparisons in Ruby and implementing the initial prototype, respectively.
|
220
|
+
👏👏👏
|
9
221
|
|
10
222
|
## Code of Conduct
|
11
223
|
|
data/lib/put/debug.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Put
|
2
|
+
class Debug
|
3
|
+
Result = Struct.new(:success?, :incomparables, keyword_init: true)
|
4
|
+
Incomparable = Struct.new(
|
5
|
+
:sorting_index, :left, :left_index, :left_value,
|
6
|
+
:right, :right_index, :right_value, keyword_init: true
|
7
|
+
) {
|
8
|
+
def inspect
|
9
|
+
both_puts_things = left.is_a?(PutsThing) && right.is_a?(PutsThing)
|
10
|
+
left_desc = (both_puts_things ? left_value : left).inspect
|
11
|
+
right_desc = (both_puts_things ? right_value : right).inspect
|
12
|
+
"Sorting comparator at index #{sorting_index} failed, because items at indices #{left_index} and #{right_index} were not comparable. Their values were `#{left_desc}' and `#{right_desc}', respectively."
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
def call(sorting_arrays)
|
17
|
+
sorting_arrays.sort
|
18
|
+
Result.new(success?: true, incomparables: [])
|
19
|
+
rescue ArgumentError
|
20
|
+
# TODO this is O(n^lol)
|
21
|
+
incomparables = sorting_arrays.transpose.map.with_index { |comparables, sorting_index|
|
22
|
+
comparables.map.with_index { |comparable, comparable_index|
|
23
|
+
comparables.map.with_index { |other, other_index|
|
24
|
+
next if comparable_index == other_index
|
25
|
+
if (comparable <=> other).nil?
|
26
|
+
Incomparable.new(
|
27
|
+
sorting_index: sorting_index,
|
28
|
+
left: comparable,
|
29
|
+
left_index: comparable_index,
|
30
|
+
left_value: (comparable.value if comparable.is_a?(PutsThing)),
|
31
|
+
right: other,
|
32
|
+
right_index: other_index,
|
33
|
+
right_value: (other.value if other.is_a?(PutsThing))
|
34
|
+
)
|
35
|
+
end
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}.flatten.compact.uniq { |inc|
|
39
|
+
# Remove dupes where two items are incomparable in both <=> directions:
|
40
|
+
[inc.sorting_index] + [inc.left_index, inc.right_index].sort
|
41
|
+
}
|
42
|
+
Result.new(success?: false, incomparables: incomparables)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/put/puts_thing.rb
CHANGED
@@ -23,43 +23,5 @@ module Put
|
|
23
23
|
def nils_first?
|
24
24
|
false
|
25
25
|
end
|
26
|
-
|
27
|
-
class First < PutsThing
|
28
|
-
def value
|
29
|
-
-Float::INFINITY
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
class Last < PutsThing
|
34
|
-
def value
|
35
|
-
Float::INFINITY
|
36
|
-
end
|
37
|
-
|
38
|
-
def nils_first?
|
39
|
-
true
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
class InOrder < PutsThing
|
44
|
-
def initialize(value, nils_first:)
|
45
|
-
@value = value
|
46
|
-
@nils_first = nils_first
|
47
|
-
end
|
48
|
-
|
49
|
-
attr_reader :value
|
50
|
-
|
51
|
-
def nils_first?
|
52
|
-
@nils_first
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
class Ascending < InOrder
|
57
|
-
end
|
58
|
-
|
59
|
-
class Descending < InOrder
|
60
|
-
def reverse?
|
61
|
-
true
|
62
|
-
end
|
63
|
-
end
|
64
26
|
end
|
65
27
|
end
|
data/lib/put/version.rb
CHANGED
data/lib/put.rb
CHANGED
@@ -1,6 +1,16 @@
|
|
1
|
+
require_relative "put/debug"
|
1
2
|
require_relative "put/version"
|
2
3
|
require_relative "put/nil_ext"
|
3
4
|
require_relative "put/puts_thing"
|
5
|
+
require_relative "put/puts_thing/anywhere"
|
6
|
+
require_relative "put/puts_thing/first"
|
7
|
+
require_relative "put/puts_thing/last"
|
8
|
+
require_relative "put/puts_thing/in_order"
|
9
|
+
require_relative "put/puts_thing/ascending"
|
10
|
+
require_relative "put/puts_thing/descending"
|
11
|
+
require_relative "put/puts_thing/nil_order"
|
12
|
+
require_relative "put/puts_thing/nils_first"
|
13
|
+
require_relative "put/puts_thing/nils_last"
|
4
14
|
|
5
15
|
module Put
|
6
16
|
def self.first
|
@@ -18,4 +28,20 @@ module Put
|
|
18
28
|
def self.asc(value, nils_first: false)
|
19
29
|
PutsThing::Ascending.new(value, nils_first: nils_first)
|
20
30
|
end
|
31
|
+
|
32
|
+
def self.nils_first(value)
|
33
|
+
PutsThing::NilsFirst.new(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.nils_last(value)
|
37
|
+
PutsThing::NilsLast.new(value)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.anywhere(seed: nil)
|
41
|
+
PutsThing::Anywhere.new(seed)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.debug(sorting_arrays)
|
45
|
+
Debug.new.call(sorting_arrays)
|
46
|
+
end
|
21
47
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: put
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Searls
|
@@ -25,8 +25,18 @@ files:
|
|
25
25
|
- README.md
|
26
26
|
- Rakefile
|
27
27
|
- lib/put.rb
|
28
|
+
- lib/put/debug.rb
|
28
29
|
- lib/put/nil_ext.rb
|
29
30
|
- lib/put/puts_thing.rb
|
31
|
+
- lib/put/puts_thing/anywhere.rb
|
32
|
+
- lib/put/puts_thing/ascending.rb
|
33
|
+
- lib/put/puts_thing/descending.rb
|
34
|
+
- lib/put/puts_thing/first.rb
|
35
|
+
- lib/put/puts_thing/in_order.rb
|
36
|
+
- lib/put/puts_thing/last.rb
|
37
|
+
- lib/put/puts_thing/nil_order.rb
|
38
|
+
- lib/put/puts_thing/nils_first.rb
|
39
|
+
- lib/put/puts_thing/nils_last.rb
|
30
40
|
- lib/put/version.rb
|
31
41
|
- put.gemspec
|
32
42
|
- sig/put.rbs
|
@@ -52,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
62
|
- !ruby/object:Gem::Version
|
53
63
|
version: '0'
|
54
64
|
requirements: []
|
55
|
-
rubygems_version: 3.3.
|
65
|
+
rubygems_version: 3.3.20
|
56
66
|
signing_key:
|
57
67
|
specification_version: 4
|
58
68
|
summary: Put helps you write prioritized, multi-variate sort_by blocks
|