qfill 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +8 -0
- data/.github/workflows/style.yml +37 -0
- data/.github/workflows/test.yml +55 -0
- data/.rspec +3 -2
- data/.rubocop.yml +26 -0
- data/.rubocop_todo.yml +163 -0
- data/.simplecov +6 -0
- data/CODE_OF_CONDUCT.md +133 -0
- data/Gemfile +33 -0
- data/Guardfile +12 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +2 -2
- data/Rakefile +24 -1
- data/lib/qfill.rb +13 -10
- data/lib/qfill/errors/invalid_index.rb +8 -0
- data/lib/qfill/filter.rb +5 -3
- data/lib/qfill/list.rb +4 -2
- data/lib/qfill/list_set.rb +13 -9
- data/lib/qfill/manager.rb +78 -124
- data/lib/qfill/origin.rb +4 -3
- data/lib/qfill/popper.rb +30 -25
- data/lib/qfill/pusher.rb +33 -22
- data/lib/qfill/result.rb +63 -42
- data/lib/qfill/strategy.rb +13 -0
- data/lib/qfill/strategy/base.rb +65 -0
- data/lib/qfill/strategy/drain_to_empty.rb +73 -0
- data/lib/qfill/strategy/drain_to_limit.rb +46 -0
- data/lib/qfill/strategy/sample.rb +42 -0
- data/lib/qfill/strategy/time_slice.rb +106 -0
- data/lib/qfill/version.rb +3 -1
- data/maintenance-branch +1 -0
- data/qfill.gemspec +15 -13
- data/spec/qfill/filter_spec.rb +35 -26
- data/spec/qfill/list_set_spec.rb +28 -23
- data/spec/qfill/list_spec.rb +35 -27
- data/spec/qfill/manager_spec.rb +670 -434
- data/spec/qfill/origin_spec.rb +45 -35
- data/spec/qfill/popper_spec.rb +36 -30
- data/spec/qfill/pusher_spec.rb +32 -26
- data/spec/qfill/result_spec.rb +49 -38
- data/spec/qfill_spec.rb +6 -5
- data/spec/spec_helper.rb +11 -38
- data/spec/support/helper.rb +13 -0
- data/spec/support/random_object.rb +30 -0
- metadata +52 -23
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qfill
|
4
|
+
module Strategy
|
5
|
+
class DrainToEmpty < Qfill::Strategy::Base
|
6
|
+
NAME = :drain_to_empty
|
7
|
+
|
8
|
+
def on_fill!
|
9
|
+
preferred_potential_ratio = 0
|
10
|
+
preferred_potential = 0
|
11
|
+
result.list_ratios.each do |list_name, list_ratio|
|
12
|
+
poppy = result.preferred.select { |x| x == list_name }
|
13
|
+
next unless poppy
|
14
|
+
|
15
|
+
preferred_potential_ratio += list_ratio
|
16
|
+
num = popper[list_name].elements.length
|
17
|
+
preferred_potential += num
|
18
|
+
result.max_tracker[list_name] = num
|
19
|
+
end
|
20
|
+
result.preferred_potential = preferred_potential
|
21
|
+
result.preferred_potential_ratio = preferred_potential_ratio
|
22
|
+
end
|
23
|
+
|
24
|
+
def result_max!
|
25
|
+
result.max = if result.preferred_potential_ratio.positive?
|
26
|
+
[
|
27
|
+
(result.preferred_potential / result.preferred_potential_ratio),
|
28
|
+
primary_list_total,
|
29
|
+
remaining
|
30
|
+
].min
|
31
|
+
else
|
32
|
+
[
|
33
|
+
primary_list_total,
|
34
|
+
remaining
|
35
|
+
].min
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def fill_up_to_ratio!
|
40
|
+
popper.primary.each do |queue|
|
41
|
+
array_to_push = popper.next_objects!(queue.name, [result.max, remaining].min)
|
42
|
+
self.added = result.push(array_to_push, queue.name)
|
43
|
+
popper.current_index = popper.index_of(queue.name)
|
44
|
+
bump!
|
45
|
+
puts "[fill_up_to_ratio!]#{self}[Q:#{queue.name}][added:#{added}]" if Qfill::VERBOSE
|
46
|
+
break if is_full?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def fill_according_to_list_ratios!
|
51
|
+
# Are there any elements in preferred queues that we should add?
|
52
|
+
return unless result.preferred_potential.positive?
|
53
|
+
|
54
|
+
# Setup a ratio modifier for the non-preferred queues
|
55
|
+
result.list_ratios.each do |list_name, list_ratio|
|
56
|
+
max_from_list = if result.max_tracker[list_name]
|
57
|
+
[result.max_tracker[list_name], remaining].min
|
58
|
+
else
|
59
|
+
Qfill::Result.get_limit_from_max_and_ratio(
|
60
|
+
result.max, list_ratio, remaining
|
61
|
+
)
|
62
|
+
end
|
63
|
+
array_to_push = popper.next_objects!(list_name, max_from_list)
|
64
|
+
self.added = result.push(array_to_push, list_name)
|
65
|
+
popper.current_index = popper.index_of(list_name)
|
66
|
+
bump!
|
67
|
+
puts "[fill_according_to_list_ratios!]#{self}[#{list_name}][added:#{added}]" if Qfill::VERBOSE
|
68
|
+
break if is_full?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qfill
|
4
|
+
module Strategy
|
5
|
+
class DrainToLimit < Qfill::Strategy::Base
|
6
|
+
NAME = :drain_to_limit
|
7
|
+
|
8
|
+
def on_fill!
|
9
|
+
# NOOP
|
10
|
+
end
|
11
|
+
|
12
|
+
def result_max!
|
13
|
+
result.max = Qfill::Result.get_limit_from_max_and_ratio(primary_list_total, result.ratio, remaining)
|
14
|
+
end
|
15
|
+
|
16
|
+
def fill_up_to_ratio!
|
17
|
+
num_primary = popper.primary.length
|
18
|
+
ratio = 1.0 / num_primary # 1 divided by the number of queues
|
19
|
+
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, ratio, remaining)
|
20
|
+
popper.primary.each_with_index do |queue, idx|
|
21
|
+
# Are there leftovers that will be missed by a straight ratio'd iteration?
|
22
|
+
mod = result.max % num_primary
|
23
|
+
max_from_list += (mod / num_primary).ceil if idx.zero? && mod.positive?
|
24
|
+
array_to_push = popper.next_objects!(queue.name, [max_from_list, remaining].min)
|
25
|
+
self.added = result.push(array_to_push, queue.name)
|
26
|
+
popper.current_index = popper.index_of(queue.name)
|
27
|
+
puts "[fill_up_to_ratio!]#{self}[Q:#{queue.name}][added:#{added}]" if Qfill::VERBOSE
|
28
|
+
bump!
|
29
|
+
break if is_full?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def fill_according_to_list_ratios!
|
34
|
+
result.list_ratios.each do |list_name, list_ratio|
|
35
|
+
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio, remaining)
|
36
|
+
array_to_push = popper.next_objects!(list_name, max_from_list)
|
37
|
+
self.added = result.push(array_to_push, list_name)
|
38
|
+
popper.current_index = popper.index_of(list_name)
|
39
|
+
puts "[fill_according_to_list_ratios!]#{self}[#{list_name}][added:#{added}]" if Qfill::VERBOSE
|
40
|
+
bump!
|
41
|
+
break if is_full?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qfill
|
4
|
+
module Strategy
|
5
|
+
class Sample < Qfill::Strategy::Base
|
6
|
+
NAME = :sample
|
7
|
+
|
8
|
+
def on_fill!
|
9
|
+
# NOOP
|
10
|
+
end
|
11
|
+
|
12
|
+
def result_max!
|
13
|
+
result.max = Qfill::Result.get_limit_from_max_and_ratio(primary_list_total, result.ratio, remaining)
|
14
|
+
end
|
15
|
+
|
16
|
+
def fill_up_to_ratio!
|
17
|
+
ratio = 1.0 / popper.primary.length # 1 divided by the number of queues
|
18
|
+
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, ratio, remaining)
|
19
|
+
while !is_full? && !result.is_full? && !popper.totally_empty? && (origin_list = popper.current_list)
|
20
|
+
array_to_push = popper.next_objects!(origin_list.name, [max_from_list, remaining].min)
|
21
|
+
self.added = result.push(array_to_push, origin_list.name)
|
22
|
+
bump!
|
23
|
+
puts "[fill_up_to_ratio!]#{self}[Added:#{added}][Max List:#{max_from_list}][ratio:#{ratio}][added:#{added}]" if Qfill::VERBOSE
|
24
|
+
popper.set_next_as_current!
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def fill_according_to_list_ratios!
|
29
|
+
# puts "#{!is_full?} && #{result.fill_count} >= #{result.max} && #{!self.popper.totally_empty?} && #{(list_ratio_tuple = result.current_list_ratio)}"
|
30
|
+
while !is_full? && !result.is_full? && !popper.totally_empty? && (list_ratio_tuple = result.current_list_ratio)
|
31
|
+
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio_tuple[1], remaining)
|
32
|
+
array_to_push = popper.next_objects!(list_ratio_tuple[0], max_from_list)
|
33
|
+
self.added = result.push(array_to_push, list_ratio_tuple[0])
|
34
|
+
bump!
|
35
|
+
puts "[fill_according_to_list_ratios!]#{self}[#{list_ratio_tuple[0]}][added:#{added}]" if Qfill::VERBOSE
|
36
|
+
result.set_next_as_current!
|
37
|
+
break if is_full?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qfill
|
4
|
+
module Strategy
|
5
|
+
# Qfill::Manager.new(
|
6
|
+
# :popper => popper,
|
7
|
+
# :strategy_options => {
|
8
|
+
# :window_size => 20,
|
9
|
+
# :window_units => "minutes" # "days", "hours", "minutes", "seconds",
|
10
|
+
# # NOTE: pane_size/units can't be larger than the window_size/units
|
11
|
+
# :pane_size => 2
|
12
|
+
# :pane_units => "seconds" # "days", "hours", "minutes", "seconds",
|
13
|
+
# },
|
14
|
+
# )
|
15
|
+
class TimeSlice < Qfill::Strategy::Sample
|
16
|
+
NAME = :time_slice
|
17
|
+
|
18
|
+
def on_fill!
|
19
|
+
# NOOP
|
20
|
+
end
|
21
|
+
|
22
|
+
CONVERSIONS = {
|
23
|
+
%w[seconds seconds] => 1,
|
24
|
+
%w[seconds minutes] => 60,
|
25
|
+
%w[seconds hours] => 60 * 60,
|
26
|
+
%w[seconds days] => 60 * 60 * 24,
|
27
|
+
%w[minutes minutes] => 1,
|
28
|
+
%w[minutes hours] => 60,
|
29
|
+
%w[minutes days] => 60 * 24,
|
30
|
+
%w[hours hours] => 1,
|
31
|
+
%w[hours days] => 24,
|
32
|
+
%w[days days] => 1
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
# If window_units == "minutes" and pane_units == "seconds", and
|
36
|
+
# window_size == 20 and pane_size == 2
|
37
|
+
# Then there would be (20 * CONVERSIONS[[pane_units, window_units]]) / pane_size
|
38
|
+
# i.e. (20 * 60) / 2
|
39
|
+
# i.e. 600 individual panes in the (time) window, where each pane is a "result"
|
40
|
+
def default_pusher
|
41
|
+
ratio = 1 / num_panes.to_f
|
42
|
+
array = Range.new(1, num_panes).each_with_object([]) do |pane_num, arr|
|
43
|
+
arr << { name: pane_num.to_s, ratio: ratio }
|
44
|
+
end
|
45
|
+
Qfill::Pusher.from_array_of_hashes(array)
|
46
|
+
end
|
47
|
+
|
48
|
+
def window_size
|
49
|
+
strategy_options[:window_size]
|
50
|
+
end
|
51
|
+
|
52
|
+
def window_units
|
53
|
+
strategy_options[:window_units]
|
54
|
+
end
|
55
|
+
|
56
|
+
def pane_size
|
57
|
+
strategy_options[:pane_size]
|
58
|
+
end
|
59
|
+
|
60
|
+
def pane_units
|
61
|
+
strategy_options[:pane_units]
|
62
|
+
end
|
63
|
+
|
64
|
+
def conversion
|
65
|
+
conversion_idx = [pane_units, window_units]
|
66
|
+
conv = CONVERSIONS[conversion_idx]
|
67
|
+
raise ArgumentError, "pane_units: #{pane_units} must not be larger than window_units: #{window_units}" unless conv
|
68
|
+
|
69
|
+
conv
|
70
|
+
end
|
71
|
+
|
72
|
+
def num_panes
|
73
|
+
((window_size * conversion) / pane_size)
|
74
|
+
end
|
75
|
+
|
76
|
+
def result_max!
|
77
|
+
result.max = Qfill::Result.get_limit_from_max_and_ratio(primary_list_total, result.ratio, remaining)
|
78
|
+
end
|
79
|
+
|
80
|
+
def fill_up_to_ratio!
|
81
|
+
ratio = 1.0 / popper.primary.length # 1 divided by the number of queues
|
82
|
+
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, ratio, remaining)
|
83
|
+
while !is_full? && !result.is_full? && !popper.totally_empty? && (origin_list = popper.current_list)
|
84
|
+
array_to_push = popper.next_objects!(origin_list.name, [max_from_list, remaining].min)
|
85
|
+
self.added = result.push(array_to_push, origin_list.name)
|
86
|
+
bump!
|
87
|
+
puts "[fill_up_to_ratio!]#{self}[Added:#{added}][Max List:#{max_from_list}][ratio:#{ratio}][added:#{added}]" if Qfill::VERBOSE
|
88
|
+
popper.set_next_as_current!
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def fill_according_to_list_ratios!
|
93
|
+
# puts "#{!is_full?} && #{result.fill_count} >= #{result.max} && #{!self.popper.totally_empty?} && #{(list_ratio_tuple = result.current_list_ratio)}"
|
94
|
+
while !is_full? && !result.is_full? && !popper.totally_empty? && (list_ratio_tuple = result.current_list_ratio)
|
95
|
+
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio_tuple[1], remaining)
|
96
|
+
array_to_push = popper.next_objects!(list_ratio_tuple[0], max_from_list)
|
97
|
+
self.added = result.push(array_to_push, list_ratio_tuple[0])
|
98
|
+
bump!
|
99
|
+
puts "[fill_according_to_list_ratios!]#{self}[#{list_ratio_tuple[0]}][added:#{added}]" if Qfill::VERBOSE
|
100
|
+
result.set_next_as_current!
|
101
|
+
break if is_full?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/qfill/version.rb
CHANGED
data/maintenance-branch
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
master
|
data/qfill.gemspec
CHANGED
@@ -1,15 +1,16 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'qfill/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |gem|
|
7
|
-
gem.name =
|
8
|
+
gem.name = 'qfill'
|
8
9
|
gem.version = Qfill::VERSION
|
9
|
-
gem.authors = [
|
10
|
-
gem.email = [
|
11
|
-
gem.description =
|
12
|
-
gem.summary =
|
10
|
+
gem.authors = ['Peter Boling']
|
11
|
+
gem.email = ['peter.boling@gmail.com']
|
12
|
+
gem.description = 'Advanced Queue Transformation'
|
13
|
+
gem.summary = 'You have a set of arrays that need to be turned into a different set of arrays
|
13
14
|
according to a potentially non-uniform set of rules.
|
14
15
|
|
15
16
|
Now you can easily turn this:
|
@@ -24,13 +25,14 @@ result_b # => [3,5,7,9]
|
|
24
25
|
result_c # => [4,6,8]
|
25
26
|
|
26
27
|
by specifying filters for handling each transformation.
|
27
|
-
|
28
|
-
gem.homepage =
|
28
|
+
'
|
29
|
+
gem.homepage = 'https://github.com/pboling/qfill'
|
29
30
|
|
30
|
-
gem.files = `git ls-files`.split(
|
31
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
31
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
32
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
32
33
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
33
|
-
gem.require_paths = [
|
34
|
+
gem.require_paths = ['lib']
|
34
35
|
|
35
|
-
gem.add_development_dependency '
|
36
|
+
gem.add_development_dependency 'rake', '~> 13'
|
37
|
+
gem.add_development_dependency 'rspec', '~> 3'
|
36
38
|
end
|
data/spec/qfill/filter_spec.rb
CHANGED
@@ -1,43 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
describe Qfill::Filter do
|
3
|
-
|
4
|
-
context
|
5
|
-
before
|
6
|
-
@lambda = ->
|
5
|
+
describe '#new' do
|
6
|
+
context 'with processor' do
|
7
|
+
before do
|
8
|
+
@lambda = ->(object) { !object.nil? }
|
7
9
|
end
|
8
|
-
|
9
|
-
|
10
|
+
|
11
|
+
it 'instantiates with processor' do
|
12
|
+
expect(described_class.new(@lambda)).to be_a(described_class)
|
10
13
|
end
|
11
14
|
end
|
12
15
|
|
13
|
-
context
|
14
|
-
before
|
15
|
-
@lambda = ->
|
16
|
-
@arguments = [
|
16
|
+
context 'with processor and arguments' do
|
17
|
+
before do
|
18
|
+
@lambda = ->(object, first, second) { !object.nil? && first == first && second == 'second' }
|
19
|
+
@arguments = %w[first second]
|
17
20
|
end
|
18
|
-
|
19
|
-
|
21
|
+
|
22
|
+
it 'instantiates with processor' do
|
23
|
+
expect(described_class.new(@lambda, *@arguments)).to be_a(described_class)
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
23
27
|
|
24
|
-
|
25
|
-
before
|
26
|
-
@lambda = ->
|
27
|
-
@arguments = [
|
28
|
-
@filter =
|
28
|
+
describe '#run' do
|
29
|
+
before do
|
30
|
+
@lambda = ->(object, first, second) { !object.nil? && first == first && second == 'second' }
|
31
|
+
@arguments = %w[first second]
|
32
|
+
@filter = described_class.new(@lambda, *@arguments)
|
29
33
|
end
|
30
|
-
|
31
|
-
|
34
|
+
|
35
|
+
it 'returns the correct result' do
|
36
|
+
expect(@filter.run('not nil')).to eq(true)
|
32
37
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@
|
37
|
-
|
38
|
+
|
39
|
+
context 'with extra arguments' do
|
40
|
+
before do
|
41
|
+
@lambda = lambda { |object, _special_arg1, special_arg2, first, second, third|
|
42
|
+
!object.nil? && first == first && second == 'second' && special_arg1 = 'this' && special_arg2 == 'thing' && third == 'third'
|
43
|
+
}
|
44
|
+
@arguments = %w[first second third]
|
45
|
+
@filter = described_class.new(@lambda, *@arguments)
|
38
46
|
end
|
39
|
-
|
40
|
-
|
47
|
+
|
48
|
+
it 'properlies use arity' do
|
49
|
+
expect(@filter.run('not nil', 'this', 'thing')).to eq(true)
|
41
50
|
end
|
42
51
|
end
|
43
52
|
end
|
data/spec/qfill/list_set_spec.rb
CHANGED
@@ -1,36 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
describe Qfill::ListSet do
|
3
|
-
|
4
|
-
context
|
5
|
-
it
|
6
|
-
expect {
|
5
|
+
describe '#new' do
|
6
|
+
context 'with no arguments' do
|
7
|
+
it 'raises ArgumentError' do
|
8
|
+
expect { described_class.new }.to raise_error(ArgumentError)
|
7
9
|
end
|
8
10
|
end
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
|
12
|
+
context 'with arguments' do
|
13
|
+
before do
|
14
|
+
@filter = Qfill::Filter.new(->(object) { object.is_a?(Numeric) })
|
12
15
|
@origin_queues = [
|
13
16
|
Qfill::List.new(
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
name: 'High List',
|
18
|
+
elements: [1, 2, 3, 'c'],
|
19
|
+
filter: @filter
|
20
|
+
),
|
21
|
+
Qfill::List.new(name: 'Medium List',
|
22
|
+
elements: ['e', 'f', 4, 5],
|
23
|
+
filter: @filter),
|
24
|
+
Qfill::List.new(name: 'Low List',
|
25
|
+
elements: [7, 8, 'd'],
|
26
|
+
filter: @filter)
|
23
27
|
]
|
24
28
|
end
|
25
|
-
|
26
|
-
|
29
|
+
|
30
|
+
it 'does not raise any errors' do
|
31
|
+
expect { described_class.new(*@origin_queues) }.not_to raise_error
|
27
32
|
end
|
28
|
-
|
29
|
-
|
30
|
-
popper
|
31
|
-
popper.queues.
|
33
|
+
|
34
|
+
it 'instantiates with name' do
|
35
|
+
popper = described_class.new(*@origin_queues)
|
36
|
+
expect(popper.queues.first.elements).to eq([1, 2, 3, 'c'])
|
37
|
+
expect(popper.queues.last.elements).to eq([7, 8, 'd'])
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
35
|
-
|
36
41
|
end
|