lite-containers 0.0.5

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +72 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +127 -0
  6. data/README.md +145 -0
  7. data/bench/containers_bench.rb +122 -0
  8. data/lib/lite/containers/abstract/collection.rb +25 -0
  9. data/lib/lite/containers/abstract/deque.rb +23 -0
  10. data/lib/lite/containers/abstract/implicit_key.rb +23 -0
  11. data/lib/lite/containers/abstract/queue.rb +24 -0
  12. data/lib/lite/containers/abstract/sorted_map.rb +29 -0
  13. data/lib/lite/containers/avl_tree/balance.rb +74 -0
  14. data/lib/lite/containers/avl_tree/delete.rb +32 -0
  15. data/lib/lite/containers/avl_tree/find.rb +99 -0
  16. data/lib/lite/containers/avl_tree/implementation.rb +88 -0
  17. data/lib/lite/containers/avl_tree/insert.rb +27 -0
  18. data/lib/lite/containers/avl_tree/interfaces/bracket_assign.rb +33 -0
  19. data/lib/lite/containers/avl_tree/interfaces/key_extraction_strategy/abstract.rb +48 -0
  20. data/lib/lite/containers/avl_tree/interfaces/key_extraction_strategy/explicit.rb +34 -0
  21. data/lib/lite/containers/avl_tree/interfaces/key_extraction_strategy/implicit.rb +60 -0
  22. data/lib/lite/containers/avl_tree/node.rb +35 -0
  23. data/lib/lite/containers/avl_tree/traversal.rb +43 -0
  24. data/lib/lite/containers/avl_tree.rb +159 -0
  25. data/lib/lite/containers/error.rb +7 -0
  26. data/lib/lite/containers/heap.rb +169 -0
  27. data/lib/lite/containers/helpers/comparison.rb +106 -0
  28. data/lib/lite/containers/helpers/key_extractor.rb +29 -0
  29. data/lib/lite/containers/helpers/merge.rb +49 -0
  30. data/lib/lite/containers/sorted_array/binary_search.rb +42 -0
  31. data/lib/lite/containers/sorted_array.rb +188 -0
  32. data/lib/lite/containers/top_n/abstract.rb +56 -0
  33. data/lib/lite/containers/top_n/avl_tree.rb +25 -0
  34. data/lib/lite/containers/top_n/deque.rb +33 -0
  35. data/lib/lite/containers/top_n/error.rb +11 -0
  36. data/lib/lite/containers/top_n/heap.rb +32 -0
  37. data/lib/lite/containers/top_n.rb +31 -0
  38. data/lib/lite/containers/version.rb +7 -0
  39. data/lib/lite/containers.rb +3 -0
  40. data/lite_containers.gemspec +25 -0
  41. metadata +83 -0
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/comparison'
4
+ require_relative 'abstract/collection'
5
+ require_relative 'abstract/queue'
6
+ require_relative 'abstract/implicit_key'
7
+
8
+ module Lite
9
+ module Containers
10
+ class Heap # rubocop:disable Metrics/ClassLength
11
+ include Abstract::Collection
12
+ include Abstract::ImplicitKey
13
+ include Abstract::Queue
14
+
15
+ class Error < StandardError; end
16
+
17
+ def self.instance(type, key_extractor: nil)
18
+ comparison = Helpers::Comparison.instance(type, key_extractor: key_extractor)
19
+ with_comparison(comparison)
20
+ end
21
+
22
+ def self.with_comparison(comparison)
23
+ new(comparison)
24
+ end
25
+
26
+ private_class_method :new
27
+
28
+ def initialize(comparison)
29
+ @comparison = comparison
30
+ reset!
31
+ freeze
32
+ end
33
+
34
+ def size
35
+ @array.size
36
+ end
37
+
38
+ def top
39
+ @array.first
40
+ end
41
+
42
+ def front
43
+ top
44
+ end
45
+
46
+ def push(value)
47
+ @array.push value
48
+ sift_up(length - 1)
49
+ self
50
+ end
51
+
52
+ def replace_top(value)
53
+ sift_down(0, value)
54
+ end
55
+
56
+ def pop
57
+ return if length.zero?
58
+
59
+ if length == 1
60
+ @array.pop
61
+ else
62
+ value = top
63
+ sift_down(0, @array.pop)
64
+ value
65
+ end
66
+ end
67
+
68
+ def pop_front
69
+ pop
70
+ end
71
+
72
+ def drain!
73
+ result = []
74
+ result << pop while length.positive?
75
+ result
76
+ end
77
+
78
+ def reset!
79
+ if frozen?
80
+ @array.clear
81
+ else
82
+ @array = []
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def parent_index(index)
89
+ ((index - 1) / 2)
90
+ end
91
+
92
+ def left_child_value(parent_index)
93
+ index = left_child_index(parent_index)
94
+ @array[index]
95
+ end
96
+
97
+ def left_child_index(index)
98
+ (2 * index) + 1
99
+ end
100
+
101
+ def right_child_value(parent_index)
102
+ index = right_child_index(parent_index)
103
+ @array[index]
104
+ end
105
+
106
+ def right_child_index(index)
107
+ (2 * index) + 2
108
+ end
109
+
110
+ def sift_up(index)
111
+ continue = true
112
+ while continue && index.positive?
113
+ parent_index = parent_index(index)
114
+ if greater? @array[index], @array[parent_index]
115
+ @array[parent_index], @array[index] = @array[index], @array[parent_index]
116
+ index = parent_index
117
+ else
118
+ continue = false
119
+ end
120
+ end
121
+ end
122
+
123
+ def sift_down(index, replacement)
124
+ not_leaf_index = (length / 2) - 1
125
+ while index <= not_leaf_index
126
+ child_index = left_greater?(index) ? left_child_index(index) : right_child_index(index)
127
+ @array[index] = @array[child_index]
128
+ index = child_index
129
+ end
130
+ sift_up_replacement(index, replacement)
131
+ end
132
+
133
+ def sift_up_replacement(index, replacement)
134
+ @array[index] = replacement
135
+ sift_up(index)
136
+ end
137
+
138
+ def balanced_for_left?(parent_value, parent_index)
139
+ value = left_child_value(parent_index)
140
+ return true if value.nil?
141
+
142
+ less_than_or_equal?(value, parent_value)
143
+ end
144
+
145
+ def balanced_for_right?(parent_value, parent_index)
146
+ value = right_child_value(parent_index)
147
+ return true if value.nil?
148
+
149
+ less_than_or_equal?(value, parent_value)
150
+ end
151
+
152
+ def left_greater?(index)
153
+ left_value = left_child_value(index)
154
+ right_value = right_child_value(index)
155
+ return true if right_value.nil?
156
+
157
+ greater? left_value, right_value
158
+ end
159
+
160
+ def less_than_or_equal?(a, b)
161
+ @comparison.compare(a, b) < 1
162
+ end
163
+
164
+ def greater?(a, b)
165
+ @comparison.compare(a, b).positive?
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Lite
6
+ module Containers
7
+ module Helpers
8
+ module Comparison
9
+ class Error < Containers::Error; end
10
+
11
+ def self.instance(type, key_extractor: nil)
12
+ type = type.to_sym
13
+ case type
14
+ when :max then Max.instance(key_extractor)
15
+ when :min then Min.instance(key_extractor)
16
+ else raise Error, "Unexpected comparison type: '#{type}'"
17
+ end
18
+ end
19
+
20
+ class Abstract
21
+ def initialize(key_extractor)
22
+ @key_extractor = key_extractor
23
+ freeze
24
+ end
25
+
26
+ def to_key(element)
27
+ @key_extractor.call(element)
28
+ end
29
+
30
+ def for_item(item)
31
+ method(:compare).curry.call(item)
32
+ end
33
+
34
+ def for_key(key)
35
+ comparison = self.class
36
+ proc do |b|
37
+ comparison.compare(key, to_key(b))
38
+ end
39
+ end
40
+
41
+ def compare(a, b)
42
+ self.class.compare(to_key(a), to_key(b))
43
+ end
44
+
45
+ class << self
46
+ def to_key(element)
47
+ element
48
+ end
49
+
50
+ def for_item(item)
51
+ method(:compare).curry.call(item)
52
+ end
53
+
54
+ alias for_key for_item
55
+ end
56
+ end
57
+
58
+ class Max < Abstract
59
+ def invert
60
+ Comparison.instance :min, key_extractor: @key_extractor
61
+ end
62
+
63
+ class << self
64
+ def instance(key_extractor)
65
+ key_extractor ? new(key_extractor) : self
66
+ end
67
+
68
+ def compare(a, b)
69
+ result = a <=> b
70
+ raise Error, "No meaningful comparison between #{a} <=> #{b}" if result.nil?
71
+
72
+ result
73
+ end
74
+
75
+ def invert
76
+ Min
77
+ end
78
+ end
79
+ end
80
+
81
+ class Min < Abstract
82
+ def invert
83
+ Comparison.instance :max, key_extractor: @key_extractor
84
+ end
85
+
86
+ class << self
87
+ def instance(key_extractor)
88
+ key_extractor ? new(key_extractor) : self
89
+ end
90
+
91
+ def compare(a, b)
92
+ result = b <=> a
93
+ raise Error, "No meaningful comparison between #{a} <=> #{b}" if result.nil?
94
+
95
+ result
96
+ end
97
+
98
+ def invert
99
+ Max
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Lite
6
+ module Containers
7
+ module Helpers
8
+ class KeyExtractor
9
+ def self.instance(input)
10
+ return if input.nil?
11
+ return input if input.respond_to? :to_key
12
+ return new input if input.is_a? Proc
13
+
14
+ raise Error, "Expected nil, key extractor or a proc, got #{input.inspect}"
15
+ end
16
+
17
+ private_class_method :new
18
+
19
+ def initialize(block)
20
+ @block = block
21
+ end
22
+
23
+ def to_key(element)
24
+ @block.call(element)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Lite
6
+ module Containers
7
+ module Helpers
8
+ module Merge
9
+ class Error < Containers::Error; end
10
+
11
+ def self.instance(strategy)
12
+ case strategy
13
+ when nil, :replace then Replace
14
+ when :keep then Keep
15
+ when Proc then Custom.new(strategy)
16
+ else raise Error, "Unexpected strategy for merge: #{strategy}"
17
+ end
18
+ end
19
+
20
+ class Replace
21
+ def self.merge(_old, fresh)
22
+ fresh
23
+ end
24
+ end
25
+
26
+ class Keep
27
+ def self.merge(old, _fresh)
28
+ old
29
+ end
30
+ end
31
+
32
+ class Custom
33
+ def initialize(proc)
34
+ @proc = proc
35
+ freeze
36
+ end
37
+
38
+ def merge(old, fresh)
39
+ proc.call(old, fresh)
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :proc
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/comparison'
4
+
5
+ module Lite
6
+ module Containers
7
+ class SortedArray
8
+ module BinarySearch
9
+ def self.position_of(sorted_array, key, comparison: Helpers::Comparison::Max)
10
+ comparator = comparison.for_key(key)
11
+ find_position(sorted_array, comparator)
12
+ end
13
+
14
+ def self.find_position(sorted_array, comparator)
15
+ upper = sorted_array.length
16
+ lower = 0
17
+ found = false
18
+ index = lower
19
+
20
+ while !found && upper > lower
21
+ index = midpoint(lower, upper)
22
+ candidate = sorted_array[index]
23
+ result = comparator.call(candidate)
24
+ if result.zero?
25
+ found = true
26
+ elsif result.negative?
27
+ upper = index
28
+ else
29
+ index += 1
30
+ lower = index
31
+ end
32
+ end
33
+ [found, index]
34
+ end
35
+
36
+ def self.midpoint(lower, upper)
37
+ ((upper - lower) / 2) + lower
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+ require_relative 'helpers/comparison'
5
+ require_relative 'helpers/merge'
6
+ require_relative 'sorted_array/binary_search'
7
+ require_relative 'abstract/collection'
8
+ require_relative 'abstract/deque'
9
+ require_relative 'abstract/implicit_key'
10
+ require_relative 'abstract/sorted_map'
11
+
12
+ module Lite
13
+ module Containers
14
+ class SortedArray # rubocop:disable Metrics/ClassLength
15
+ include Abstract::Collection
16
+ include Enumerable
17
+ include Abstract::ImplicitKey
18
+ include Abstract::Deque
19
+ include Abstract::SortedMap
20
+
21
+ class Error < Containers::Error; end
22
+
23
+ attr_reader :comparator
24
+
25
+ def self.from_unsorted(type, array, key_extractor: nil, merge: nil)
26
+ comparison = comparison(type, key_extractor)
27
+ sorted = array.sort { |a, b| comparison.compare(a, b) }
28
+ construct(sorted, comparison, merge)
29
+ end
30
+
31
+ def self.from_sorted(type, array, key_extractor: nil, merge: nil)
32
+ comparison = comparison(type, key_extractor)
33
+ sorted = ensure_sorted!(array, comparison)
34
+ construct(sorted, comparison, merge)
35
+ end
36
+
37
+ def self.from_sorted_unsafe(type, sorted, key_extractor: nil, merge: nil)
38
+ comparison = comparison(type, key_extractor)
39
+ construct(sorted, comparison, merge)
40
+ end
41
+
42
+ def self.comparison(type, key_extractor)
43
+ Helpers::Comparison.instance(type, key_extractor: key_extractor)
44
+ end
45
+
46
+ def self.construct(sorted, comparison, merge)
47
+ merge = Helpers::Merge.instance(merge)
48
+ deduplicated = handle_duplicates(sorted, comparison, merge)
49
+ new(deduplicated, comparison, merge)
50
+ end
51
+
52
+ def self.handle_duplicates(array, comparison, merge)
53
+ chunks = array.chunk_while { |a, b| comparison.compare(a, b).zero? }
54
+ chunks.map do |chunk|
55
+ first, *rest = chunk
56
+ next first if rest.empty?
57
+
58
+ rest.reduce(first) do |last, current|
59
+ merge.merge(last, current)
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.ensure_sorted!(array, comparison)
65
+ array.each_cons(2) do |a, b|
66
+ raise Error, 'Input array is not sorted' unless comparison.compare(a, b) < 1
67
+ end
68
+ array
69
+ end
70
+
71
+ private_class_method :new, :comparison, :construct, :handle_duplicates, :ensure_sorted!
72
+
73
+ def initialize(array, comparison, merge)
74
+ @comparison = comparison
75
+ @merge = merge
76
+ @array = array.freeze
77
+ end
78
+
79
+ def drain!
80
+ array = @array
81
+ @array = []
82
+ array.reverse
83
+ end
84
+
85
+ def size
86
+ @array.size
87
+ end
88
+
89
+ def index_of(item)
90
+ found, index = position_of(item)
91
+ return unless found
92
+
93
+ index
94
+ end
95
+
96
+ def position_of(item)
97
+ BinarySearch.find_position(@array, comparison.for_item(item))
98
+ end
99
+
100
+ def each(&block)
101
+ if block
102
+ array.each(&block)
103
+ self
104
+ else
105
+ array.each
106
+ end
107
+ end
108
+
109
+ def to_array
110
+ array
111
+ end
112
+
113
+ def front
114
+ array.last
115
+ end
116
+
117
+ def pop_front
118
+ *rest, item = @array
119
+ @array = rest.freeze
120
+ item
121
+ end
122
+
123
+ def back
124
+ array.first
125
+ end
126
+
127
+ def pop_back
128
+ item, *rest = @array
129
+ @array = rest.freeze
130
+ item
131
+ end
132
+
133
+ def [](index)
134
+ array[index]
135
+ end
136
+
137
+ def key?(key)
138
+ found, = BinarySearch.position_of(@array, key, comparison: comparison)
139
+ found
140
+ end
141
+
142
+ def find(key)
143
+ found, index = BinarySearch.position_of(@array, key, comparison: comparison)
144
+ self[index] if found
145
+ end
146
+
147
+ def find_or_nearest_backwards(key)
148
+ found, position = BinarySearch.position_of(@array, key, comparison: comparison)
149
+ return self[position] if found
150
+ return if position.zero?
151
+
152
+ self[position - 1]
153
+ end
154
+
155
+ def find_or_nearest_forwards(key)
156
+ _, position = BinarySearch.position_of(@array, key, comparison: comparison)
157
+ self[position]
158
+ end
159
+
160
+ def remove_at(index)
161
+ return if index.negative?
162
+
163
+ before = array[0...index]
164
+ to_remove = array[index]
165
+ after = array[(index + 1)..]
166
+ @array = [*before, *after].freeze
167
+ to_remove
168
+ end
169
+
170
+ def push(item)
171
+ found, position = position_of(item)
172
+ item = handle_duplicate(position, item) if found
173
+ before = array[0...position]
174
+ keep_from = found ? position + 1 : position
175
+ after = array[keep_from..]
176
+ @array = [*before, item, *after].freeze
177
+ end
178
+
179
+ private
180
+
181
+ attr_reader :array, :comparison, :merge
182
+
183
+ def handle_duplicate(index, fresh)
184
+ merge.merge(array[index], fresh)
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'error'
5
+ require_relative '../abstract/collection'
6
+ require_relative '../abstract/implicit_key'
7
+ require_relative '../top_n'
8
+
9
+ module Lite
10
+ module Containers
11
+ module TopN
12
+ module Abstract
13
+ include Containers::Abstract::Collection
14
+ include Containers::Abstract::ImplicitKey
15
+ extend Forwardable
16
+
17
+ def_delegator(:@backend, :size)
18
+
19
+ def initialize(backend, limit, filter)
20
+ @backend = backend
21
+ @limit = limit
22
+ @filter = filter
23
+ end
24
+
25
+ attr_reader :limit
26
+
27
+ def push(new_item)
28
+ return unless pass?(new_item)
29
+
30
+ @backend.push(new_item)
31
+ shrink
32
+ end
33
+
34
+ def drain!
35
+ @backend.drain!
36
+ end
37
+
38
+ private
39
+
40
+ def pass?(item)
41
+ return true if @filter.nil?
42
+
43
+ @filter.call(item)
44
+ end
45
+
46
+ def shrink
47
+ return [] if @limit.nil?
48
+
49
+ drop = []
50
+ drop << pop while @backend.size > @limit
51
+ drop
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract'
4
+ require_relative 'deque'
5
+ require_relative '../avl_tree'
6
+
7
+ module Lite
8
+ module Containers
9
+ module TopN
10
+ class AvlTree
11
+ include Deque
12
+ include Abstract
13
+
14
+ Backend = Containers::AvlTree::ImplicitKey
15
+
16
+ def self.instance(type, limit: nil, filter: nil, **backend_options)
17
+ backend = Backend.instance(type, **backend_options)
18
+ new backend, limit, filter
19
+ end
20
+ end
21
+
22
+ register_backend :avl_tree, AvlTree
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../abstract/deque'
4
+
5
+ module Lite
6
+ module Containers
7
+ module TopN
8
+ module Deque
9
+ include Containers::Abstract::Deque
10
+
11
+ def pop
12
+ pop_back
13
+ end
14
+
15
+ def front
16
+ @backend.front
17
+ end
18
+
19
+ def pop_front
20
+ @backend.pop_front
21
+ end
22
+
23
+ def back
24
+ @backend.back
25
+ end
26
+
27
+ def pop_back
28
+ @backend.pop_back
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Lite
6
+ module Containers
7
+ module TopN
8
+ class Error < RuntimeError; end
9
+ end
10
+ end
11
+ end