array-sort 0.1.1 → 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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -0
  3. data/README.md +228 -29
  4. data/lib/array/sort.rb +5 -8
  5. data/lib/array_sort/algorithms/binary_insertion_sort.rb +41 -0
  6. data/lib/array_sort/algorithms/bubble_sort.rb +31 -0
  7. data/lib/array_sort/algorithms/bucket_sort.rb +67 -0
  8. data/lib/array_sort/algorithms/cocktail_shaker_sort.rb +42 -0
  9. data/lib/array_sort/algorithms/comb_sort.rb +33 -0
  10. data/lib/array_sort/algorithms/counting_sort.rb +50 -0
  11. data/lib/array_sort/algorithms/cycle_sort.rb +46 -0
  12. data/lib/array_sort/algorithms/gnome_sort.rb +28 -0
  13. data/lib/array_sort/algorithms/heap_sort.rb +43 -0
  14. data/lib/array_sort/algorithms/insertion_sort.rb +35 -0
  15. data/lib/array_sort/algorithms/intro_sort.rb +80 -0
  16. data/lib/array_sort/algorithms/merge_sort.rb +57 -0
  17. data/lib/array_sort/algorithms/odd_even_sort.rb +34 -0
  18. data/lib/array_sort/algorithms/pancake_sort.rb +39 -0
  19. data/lib/array_sort/algorithms/quick_sort.rb +68 -0
  20. data/lib/array_sort/algorithms/radix_sort.rb +58 -0
  21. data/lib/array_sort/algorithms/selection_sort.rb +25 -0
  22. data/lib/array_sort/algorithms/shell_sort.rb +41 -0
  23. data/lib/array_sort/algorithms/smooth_sort.rb +106 -0
  24. data/lib/array_sort/algorithms/tim_sort.rb +335 -0
  25. data/lib/array_sort/algorithms/tournament_sort.rb +53 -0
  26. data/lib/array_sort/algorithms/tree_sort.rb +96 -0
  27. data/lib/array_sort/array_methods.rb +67 -0
  28. data/lib/array_sort/core.rb +124 -0
  29. data/lib/array_sort/functions.rb +23 -0
  30. data/lib/array_sort/refinements.rb +16 -0
  31. data/lib/array_sort/registry.rb +68 -0
  32. data/lib/array_sort/trace.rb +111 -0
  33. data/lib/{array/sort → array_sort}/version.rb +1 -1
  34. data/lib/array_sort.rb +43 -0
  35. data/sig/array_sort.rbs +368 -0
  36. metadata +47 -71
  37. data/.gitignore +0 -9
  38. data/.rubocop.yml +0 -11
  39. data/.travis.yml +0 -6
  40. data/Gemfile +0 -6
  41. data/Gemfile.lock +0 -22
  42. data/Rakefile +0 -10
  43. data/array-sort.gemspec +0 -37
  44. data/bin/console +0 -14
  45. data/bin/setup +0 -8
  46. data/lib/array/sort/bubble_sort.rb +0 -91
  47. data/lib/array/sort/heap_sort.rb +0 -111
  48. data/lib/array/sort/helper.rb +0 -34
  49. data/lib/array/sort/insertion_sort.rb +0 -78
  50. data/lib/array/sort/merge_sort.rb +0 -94
  51. data/lib/array/sort/quick_sort.rb +0 -73
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Timsort: finds the runs that are already in order in the data, extends short runs with binary insertion sort, and
5
+ # merges runs in a carefully balanced order. This is the algorithm behind Python's sorted() and Java's
6
+ # Arrays.sort for objects.
7
+ #
8
+ # Real-world data is often partly sorted, and Timsort takes advantage of it: sorted or reverse-sorted input takes a
9
+ # single O(n) pass. Merges "gallop" (search exponentially) when one run keeps winning, so merging runs that barely
10
+ # overlap is cheap too.
11
+ #
12
+ # This follows the structure of the reference implementation, including the corrected merge invariants
13
+ # (de Gouw et al., 2015), but always merges with a temporary copy of the left run rather than choosing the smaller
14
+ # run, which keeps it simpler at the cost of some extra memory.
15
+ #
16
+ # @api private
17
+ module TimSort
18
+ # Arrays shorter than this are sorted with binary insertion sort alone.
19
+ MIN_MERGE = 32
20
+ # How many consecutive wins by one run switch a merge into galloping mode.
21
+ MIN_GALLOP = 7
22
+
23
+ module_function
24
+
25
+ def call(array, compare)
26
+ Sorter.new(array, compare).sort if array.length > 1
27
+ array
28
+ end
29
+
30
+ # Holds the state of one sort: the array, the stack of pending runs and the adaptive galloping threshold.
31
+ class Sorter
32
+ def initialize(array, compare)
33
+ @array = array
34
+ @compare = compare
35
+ @runs = [] # [start, length] pairs, waiting to be merged
36
+ @min_gallop = MIN_GALLOP
37
+ end
38
+
39
+ def sort
40
+ length = @array.length
41
+ min_run = min_run_length(length)
42
+ low = 0
43
+ while low < length
44
+ run_length = count_run_and_make_ascending(low, length)
45
+ if run_length < min_run
46
+ forced = [length - low, min_run].min
47
+ binary_insertion_sort(low, low + forced, low + run_length)
48
+ run_length = forced
49
+ end
50
+ @runs << [low, run_length]
51
+ merge_collapse
52
+ low += run_length
53
+ end
54
+ merge_force_collapse
55
+ end
56
+
57
+ private
58
+
59
+ def less?(a, b) = @compare.call(a, b).negative?
60
+
61
+ # A run length in (MIN_MERGE / 2)..MIN_MERGE such that length / min_run is a power of two or just below one,
62
+ # which keeps the final merges balanced.
63
+ def min_run_length(length)
64
+ remainder = 0
65
+ while length >= MIN_MERGE
66
+ remainder |= length & 1
67
+ length >>= 1
68
+ end
69
+ length + remainder
70
+ end
71
+
72
+ # Returns the length of the run starting at +low+, reversing it first if it is strictly descending. (Only
73
+ # strictly descending runs are reversed, so equal elements never swap places.)
74
+ def count_run_and_make_ascending(low, high)
75
+ run_high = low + 1
76
+ return 1 if run_high == high
77
+
78
+ if less?(@array[run_high], @array[low])
79
+ run_high += 1 while run_high < high && less?(@array[run_high], @array[run_high - 1])
80
+ reverse_range(low, run_high - 1)
81
+ else
82
+ run_high += 1 while run_high < high && !less?(@array[run_high], @array[run_high - 1])
83
+ end
84
+ run_high - low
85
+ end
86
+
87
+ def reverse_range(low, high)
88
+ while low < high
89
+ @array[low], @array[high] = @array[high], @array[low]
90
+ low += 1
91
+ high -= 1
92
+ end
93
+ end
94
+
95
+ # Sorts +@array[low...high]+, given that +@array[low...start]+ is already sorted.
96
+ def binary_insertion_sort(low, high, start)
97
+ (start...high).each do |i|
98
+ element = @array[i]
99
+ position = BinaryInsertionSort.upper_bound(@array, @compare, element, low, i)
100
+ next if position == i
101
+
102
+ i.downto(position + 1) { |j| @array[j] = @array[j - 1] }
103
+ @array[position] = element
104
+ end
105
+ end
106
+
107
+ # Merges runs at the top of the stack until these hold for the top three run lengths z, y, x:
108
+ # z > y + x and y > x. That keeps the stack O(log n) deep and the merges balanced.
109
+ def merge_collapse
110
+ while @runs.size > 1
111
+ n = @runs.size - 2
112
+ if (n.positive? && run_length(n - 1) <= run_length(n) + run_length(n + 1)) ||
113
+ (n > 1 && run_length(n - 2) <= run_length(n - 1) + run_length(n))
114
+ n -= 1 if run_length(n - 1) < run_length(n + 1)
115
+ elsif run_length(n) > run_length(n + 1)
116
+ break
117
+ end
118
+ merge_at(n)
119
+ end
120
+ end
121
+
122
+ def merge_force_collapse
123
+ while @runs.size > 1
124
+ n = @runs.size - 2
125
+ n -= 1 if n.positive? && run_length(n - 1) < run_length(n + 1)
126
+ merge_at(n)
127
+ end
128
+ end
129
+
130
+ def run_length(index) = @runs[index][1]
131
+
132
+ # Merges the runs at stack positions +index+ and +index + 1+.
133
+ def merge_at(index)
134
+ base1, length1 = @runs[index]
135
+ base2, length2 = @runs[index + 1]
136
+ @runs[index] = [base1, length1 + length2]
137
+ @runs.delete_at(index + 1)
138
+
139
+ # Elements of run 1 that are no larger than run 2's first element are already in place.
140
+ skipped = gallop_right(@array[base2], @array, base1, length1, 0)
141
+ base1 += skipped
142
+ length1 -= skipped
143
+ return if length1.zero?
144
+
145
+ # Likewise, elements of run 2 that are no smaller than run 1's last element are already in place.
146
+ length2 = gallop_left(@array[base1 + length1 - 1], @array, base2, length2, length2 - 1)
147
+ return if length2.zero?
148
+
149
+ merge_low(base1, length1, base2, length2)
150
+ end
151
+
152
+ # Merges two adjacent runs by copying the first into a temporary array and merging back from the front.
153
+ def merge_low(base1, length1, base2, length2)
154
+ temp = @array[base1, length1]
155
+ cursor1 = 0 # into temp
156
+ cursor2 = base2 # into @array
157
+ dest = base1
158
+ min_gallop = @min_gallop
159
+
160
+ catch(:done) do
161
+ @array[dest] = @array[cursor2]
162
+ dest += 1
163
+ cursor2 += 1
164
+ length2 -= 1
165
+ throw :done if length2.zero? || length1 == 1
166
+
167
+ loop do
168
+ count1 = 0 # consecutive wins by run 1
169
+ count2 = 0 # consecutive wins by run 2
170
+
171
+ # One element at a time, until one run wins MIN_GALLOP times in a row.
172
+ loop do
173
+ if less?(@array[cursor2], temp[cursor1])
174
+ @array[dest] = @array[cursor2]
175
+ dest += 1
176
+ cursor2 += 1
177
+ count2 += 1
178
+ count1 = 0
179
+ length2 -= 1
180
+ throw :done if length2.zero?
181
+ else
182
+ @array[dest] = temp[cursor1]
183
+ dest += 1
184
+ cursor1 += 1
185
+ count1 += 1
186
+ count2 = 0
187
+ length1 -= 1
188
+ throw :done if length1 == 1
189
+ end
190
+ break if count1 >= min_gallop || count2 >= min_gallop
191
+ end
192
+
193
+ # Galloping: find how far each run's winning streak goes with an exponential search, and copy it in bulk.
194
+ loop do
195
+ count1 = gallop_right(@array[cursor2], temp, cursor1, length1, 0)
196
+ count1.times do
197
+ @array[dest] = temp[cursor1]
198
+ dest += 1
199
+ cursor1 += 1
200
+ end
201
+ length1 -= count1
202
+ throw :done if length1 <= 1
203
+
204
+ @array[dest] = @array[cursor2]
205
+ dest += 1
206
+ cursor2 += 1
207
+ length2 -= 1
208
+ throw :done if length2.zero?
209
+
210
+ count2 = gallop_left(temp[cursor1], @array, cursor2, length2, 0)
211
+ count2.times do
212
+ @array[dest] = @array[cursor2]
213
+ dest += 1
214
+ cursor2 += 1
215
+ end
216
+ length2 -= count2
217
+ throw :done if length2.zero?
218
+
219
+ @array[dest] = temp[cursor1]
220
+ dest += 1
221
+ cursor1 += 1
222
+ length1 -= 1
223
+ throw :done if length1 == 1
224
+
225
+ min_gallop -= 1
226
+ break if count1 < MIN_GALLOP && count2 < MIN_GALLOP
227
+ end
228
+ # Leaving galloping mode makes it a little harder to re-enter.
229
+ min_gallop = [min_gallop, 0].max + 2
230
+ end
231
+ end
232
+ @min_gallop = [min_gallop, 1].max
233
+
234
+ if length1 == 1
235
+ # Run 1's last element is larger than everything left in run 2, which slides down to make room for it.
236
+ length2.times do
237
+ @array[dest] = @array[cursor2]
238
+ dest += 1
239
+ cursor2 += 1
240
+ end
241
+ @array[dest] = temp[cursor1]
242
+ else
243
+ # Run 2 is exhausted, so the rest of run 1 fills the gap. (Nothing is left at all only if the comparison is
244
+ # inconsistent, in which case every element is still in the array.)
245
+ length1.times do
246
+ @array[dest] = temp[cursor1]
247
+ dest += 1
248
+ cursor1 += 1
249
+ end
250
+ end
251
+ end
252
+
253
+ # Returns the leftmost position in +array[base, length]+ where +key+ could be inserted keeping it sorted, i.e. the
254
+ # number of elements less than +key+. The search starts at +base + hint+ and gallops outwards from there.
255
+ def gallop_left(key, array, base, length, hint)
256
+ last_offset = 0
257
+ offset = 1
258
+ if less?(array[base + hint], key)
259
+ # Gallop right until array[base + hint + last_offset] < key <= array[base + hint + offset].
260
+ max_offset = length - hint
261
+ while offset < max_offset && less?(array[base + hint + offset], key)
262
+ last_offset = offset
263
+ offset = (offset * 2) + 1
264
+ end
265
+ offset = [offset, max_offset].min
266
+ last_offset += hint
267
+ offset += hint
268
+ else
269
+ # Gallop left until array[base + hint - offset] < key <= array[base + hint - last_offset].
270
+ max_offset = hint + 1
271
+ while offset < max_offset && !less?(array[base + hint - offset], key)
272
+ last_offset = offset
273
+ offset = (offset * 2) + 1
274
+ end
275
+ offset = [offset, max_offset].min
276
+ last_offset, offset = hint - offset, hint - last_offset
277
+ end
278
+
279
+ # Binary search: array[base + last_offset] < key <= array[base + offset].
280
+ last_offset += 1
281
+ while last_offset < offset
282
+ mid = last_offset + ((offset - last_offset) / 2)
283
+ if less?(array[base + mid], key)
284
+ last_offset = mid + 1
285
+ else
286
+ offset = mid
287
+ end
288
+ end
289
+ offset
290
+ end
291
+
292
+ # Like #gallop_left, but returns the rightmost position, i.e. the number of elements less than or equal to
293
+ # +key+. Placing a run-2 element after equal run-1 elements is what keeps merges stable.
294
+ def gallop_right(key, array, base, length, hint)
295
+ last_offset = 0
296
+ offset = 1
297
+ if less?(key, array[base + hint])
298
+ # Gallop left until array[base + hint - offset] <= key < array[base + hint - last_offset].
299
+ max_offset = hint + 1
300
+ while offset < max_offset && less?(key, array[base + hint - offset])
301
+ last_offset = offset
302
+ offset = (offset * 2) + 1
303
+ end
304
+ offset = [offset, max_offset].min
305
+ last_offset, offset = hint - offset, hint - last_offset
306
+ else
307
+ # Gallop right until array[base + hint + last_offset] <= key < array[base + hint + offset].
308
+ max_offset = length - hint
309
+ while offset < max_offset && !less?(key, array[base + hint + offset])
310
+ last_offset = offset
311
+ offset = (offset * 2) + 1
312
+ end
313
+ offset = [offset, max_offset].min
314
+ last_offset += hint
315
+ offset += hint
316
+ end
317
+
318
+ # Binary search: array[base + last_offset] <= key < array[base + offset].
319
+ last_offset += 1
320
+ while last_offset < offset
321
+ mid = last_offset + ((offset - last_offset) / 2)
322
+ if less?(key, array[base + mid])
323
+ offset = mid
324
+ else
325
+ last_offset = mid + 1
326
+ end
327
+ end
328
+ offset
329
+ end
330
+ end
331
+ end
332
+
333
+ register :tim, TimSort, title: 'Timsort', stable: true,
334
+ best: 'n', average: 'n log n', worst: 'n log n', space: 'n'
335
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Tournament sort: runs a knockout tournament between the elements, where each match is won by the smaller one. The
5
+ # champion is the smallest element; it is removed and only the matches it played are replayed to find the next.
6
+ #
7
+ # Each replay takes log n comparisons. Ties are won by the element that came first, which makes the sort stable.
8
+ #
9
+ # @api private
10
+ module TournamentSort
11
+ module_function
12
+
13
+ def call(array, compare)
14
+ count = array.length
15
+ return array if count < 2
16
+
17
+ elements = Array.new(array)
18
+ leaves = 1
19
+ leaves <<= 1 while leaves < count
20
+
21
+ # A complete binary tree stored in an array: node i has children 2i and 2i + 1, leaves are leaves...2 * leaves,
22
+ # and every node holds the index of the element that won there (nil once that element has been taken).
23
+ tree = Array.new(2 * leaves)
24
+ count.times { |i| tree[leaves + i] = i }
25
+ (leaves - 1).downto(1) { |node| tree[node] = winner(elements, compare, tree[2 * node], tree[(2 * node) + 1]) }
26
+
27
+ count.times do |position|
28
+ champion = tree[1]
29
+ array[position] = elements[champion]
30
+
31
+ node = leaves + champion
32
+ tree[node] = nil
33
+ while node > 1
34
+ node /= 2
35
+ tree[node] = winner(elements, compare, tree[2 * node], tree[(2 * node) + 1])
36
+ end
37
+ end
38
+ array
39
+ end
40
+
41
+ # Returns whichever of the element indices +left+ and +right+ wins (is smaller). +left+ always comes from earlier
42
+ # in the array, so it wins ties.
43
+ def winner(elements, compare, left, right)
44
+ return right if left.nil?
45
+ return left if right.nil?
46
+
47
+ compare.call(elements[right], elements[left]).negative? ? right : left
48
+ end
49
+ end
50
+
51
+ register :tournament, TournamentSort, title: 'Tournament sort', stable: true,
52
+ best: 'n log n', average: 'n log n', worst: 'n log n', space: 'n'
53
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Tree sort: inserts every element into a binary search tree, then reads them back in order.
5
+ #
6
+ # The tree is an AVL tree, which rebalances itself on every insertion so that sorted input doesn't degrade it into a
7
+ # linked list (the classic O(n²) trap of tree sort). Equal elements are inserted to the right of each other, so an
8
+ # in-order walk returns them in their original order and the sort is stable.
9
+ #
10
+ # @api private
11
+ module TreeSort
12
+ Node = Struct.new(:value, :left, :right, :height)
13
+ private_constant :Node
14
+
15
+ module_function
16
+
17
+ def call(array, compare)
18
+ root = nil
19
+ array.each { |element| root = insert(root, element, compare) }
20
+
21
+ index = 0
22
+ each_in_order(root) do |value|
23
+ array[index] = value
24
+ index += 1
25
+ end
26
+ array
27
+ end
28
+
29
+ def insert(node, value, compare)
30
+ return Node.new(value, nil, nil, 1) if node.nil?
31
+
32
+ if compare.call(value, node.value).negative?
33
+ node.left = insert(node.left, value, compare)
34
+ else
35
+ node.right = insert(node.right, value, compare)
36
+ end
37
+ rebalance(node)
38
+ end
39
+
40
+ # Walks the tree in order without recursion.
41
+ def each_in_order(node)
42
+ stack = []
43
+ while node || !stack.empty?
44
+ while node
45
+ stack.push(node)
46
+ node = node.left
47
+ end
48
+ node = stack.pop
49
+ yield node.value
50
+ node = node.right
51
+ end
52
+ end
53
+
54
+ def height(node) = node ? node.height : 0
55
+
56
+ def balance(node) = height(node.left) - height(node.right)
57
+
58
+ def update_height(node)
59
+ node.height = [height(node.left), height(node.right)].max + 1
60
+ node
61
+ end
62
+
63
+ # Restores the AVL property (subtree heights differ by at most one) at +node+ and returns the subtree's new root.
64
+ # Rotations never change the in-order sequence, so they preserve both sorting and stability.
65
+ def rebalance(node)
66
+ update_height(node)
67
+ case balance(node)
68
+ when 2
69
+ node.left = rotate_left(node.left) if balance(node.left).negative?
70
+ rotate_right(node)
71
+ when -2
72
+ node.right = rotate_right(node.right) if balance(node.right).positive?
73
+ rotate_left(node)
74
+ else
75
+ node
76
+ end
77
+ end
78
+
79
+ def rotate_right(node)
80
+ pivot = node.left
81
+ node.left = pivot.right
82
+ pivot.right = update_height(node)
83
+ update_height(pivot)
84
+ end
85
+
86
+ def rotate_left(node)
87
+ pivot = node.right
88
+ node.right = pivot.left
89
+ pivot.left = update_height(node)
90
+ update_height(pivot)
91
+ end
92
+ end
93
+
94
+ register :tree, TreeSort, title: 'Tree sort (AVL)', stable: true,
95
+ best: 'n log n', average: 'n log n', worst: 'n log n', space: 'n'
96
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # The methods ArraySort adds to arrays. +require 'array/sort'+ includes this module into +Array+; +using
5
+ # ArraySort::Refinements+ adds the same methods to +Array+ in one file only.
6
+ #
7
+ # For every algorithm name +x+ in {ArraySort.algorithms} there are four methods with the same contract as their
8
+ # native counterparts:
9
+ #
10
+ # [+x_sort(&block)+] Returns a new sorted array. Comparison sorts accept an optional comparison block.
11
+ # [+x_sort!(&block)+] Sorts +self+ in place and returns it.
12
+ # [+x_sort_by(&block)+] Returns a new array sorted by the keys the block returns, calling the block once per
13
+ # element. Returns an Enumerator without a block.
14
+ # [+x_sort_by!(&block)+] Sorts +self+ in place by the keys the block returns. Returns an Enumerator without a block.
15
+ #
16
+ # Distribution sorts (counting, radix and bucket sort) don't compare elements, so +x_sort+ and +x_sort!+ raise
17
+ # ArgumentError when given a block, and every element (or key, for the +_by+ methods) must be an Integer (or for
18
+ # bucket sort, any finite real number).
19
+ #
20
+ # See {ArraySort.algorithm} for each algorithm's stability and complexity.
21
+ module ArrayMethods
22
+ # Swap two elements in the array, and returns +self+.
23
+ #
24
+ # Negative indices count from the end of the array, as with +Array#[]+.
25
+ #
26
+ # @param index1 [Integer] The index of the first element.
27
+ # @param index2 [Integer] The index of the second element.
28
+ # @return [Array] +self+
29
+ # @raise [ArgumentError] If either of the two parameters is not an Integer.
30
+ # @raise [IndexError] If either index is outside the bounds of the array.
31
+ def swap(index1, index2)
32
+ raise ArgumentError, 'Index must be an integer.' unless index1.is_a?(Integer) && index2.is_a?(Integer)
33
+
34
+ fetch(index1)
35
+ fetch(index2)
36
+ self[index1], self[index2] = self[index2], self[index1] if index1 != index2
37
+ self
38
+ end
39
+
40
+ # The methods are compiled from source rather than created with define_method so that Refinements can import
41
+ # them (Refinement#import_methods only accepts methods written in Ruby source). For +name = :merge+ this defines
42
+ # merge_sort, merge_sort!, merge_sort_by and merge_sort_by!.
43
+ ArraySort.algorithms.each do |name|
44
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
45
+ def #{name}_sort(&)
46
+ ArraySort::Core.sort(self, :#{name}, &)
47
+ end
48
+
49
+ def #{name}_sort!(&)
50
+ ArraySort::Core.sort!(self, :#{name}, &)
51
+ end
52
+
53
+ def #{name}_sort_by(&)
54
+ return ArraySort::Core.sort_by_enumerator(self, :#{name}, bang: false) unless block_given?
55
+
56
+ ArraySort::Core.sort_by(self, :#{name}, &)
57
+ end
58
+
59
+ def #{name}_sort_by!(&)
60
+ return ArraySort::Core.sort_by_enumerator(self, :#{name}, bang: true) unless block_given?
61
+
62
+ ArraySort::Core.sort_by!(self, :#{name}, &)
63
+ end
64
+ RUBY
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Shared plumbing behind every public sort method.
5
+ #
6
+ # Algorithms come in two kinds (see {Algorithm#kind}):
7
+ #
8
+ # * Comparison algorithms respond to +call(buffer, compare)+ and sort +buffer+ in place, where +compare+ is a lambda
9
+ # returning a negative, zero or positive Integer.
10
+ # * Distribution algorithms respond to +call(buffer, key)+ and sort +buffer+ in place by the numeric key that the
11
+ # lambda +key+ returns for each element.
12
+ #
13
+ # Algorithms only ever change +buffer+ through single-element assignment (+buffer[i] = value+), which is what lets
14
+ # {ArraySort.trace} record every write. Everything else (copying, frozen checks, key caching for the +_by+ variants,
15
+ # enumerators) lives here so the algorithms stay focused on the algorithm.
16
+ #
17
+ # @api private
18
+ module Core
19
+ module_function
20
+
21
+ IDENTITY = ->(element) { element }
22
+ FIRST = ->(pair) { pair[0] }
23
+ private_constant :IDENTITY, :FIRST
24
+
25
+ # Returns a new, sorted copy of +array+.
26
+ def sort(array, name, &block)
27
+ algorithm = ArraySort.algorithm(name)
28
+ buffer = Array.new(array)
29
+ if algorithm.comparison?
30
+ algorithm.implementation.call(buffer, comparator(block))
31
+ else
32
+ if block
33
+ raise ArgumentError,
34
+ "#{name}_sort sorts by numeric key and does not take a comparison block; use #{name}_sort_by instead"
35
+ end
36
+
37
+ algorithm.implementation.call(buffer, IDENTITY)
38
+ end
39
+ buffer
40
+ end
41
+
42
+ # Sorts +array+ in place and returns it.
43
+ #
44
+ # The work happens on a private copy that replaces the contents of +array+ only once sorting has finished, so if
45
+ # the comparison raises, +array+ is left exactly as it was (matching +Array#sort!+).
46
+ def sort!(array, name, &)
47
+ ensure_mutable!(array)
48
+ array.replace(sort(array, name, &))
49
+ end
50
+
51
+ # Returns a new copy of +array+ sorted by the keys the block produces.
52
+ #
53
+ # Like +Array#sort_by+, the block is called exactly once per element.
54
+ def sort_by(array, name, &block)
55
+ algorithm = ArraySort.algorithm(name)
56
+ keyed = array.map { |element| [block.call(element), element] }
57
+ by_key = algorithm.comparison? ? ->(a, b) { normalize(a[0] <=> b[0], a[0], b[0]) } : FIRST
58
+ algorithm.implementation.call(keyed, by_key)
59
+ keyed.map!(&:last)
60
+ end
61
+
62
+ # Sorts +array+ in place by the keys the block produces and returns it.
63
+ def sort_by!(array, name, &)
64
+ ensure_mutable!(array)
65
+ array.replace(sort_by(array, name, &))
66
+ end
67
+
68
+ # Returns the Enumerator that +_sort_by+ and +_sort_by!+ return when called without a block.
69
+ #
70
+ # This is built by hand rather than with +to_enum+ so that it also works under {ArraySort::Refinements}: an
71
+ # Enumerator made by +to_enum+ calls the method from outside the refined scope, where it doesn't exist.
72
+ def sort_by_enumerator(array, name, bang:)
73
+ method = bang ? :sort_by! : :sort_by
74
+ Enumerator.new(-> { array.size }) do |yielder|
75
+ public_send(method, array, name) { |element| yielder.yield(element) }
76
+ end
77
+ end
78
+
79
+ # Wraps a comparison (the user's block, or +<=>+) so that it always returns an Integer, raising the same
80
+ # +ArgumentError+ as +Array#sort+ when two elements cannot be compared.
81
+ def comparator(block)
82
+ if block
83
+ ->(a, b) { normalize(block.call(a, b), a, b) }
84
+ else
85
+ ->(a, b) { normalize(a <=> b, a, b) }
86
+ end
87
+ end
88
+
89
+ def normalize(result, element1, element2)
90
+ return result if result.is_a?(Integer)
91
+
92
+ sign = result <=> 0 unless result.nil?
93
+ raise ArgumentError, "comparison of #{element1.class} with #{element2.class} failed" if sign.nil?
94
+
95
+ sign
96
+ end
97
+
98
+ # Returns the keys of +array+ under +key+, checking that each one is an Integer.
99
+ def integer_keys(array, key, algorithm_title)
100
+ array.map do |element|
101
+ value = key.call(element)
102
+ raise ArgumentError, "#{algorithm_title} needs Integer keys, got #{value.class}" unless value.is_a?(Integer)
103
+
104
+ value
105
+ end
106
+ end
107
+
108
+ # Returns the keys of +array+ under +key+, checking that each one is a finite real number.
109
+ def real_keys(array, key, algorithm_title)
110
+ array.map do |element|
111
+ value = key.call(element)
112
+ unless value.is_a?(Numeric) && value.real? && value.finite?
113
+ raise ArgumentError, "#{algorithm_title} needs finite real number keys, got #{value.inspect}"
114
+ end
115
+
116
+ value
117
+ end
118
+ end
119
+
120
+ def ensure_mutable!(array)
121
+ raise FrozenError.new("can't modify frozen #{array.class}: #{array.inspect}", receiver: array) if array.frozen?
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # For every algorithm name +x+ in {ArraySort.algorithms}, ArraySort responds to +x_sort(array, &block)+,
5
+ # +x_sort!(array, &block)+, +x_sort_by(array, &block)+ and +x_sort_by!(array, &block)+. They behave exactly like
6
+ # the Array methods of the same name (see {ArrayMethods}) but take the array as an argument, so they work without
7
+ # patching Array.
8
+ #
9
+ # @example
10
+ # require 'array_sort'
11
+ # ArraySort.merge_sort([3, 1, 2]) # => [1, 2, 3]
12
+ # ArraySort.quick_sort_by(%w[ccc a bb], &:size) # => ["a", "bb", "ccc"]
13
+ ArraySort.algorithms.each do |name|
14
+ define_singleton_method(:"#{name}_sort") { |array, &block| Core.sort(array, name, &block) }
15
+ define_singleton_method(:"#{name}_sort!") { |array, &block| Core.sort!(array, name, &block) }
16
+ define_singleton_method(:"#{name}_sort_by") do |array, &block|
17
+ block ? Core.sort_by(array, name, &block) : Core.sort_by_enumerator(array, name, bang: false)
18
+ end
19
+ define_singleton_method(:"#{name}_sort_by!") do |array, &block|
20
+ block ? Core.sort_by!(array, name, &block) : Core.sort_by_enumerator(array, name, bang: true)
21
+ end
22
+ end
23
+ end