put 0.0.2 → 0.1.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: 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