qfill 0.0.3 → 0.1.1
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 +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 +31 -0
- data/Guardfile +12 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +2 -2
- data/Rakefile +24 -1
- 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 +17 -7
- data/lib/qfill/manager.rb +90 -61
- data/lib/qfill/origin.rb +4 -3
- data/lib/qfill/popper.rb +31 -26
- data/lib/qfill/pusher.rb +34 -23
- data/lib/qfill/result.rb +78 -40
- 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 +105 -0
- data/lib/qfill/strategy.rb +13 -0
- data/lib/qfill/version.rb +3 -1
- data/lib/qfill.rb +13 -10
- 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 +715 -284
- 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 -37
- data/spec/support/helper.rb +13 -0
- data/spec/support/random_object.rb +30 -0
- metadata +52 -23
data/lib/qfill/manager.rb
CHANGED
@@ -1,91 +1,120 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
# A Qfill::Manager builds a set of result data (as `result`) from the source data in Qfill::Popper,
|
6
|
+
# according to the Qfill::Result definitions in the Qfill::Pusher, and the selected strategy.
|
7
|
+
#
|
8
|
+
# Qfill::Manager.new(
|
2
9
|
# :all_list_max => 40,
|
3
10
|
# :popper => popper,
|
4
11
|
# :pusher => pusher,
|
5
|
-
#)
|
12
|
+
# )
|
6
13
|
module Qfill
|
7
14
|
class Manager
|
8
|
-
|
15
|
+
extend Forwardable
|
16
|
+
def_delegators :@strategy,
|
17
|
+
:popper,
|
18
|
+
:pusher,
|
19
|
+
:result,
|
20
|
+
:remaining_to_fill
|
21
|
+
attr_accessor :all_list_max,
|
22
|
+
:primary_list_total,
|
23
|
+
:popper,
|
24
|
+
:pusher,
|
25
|
+
:fill_count,
|
26
|
+
:result,
|
27
|
+
:strategy_options
|
9
28
|
|
10
|
-
STRATEGY_OPTIONS = [
|
29
|
+
STRATEGY_OPTIONS = %i[drain_to_limit drain_to_empty sample time_slice].freeze
|
11
30
|
|
12
31
|
def initialize(options = {})
|
13
|
-
unless options[:popper] && options[:pusher]
|
14
|
-
raise ArgumentError, "#{self.class}: popper and pusher are required options for #{self.class}.new(options)"
|
15
|
-
end
|
16
32
|
unless options[:strategy].nil? || STRATEGY_OPTIONS.include?(options[:strategy])
|
17
|
-
raise ArgumentError,
|
33
|
+
raise ArgumentError,
|
34
|
+
"#{self.class}: strategy is optional, but must be one of #{STRATEGY_OPTIONS.inspect} if provided"
|
18
35
|
end
|
36
|
+
|
37
|
+
@fill_count = 0
|
38
|
+
|
19
39
|
@popper = options[:popper]
|
20
40
|
@pusher = options[:pusher]
|
21
|
-
|
22
|
-
@
|
23
|
-
|
24
|
-
|
41
|
+
@strategy_name = options[:strategy] || :drain_to_limit # or :drain_to_empty or :sample
|
42
|
+
@strategy_options = options[:strategy_options]
|
43
|
+
|
44
|
+
# Allow the strategy to define the pusher when not defined by user
|
45
|
+
@pusher ||= strategy.default_pusher
|
46
|
+
unless @popper && @pusher
|
47
|
+
raise ArgumentError, "#{self.class}: popper and pusher (except where defined by the strategy) are required options for #{self.class}.new(options)"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Provided by user, or defaults to the total number of elements in popper list set
|
51
|
+
@all_list_max = if options[:all_list_max]
|
52
|
+
[options[:all_list_max],
|
53
|
+
popper.count_all_elements].min
|
54
|
+
else
|
55
|
+
popper.count_all_elements
|
56
|
+
end
|
57
|
+
@primary_list_total = popper.count_primary_elements
|
58
|
+
end
|
59
|
+
|
60
|
+
def strategy
|
61
|
+
@strategy ||= case @strategy_name
|
62
|
+
when :drain_to_empty
|
63
|
+
Qfill::Strategy::DrainToEmpty.new(self)
|
64
|
+
when :drain_to_limit
|
65
|
+
Qfill::Strategy::DrainToLimit.new(self)
|
66
|
+
when :sample
|
67
|
+
Qfill::Strategy::Sample.new(self)
|
68
|
+
when :time_slice
|
69
|
+
Qfill::Strategy::TimeSlice.new(self)
|
70
|
+
end
|
25
71
|
end
|
26
72
|
|
27
73
|
def fill!
|
28
|
-
while !is_full? && !
|
29
|
-
|
30
|
-
|
74
|
+
while !is_full? && !popper.primary_empty? && (self.result = pusher.current_list)
|
75
|
+
strategy.on_fill!
|
76
|
+
fill_to_ratio!
|
77
|
+
pusher.set_next_as_current!
|
78
|
+
result.elements.shuffle! if result.shuffle
|
31
79
|
end
|
32
80
|
end
|
33
81
|
|
34
|
-
def fill_to_ratio!
|
35
|
-
|
36
|
-
if
|
37
|
-
|
82
|
+
def fill_to_ratio!
|
83
|
+
strategy.result_max!
|
84
|
+
if result.list_ratios.empty?
|
85
|
+
fill_up_to_ratio!
|
38
86
|
else
|
39
|
-
|
87
|
+
fill_according_to_list_ratios!
|
40
88
|
end
|
41
89
|
end
|
42
90
|
|
43
|
-
def
|
44
|
-
|
45
|
-
if self.strategy == :drain
|
46
|
-
result.list_ratios.each do |list_name, list_ratio|
|
47
|
-
#puts "fill_according_to_list_ratios!, :drain, #{list_name}: Primary remaining => #{self.popper.get_primary_elements}"
|
48
|
-
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio)
|
49
|
-
array_to_push = self.popper.next_objects!(list_name, max_from_list)
|
50
|
-
added = result.push(array_to_push, list_name)
|
51
|
-
end
|
52
|
-
self.fill_count += added
|
53
|
-
elsif self.strategy == :sample
|
54
|
-
while !is_full? && !result.is_full? && !self.popper.totally_empty? && (list_ratio_tuple = result.current_list_ratio)
|
55
|
-
#puts "fill_according_to_list_ratios!, :sample, #{list_ratio_tuple[0]}: Primary remaining => #{self.popper.get_primary_elements}"
|
56
|
-
max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio_tuple[1])
|
57
|
-
array_to_push = self.popper.next_objects!(list_ratio_tuple[0], max_from_list)
|
58
|
-
added = result.push(array_to_push, list_ratio_tuple[0])
|
59
|
-
self.fill_count += added
|
60
|
-
result.set_next_as_current!
|
61
|
-
end
|
62
|
-
end
|
91
|
+
def remaining_to_fill
|
92
|
+
primary_list_total - fill_count
|
63
93
|
end
|
64
94
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
added = result.push(array_to_push, queue.name)
|
74
|
-
end
|
75
|
-
self.fill_count += added
|
76
|
-
elsif self.strategy == :sample
|
77
|
-
while !is_full? && !result.is_full? && !self.popper.totally_empty? && (origin_list = self.popper.current_list)
|
78
|
-
#puts "fill_up_to_ratio!, :sample max #{max_from_list}, #{origin_list.name}: Primary remaining => #{self.popper.get_primary_elements}"
|
79
|
-
array_to_push = self.popper.next_objects!(origin_list.name, max_from_list)
|
80
|
-
added = result.push(array_to_push, origin_list.name)
|
81
|
-
self.fill_count += added
|
82
|
-
self.popper.set_next_as_current!
|
83
|
-
end
|
84
|
-
end
|
95
|
+
# Go through the queues this result should be filled from and push elements from them onto the current result list.
|
96
|
+
def fill_according_to_list_ratios!
|
97
|
+
strategy.fill_according_to_list_ratios!
|
98
|
+
end
|
99
|
+
|
100
|
+
# Go through the primary (non backfill) queues in the popper and push elements from them onto the current result list.
|
101
|
+
def fill_up_to_ratio!
|
102
|
+
strategy.fill_up_to_ratio!
|
85
103
|
end
|
86
104
|
|
87
105
|
def is_full?
|
88
|
-
|
106
|
+
fill_count >= all_list_max
|
107
|
+
end
|
108
|
+
|
109
|
+
def each(&block)
|
110
|
+
# NOTE: on magic: http://blog.arkency.com/2014/01/ruby-to-enum-for-enumerator/
|
111
|
+
return enum_for(:each) unless block # Sparkling magic!
|
112
|
+
|
113
|
+
pusher.each(&block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_s
|
117
|
+
"[#{strategy_name}][Result Max:#{result.max}][All Max:#{all_list_max}][Current Max:#{result.max}][Filled:#{fill_count}][Primary #:#{popper.count_primary_elements}]"
|
89
118
|
end
|
90
119
|
end
|
91
120
|
end
|
data/lib/qfill/origin.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Qfill::Origin.new(:name => "High List",
|
2
4
|
# :elements => [Thing1, Thing3],
|
3
5
|
# :backfill => "Medium List",
|
4
6
|
# :filter => filter1),
|
@@ -12,8 +14,7 @@ module Qfill
|
|
12
14
|
end
|
13
15
|
|
14
16
|
def has_backfill?
|
15
|
-
!!
|
17
|
+
!!backfill
|
16
18
|
end
|
17
|
-
|
18
19
|
end
|
19
20
|
end
|
data/lib/qfill/popper.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A Qfill::Popper (which inherits from Qfill::ListSet) is a set of source data
|
4
|
+
# which will be added to the Qfill::Pusher, by the Qfill::Manager, when generating the result data.
|
5
|
+
# Qfill::Popper is made up of an array (called queues) of Qfill::Origin objects (which inherit from Qfill::List).
|
6
|
+
#
|
7
|
+
# popper = Qfill::Popper.new(
|
2
8
|
# Qfill::Origin.new( :name => "High List",
|
3
9
|
# :elements => [Thing1, Thing3],
|
4
10
|
# :backfill => "Medium List",
|
@@ -11,9 +17,9 @@
|
|
11
17
|
# :elements => [Thing4, Thing5],
|
12
18
|
# :backfill => nil,
|
13
19
|
# :filter => filter1),
|
14
|
-
#)
|
20
|
+
# )
|
15
21
|
#
|
16
|
-
#popper = Qfill::Popper.from_array_of_hashes([
|
22
|
+
# popper = Qfill::Popper.from_array_of_hashes([
|
17
23
|
# { :name => "High List",
|
18
24
|
# :elements => [Thing1, Thing3, Thing7, Thing8, Thing12, Thing15, Thing17],
|
19
25
|
# :backfill => "Medium List",
|
@@ -26,32 +32,39 @@
|
|
26
32
|
# :elements => [Thing4, Thing5, Thing9, Thing10, Thing13, Thing14, Thing18, Thing19, Thing20],
|
27
33
|
# :backfill => nil,
|
28
34
|
# :filter => filter1},
|
29
|
-
#])
|
35
|
+
# ])
|
30
36
|
#
|
31
|
-
# Popper is made up of an array (called queues) of Origin objects.
|
32
37
|
module Qfill
|
33
38
|
class Popper < Qfill::ListSet
|
34
|
-
|
35
39
|
attr_accessor :total_elements
|
36
40
|
|
41
|
+
class << self
|
42
|
+
def from_array_of_hashes(array_of_hashes = [])
|
43
|
+
args = array_of_hashes.map do |hash|
|
44
|
+
Qfill::Origin.new(hash)
|
45
|
+
end
|
46
|
+
Qfill::Popper.new(*args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
37
50
|
def initialize(*args)
|
38
51
|
super(*args)
|
39
|
-
@total_elements =
|
52
|
+
@total_elements = count_all_elements
|
40
53
|
end
|
41
54
|
|
42
55
|
def primary
|
43
|
-
@primary ||=
|
56
|
+
@primary ||= queues.reject { |x| x.backfill == true }
|
44
57
|
end
|
45
58
|
|
46
59
|
def current_list
|
47
|
-
|
60
|
+
primary[current_index]
|
48
61
|
end
|
49
62
|
|
50
63
|
def set_next_as_current!
|
51
|
-
next_index =
|
52
|
-
if (next_index) >=
|
64
|
+
next_index = current_index + 1
|
65
|
+
if (next_index) >= primary.length
|
53
66
|
# If we have iterated through all the queues, then we reset
|
54
|
-
|
67
|
+
reset!
|
55
68
|
else
|
56
69
|
self.current_index = next_index
|
57
70
|
end
|
@@ -60,7 +73,7 @@ module Qfill
|
|
60
73
|
def next_objects!(list_name, n = 1)
|
61
74
|
origin_list = self[list_name]
|
62
75
|
if origin_list.elements.length >= n
|
63
|
-
|
76
|
+
origin_list.elements.pop(n)
|
64
77
|
else
|
65
78
|
result = origin_list.elements.pop(n)
|
66
79
|
while result.length < n && origin_list.has_backfill?
|
@@ -69,28 +82,20 @@ module Qfill
|
|
69
82
|
result += secondary_list.elements.pop(remaining)
|
70
83
|
origin_list = secondary_list
|
71
84
|
end
|
72
|
-
|
85
|
+
result
|
73
86
|
end
|
74
87
|
end
|
75
88
|
|
76
|
-
def self.from_array_of_hashes(array_of_hashes = [])
|
77
|
-
args = array_of_hashes.map do |hash|
|
78
|
-
Qfill::Origin.new(hash)
|
79
|
-
end
|
80
|
-
Qfill::Popper.new(*args)
|
81
|
-
end
|
82
|
-
|
83
89
|
def primary_empty?
|
84
|
-
|
90
|
+
count_primary_elements.zero?
|
85
91
|
end
|
86
92
|
|
87
93
|
def totally_empty?
|
88
|
-
|
94
|
+
count_all_elements.zero?
|
89
95
|
end
|
90
96
|
|
91
|
-
def
|
92
|
-
|
97
|
+
def count_primary_elements
|
98
|
+
primary.inject(0) { |counter, queue| counter += queue.elements.length }
|
93
99
|
end
|
94
|
-
|
95
100
|
end
|
96
101
|
end
|
data/lib/qfill/pusher.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A Qfill::Pusher (which inherits from Qfill::ListSet) is a set of result data
|
4
|
+
# which contribute to the definition of the result set created by the Qfill::Manager.
|
5
|
+
# Qfill::Pusher is made up of an array (called queues) of Qfill::Result objects (which inherit from Qfill::List).
|
6
|
+
#
|
7
|
+
# pusher = Qfill::Pusher.new(
|
2
8
|
# Qfill::Result.new( :name => "Best Results",
|
3
9
|
# :filter => filter3,
|
4
10
|
# :ratio => 0.5,
|
@@ -16,9 +22,9 @@
|
|
16
22
|
# "Low List" => 0.4
|
17
23
|
# }
|
18
24
|
# )
|
19
|
-
#)
|
25
|
+
# )
|
20
26
|
#
|
21
|
-
#pusher = Qfill::Pusher.from_array_of_hashes([
|
27
|
+
# pusher = Qfill::Pusher.from_array_of_hashes([
|
22
28
|
# { :name => "First Result",
|
23
29
|
# :ratio => 0.125,
|
24
30
|
# :filter => filter3,
|
@@ -34,41 +40,40 @@
|
|
34
40
|
# :ratio => 0.125 },
|
35
41
|
# { :name => "Fourth Result",
|
36
42
|
# :ratio => 0.50 },
|
37
|
-
#])
|
43
|
+
# ])
|
38
44
|
#
|
39
45
|
# Pusher is made up of an array (called queues) of Result objects.
|
40
46
|
module Qfill
|
41
47
|
class Pusher < Qfill::ListSet
|
42
|
-
|
43
48
|
def initialize(*args)
|
44
49
|
super(*args)
|
45
|
-
with_ratio =
|
46
|
-
ratio_to_split = (1 - with_ratio.
|
47
|
-
if ratio_to_split < 0
|
48
|
-
|
49
|
-
end
|
50
|
-
num_without_ratio =
|
51
|
-
if num_without_ratio
|
50
|
+
with_ratio = queues.map(&:ratio).compact
|
51
|
+
ratio_to_split = (1 - with_ratio.sum)
|
52
|
+
# if ratio_to_split < 0
|
53
|
+
# raise ArgumentError, "#{self.class}: mismatched ratios for queues #{with_ratio.join(' + ')} must not total more than 1"
|
54
|
+
# end
|
55
|
+
num_without_ratio = queues.length - with_ratio.length
|
56
|
+
if num_without_ratio.positive? && ratio_to_split <= 1
|
52
57
|
equal_portion = ratio_to_split / num_without_ratio
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
+
queues.each do |queue|
|
59
|
+
next unless queue.ratio.nil?
|
60
|
+
|
61
|
+
queue.tap do |q|
|
62
|
+
q.ratio = equal_portion
|
58
63
|
end
|
59
64
|
end
|
60
65
|
end
|
61
66
|
end
|
62
67
|
|
63
68
|
def current_list
|
64
|
-
|
69
|
+
queues[current_index]
|
65
70
|
end
|
66
71
|
|
67
72
|
def set_next_as_current!
|
68
|
-
next_index =
|
69
|
-
if (next_index) ==
|
73
|
+
next_index = current_index + 1
|
74
|
+
if (next_index) == queues.length
|
70
75
|
# If we have iterated through all the queues, then we reset
|
71
|
-
|
76
|
+
reset!
|
72
77
|
else
|
73
78
|
self.current_index = next_index
|
74
79
|
end
|
@@ -82,12 +87,18 @@ module Qfill
|
|
82
87
|
end
|
83
88
|
|
84
89
|
def more_to_fill?
|
85
|
-
!
|
90
|
+
!queues.reject(&:is_full?).empty?
|
86
91
|
end
|
87
92
|
|
88
93
|
def next_to_fill
|
89
|
-
|
94
|
+
queues.reject(&:is_full?).first
|
90
95
|
end
|
91
96
|
|
97
|
+
def each(&block)
|
98
|
+
# NOTE: on magic: http://blog.arkency.com/2014/01/ruby-to-enum-for-enumerator/
|
99
|
+
return enum_for(:each) unless block # Sparkling magic!
|
100
|
+
|
101
|
+
queues.each(&block)
|
102
|
+
end
|
92
103
|
end
|
93
104
|
end
|
data/lib/qfill/result.rb
CHANGED
@@ -1,105 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :preferred is used for :drain_to_empty
|
4
|
+
# :ratio is used for the other strategies
|
1
5
|
# Qfill::Result.new(:name => "Best Results",
|
2
6
|
# :filter => filter3,
|
3
7
|
# :ratio => 0.5,
|
4
8
|
# :list_ratios => {
|
5
9
|
# "High List" => 0.4,
|
6
10
|
# "Medium List" => 0.2,
|
7
|
-
# "Low List" => 0.4 }
|
11
|
+
# "Low List" => 0.4 },
|
12
|
+
# :preferred => ["High List", "Medium List"]
|
13
|
+
# )
|
8
14
|
module Qfill
|
9
15
|
class Result < Qfill::List
|
10
|
-
attr_accessor :ratio,
|
16
|
+
attr_accessor :ratio,
|
17
|
+
:list_ratios,
|
18
|
+
:fill_tracker,
|
19
|
+
:total_count,
|
20
|
+
:current_count,
|
21
|
+
:validate,
|
22
|
+
:current_list_ratio_index,
|
23
|
+
:max,
|
24
|
+
:shuffle,
|
25
|
+
:preferred,
|
26
|
+
:preferred_potential,
|
27
|
+
:preferred_potential_ratio,
|
28
|
+
:max_tracker
|
29
|
+
|
30
|
+
def self.get_limit_from_max_and_ratio(all_list_max, ratio, remain = nil)
|
31
|
+
return 1 if remain == 1
|
11
32
|
|
12
|
-
def self.get_limit_from_max_and_ratio(all_list_max, ratio)
|
13
33
|
limit = (all_list_max * ratio).round(0)
|
14
34
|
# If we rounded down to zero we have to keep at least one.
|
15
35
|
# This is because with small origin sets all ratios might round down to 0.
|
16
|
-
if limit
|
17
|
-
|
18
|
-
end
|
19
|
-
limit
|
36
|
+
limit += 1 if limit.zero?
|
37
|
+
remain ? [limit, remain].min : limit
|
20
38
|
end
|
21
39
|
|
22
40
|
def initialize(options = {})
|
23
41
|
super(options)
|
24
42
|
@list_ratios = options[:list_ratios] || {}
|
25
|
-
with_ratio =
|
26
|
-
ratio_leftover = (1 - with_ratio.
|
27
|
-
if ratio_leftover
|
28
|
-
raise ArgumentError,
|
43
|
+
with_ratio = list_ratio_as_array.map { |tuple| tuple[1] }.compact
|
44
|
+
ratio_leftover = (1 - with_ratio.sum)
|
45
|
+
if ratio_leftover.negative?
|
46
|
+
raise ArgumentError,
|
47
|
+
"#{self.class}: invalid list_ratios for queue '#{name}'. List Ratios (#{with_ratio.join(' + ')}) must not total more than 1"
|
29
48
|
end
|
30
|
-
|
49
|
+
|
50
|
+
@ratio = options[:ratio] || 1
|
31
51
|
@max = 0
|
52
|
+
@preferred = options[:preferred] # Used by :drain_to_empty and :drain_to_limit
|
53
|
+
@preferred_potential = 0
|
54
|
+
@preferred_potential_ratio = 0
|
32
55
|
@fill_tracker = {}
|
33
|
-
@
|
56
|
+
@max_tracker = {}
|
57
|
+
# Doesn't reset to 0 on reset!
|
58
|
+
@total_count = 0
|
59
|
+
# Does reset to 0 on reset!
|
60
|
+
@current_count = 0
|
61
|
+
@shuffle = options[:shuffle] || false
|
34
62
|
@current_list_ratio_index = 0 # Used by :sample strategy
|
35
|
-
@validate =
|
63
|
+
@validate = use_validation?
|
36
64
|
end
|
37
65
|
|
38
66
|
def list_ratio_full?(list_name, max_from_list)
|
39
|
-
|
67
|
+
fill_tracker[list_name] >= max_from_list
|
40
68
|
end
|
41
69
|
|
42
70
|
def push(objects, list_name)
|
43
|
-
|
71
|
+
validate!(list_name)
|
44
72
|
added = 0
|
73
|
+
fill_tracker[list_name] ||= 0
|
45
74
|
objects.each do |object|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
75
|
+
# The objects have already been popped.
|
76
|
+
# The only valid reason to not push an object at this point is if !allow?.
|
77
|
+
# break if is_full?
|
78
|
+
|
79
|
+
next unless allow?(object, list_name)
|
80
|
+
|
81
|
+
bump_fill_tracker!(list_name)
|
82
|
+
add!(object)
|
83
|
+
added += 1
|
84
|
+
# self.print(list_name)
|
52
85
|
end
|
53
|
-
|
86
|
+
added
|
54
87
|
end
|
55
88
|
|
56
89
|
def print(list_name)
|
57
|
-
puts "Added to #{list_name}.\nResult List #{
|
90
|
+
puts "Added to #{list_name}.\nResult List #{name} now has #{elements.length} total objects.\nSources:\n #{fill_tracker.inspect} "
|
58
91
|
end
|
59
92
|
|
60
93
|
def add!(object)
|
61
|
-
|
94
|
+
elements << object
|
62
95
|
end
|
63
96
|
|
64
97
|
def allow?(object, list_name)
|
65
|
-
!
|
98
|
+
!filter.respond_to?(:call) ||
|
66
99
|
# If there is a filter, then it must return true to proceed
|
67
|
-
|
100
|
+
filter.run(object, list_name)
|
68
101
|
end
|
69
102
|
|
70
103
|
def bump_fill_tracker!(list_name)
|
71
|
-
|
72
|
-
self.
|
73
|
-
self.
|
104
|
+
fill_tracker[list_name] += 1
|
105
|
+
self.total_count += 1
|
106
|
+
self.current_count += 1
|
74
107
|
end
|
75
108
|
|
76
109
|
# Does the queue being pushed into match one of the list_ratios
|
77
110
|
def valid?(list_name)
|
78
|
-
|
111
|
+
list_ratios.key?(list_name)
|
79
112
|
end
|
80
113
|
|
81
114
|
def validate!(list_name)
|
82
|
-
|
115
|
+
if validate && !valid?(list_name)
|
116
|
+
raise ArgumentError,
|
117
|
+
"#{self.class}: #{list_name} is an invalid list_name. Valid list_names are: #{list_ratios.keys}"
|
118
|
+
end
|
83
119
|
end
|
84
120
|
|
85
121
|
def use_validation?
|
86
|
-
!
|
122
|
+
!list_ratios.empty?
|
87
123
|
end
|
88
124
|
|
89
125
|
def list_ratio_as_array
|
90
126
|
# [["high",0.4],["medium",0.4],["low",0.2]]
|
91
|
-
@list_ratio_as_array ||=
|
127
|
+
@list_ratio_as_array ||= list_ratios.to_a
|
92
128
|
end
|
93
129
|
|
94
130
|
def current_list_ratio
|
95
|
-
|
131
|
+
list_ratio_as_array[current_list_ratio_index]
|
96
132
|
end
|
97
133
|
|
98
134
|
def set_next_as_current!
|
99
|
-
next_index =
|
100
|
-
if (next_index) ==
|
135
|
+
next_index = current_list_ratio_index + 1
|
136
|
+
if (next_index) == list_ratio_as_array.length
|
101
137
|
# If we have iterated through all the list_ratios, then we reset
|
102
|
-
|
138
|
+
reset!
|
103
139
|
else
|
104
140
|
self.current_list_ratio_index = next_index
|
105
141
|
end
|
@@ -107,14 +143,16 @@ module Qfill
|
|
107
143
|
|
108
144
|
def reset!
|
109
145
|
self.current_list_ratio_index = 0
|
146
|
+
self.current_count = 0
|
110
147
|
end
|
111
148
|
|
149
|
+
# Results can overfill, because fractions.
|
112
150
|
def is_full?
|
113
|
-
self.
|
151
|
+
self.total_count >= max
|
114
152
|
end
|
115
153
|
|
116
154
|
def to_s
|
117
|
-
"Qfill::Result: ratio: #{
|
155
|
+
"Qfill::Result: ratio: #{ratio}, list_ratios: #{list_ratios}, fill_tracker: #{fill_tracker}, total_count: #{self.total_count}, current_count: #{self.current_count}, filter: #{!!filter ? 'Yes' : 'No'}, current_list_ratio_index: #{current_list_ratio_index}, max: #{max}"
|
118
156
|
end
|
119
157
|
end
|
120
158
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Qfill
|
6
|
+
module Strategy
|
7
|
+
class Base
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@manager,
|
10
|
+
:all_list_max,
|
11
|
+
:popper,
|
12
|
+
:pusher,
|
13
|
+
:result,
|
14
|
+
:primary_list_total,
|
15
|
+
:fill_count,
|
16
|
+
:fill_count=,
|
17
|
+
:is_full?,
|
18
|
+
:strategy_options
|
19
|
+
attr_accessor :added,
|
20
|
+
:tally,
|
21
|
+
:ratio_modifier
|
22
|
+
|
23
|
+
def initialize(manager)
|
24
|
+
@manager = manager
|
25
|
+
@added = 0
|
26
|
+
@tally = 0
|
27
|
+
@ratio_modifier = 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def name
|
31
|
+
NAME
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_fill!
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
def fill_to_ratio!
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
# Go through the queues this result should be filled from and push elements from them onto the current result list.
|
43
|
+
def fill_according_to_list_ratios!
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
def fill_up_to_ratio!
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
def default_pusher
|
52
|
+
# NOOP
|
53
|
+
end
|
54
|
+
|
55
|
+
def bump!
|
56
|
+
self.tally += added
|
57
|
+
self.fill_count += added
|
58
|
+
end
|
59
|
+
|
60
|
+
def remaining
|
61
|
+
all_list_max - fill_count
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|