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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 946be21b2839e39c50fccfc55683ec69b8aa1d888a4030cd93aa3ff59f17ff18
4
- data.tar.gz: 9cd544f2130f11a1285eaeeaff6b695ac3ca80513b721783836a4d7526b70178
3
+ metadata.gz: 91d380401f71b040bc67875987d94a474c6075e28e25197acf9eeea15bfa7d24
4
+ data.tar.gz: 1f2c56fcb6258bb2d6762eff8aacebad67c83f405d0fc2bf9b863e8355daad8e
5
5
  SHA512:
6
- metadata.gz: 057e1c3b3b7bcb4b8b79e9a98ec132f1c9d6faa03defb3095ae0d2fa5eaeed7b74e3d39d8bda3ab280d8b594e492b493008d1711349d1a11ddea6c1e95802eee
7
- data.tar.gz: 2d093be68e236968499608dbd183ffbd99e7664cb6d3a929e0f15bbc904a0f86905a187f2f0046902bab0b055c5dd29399d1cf53a684e36f12a2e19e4bbcdf4b
6
+ metadata.gz: a0048ac2c7ff377dc9f3944ff540786c8b352a6475e01c20db38839fe159ba109960e40c2ca6aa92b2d81b26097dc2c627d5270bdefa36f8f6d5858ed01493a1
7
+ data.tar.gz: c11d0add1538cf3457d29b8a9a6d03ad11e8107f0e910af80a82c6699553852abb4e43180370e768dda8e8fbcc60d2518e3c93e4760a55d7f492fe3b1adb43ff
data/CHANGELOG.md CHANGED
@@ -1,4 +1,7 @@
1
- ## [Unreleased]
1
+ ## [0.1.0] - 2022-09-22
2
+
3
+ - Add Put.nils_first, Put.nils_last
4
+ - Add Put.anywhere
2
5
 
3
6
  ## [0.0.2] - 2022-09-21
4
7
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- put (0.0.2)
4
+ put (0.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -48,6 +48,7 @@ GEM
48
48
  PLATFORMS
49
49
  arm64-darwin-21
50
50
  arm64-darwin-22
51
+ x86_64-linux
51
52
 
52
53
  DEPENDENCIES
53
54
  debug
data/README.md CHANGED
@@ -1,11 +1,223 @@
1
- # Put
1
+ # Put - put your things in order 💎
2
2
 
3
- Put helps you put stuff in order when using
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
- A neat trick when implementing complex sorting rules is to map them to an
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
@@ -0,0 +1,13 @@
1
+ module Put
2
+ class PutsThing
3
+ class Anywhere < PutsThing
4
+ def initialize(seed)
5
+ @random = seed.nil? ? Random.new : Random.new(seed)
6
+ end
7
+
8
+ def value
9
+ @random.rand
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Put
2
+ class PutsThing
3
+ class Ascending < InOrder
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Put
2
+ class PutsThing
3
+ class Descending < InOrder
4
+ def reverse?
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Put
2
+ class PutsThing
3
+ class First < PutsThing
4
+ def value
5
+ -Float::INFINITY
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ module Put
2
+ class PutsThing
3
+ class InOrder < PutsThing
4
+ def initialize(value, nils_first:)
5
+ @value = value
6
+ @nils_first = nils_first
7
+ end
8
+
9
+ attr_reader :value
10
+
11
+ def nils_first?
12
+ @nils_first
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ module Put
2
+ class PutsThing
3
+ class Last < PutsThing
4
+ def value
5
+ Float::INFINITY
6
+ end
7
+
8
+ def nils_first?
9
+ true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Put
2
+ class PutsThing
3
+ class NilOrder < PutsThing
4
+ def initialize(value)
5
+ @value = value
6
+ end
7
+
8
+ def value
9
+ if @value.nil?
10
+ nil
11
+ else
12
+ 0
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module Put
2
+ class PutsThing
3
+ class NilsFirst < NilOrder
4
+ def nils_first?
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Put
2
+ class PutsThing
3
+ class NilsLast < NilOrder
4
+ def nils_first?
5
+ false
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Put
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
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.2
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.7
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