qfill 0.0.4 → 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 +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
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
97
|
def count_primary_elements
|
92
|
-
|
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
|
50
|
+
with_ratio = queues.map(&:ratio).compact
|
51
|
+
ratio_to_split = (1 - with_ratio.sum)
|
52
|
+
# if ratio_to_split < 0
|
48
53
|
# raise ArgumentError, "#{self.class}: mismatched ratios for queues #{with_ratio.join(' + ')} must not total more than 1"
|
49
|
-
#end
|
50
|
-
num_without_ratio =
|
51
|
-
if num_without_ratio
|
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,4 +1,6 @@
|
|
1
|
-
# :
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :preferred is used for :drain_to_empty
|
2
4
|
# :ratio is used for the other strategies
|
3
5
|
# Qfill::Result.new(:name => "Best Results",
|
4
6
|
# :filter => filter3,
|
@@ -11,108 +13,127 @@
|
|
11
13
|
# )
|
12
14
|
module Qfill
|
13
15
|
class Result < Qfill::List
|
14
|
-
attr_accessor :ratio,
|
15
|
-
:
|
16
|
-
|
17
|
-
|
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)
|
18
31
|
limit = (all_list_max * ratio).round(0)
|
19
32
|
# If we rounded down to zero we have to keep at least one.
|
20
33
|
# This is because with small origin sets all ratios might round down to 0.
|
21
|
-
if limit
|
22
|
-
|
23
|
-
end
|
24
|
-
limit
|
34
|
+
limit += 1 if limit.zero?
|
35
|
+
remain ? [limit, remain].min : limit
|
25
36
|
end
|
26
37
|
|
27
38
|
def initialize(options = {})
|
28
39
|
super(options)
|
29
40
|
@list_ratios = options[:list_ratios] || {}
|
30
|
-
with_ratio =
|
31
|
-
ratio_leftover = (1 - with_ratio.
|
32
|
-
if ratio_leftover
|
33
|
-
raise ArgumentError,
|
41
|
+
with_ratio = list_ratio_as_array.map { |tuple| tuple[1] }.compact
|
42
|
+
ratio_leftover = (1 - with_ratio.sum)
|
43
|
+
if ratio_leftover.negative?
|
44
|
+
raise ArgumentError,
|
45
|
+
"#{self.class}: invalid list_ratios for queue '#{name}'. List Ratios (#{with_ratio.join(' + ')}) must not total more than 1"
|
34
46
|
end
|
47
|
+
|
35
48
|
@ratio = options[:ratio] || 1
|
36
49
|
@max = 0
|
37
50
|
@preferred = options[:preferred] # Used by :drain_to_empty and :drain_to_limit
|
38
51
|
@preferred_potential = 0
|
39
52
|
@preferred_potential_ratio = 0
|
40
|
-
@strategy = options[:strategy] # nil, :drain_to_limit, :drain_to_empty or :sample
|
41
53
|
@fill_tracker = {}
|
42
54
|
@max_tracker = {}
|
43
|
-
|
55
|
+
# Doesn't reset to 0 on reset!
|
56
|
+
@total_count = 0
|
57
|
+
# Does reset to 0 on reset!
|
44
58
|
@current_count = 0
|
45
59
|
@shuffle = options[:shuffle] || false
|
46
60
|
@current_list_ratio_index = 0 # Used by :sample strategy
|
47
|
-
@validate =
|
61
|
+
@validate = use_validation?
|
48
62
|
end
|
49
63
|
|
50
64
|
def list_ratio_full?(list_name, max_from_list)
|
51
|
-
|
65
|
+
fill_tracker[list_name] >= max_from_list
|
52
66
|
end
|
53
67
|
|
54
68
|
def push(objects, list_name)
|
55
|
-
|
69
|
+
validate!(list_name)
|
56
70
|
added = 0
|
57
|
-
|
71
|
+
fill_tracker[list_name] ||= 0
|
58
72
|
objects.each do |object|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
73
|
+
# The objects have already been popped.
|
74
|
+
# The only valid reason to not push an object at this point is if !allow?.
|
75
|
+
# break if is_full?
|
76
|
+
|
77
|
+
next unless allow?(object, list_name)
|
78
|
+
|
79
|
+
bump_fill_tracker!(list_name)
|
80
|
+
add!(object)
|
81
|
+
added += 1
|
82
|
+
# self.print(list_name)
|
65
83
|
end
|
66
|
-
|
84
|
+
added
|
67
85
|
end
|
68
86
|
|
69
87
|
def print(list_name)
|
70
|
-
puts "Added to #{list_name}.\nResult List #{
|
88
|
+
puts "Added to #{list_name}.\nResult List #{name} now has #{elements.length} total objects.\nSources:\n #{fill_tracker.inspect} "
|
71
89
|
end
|
72
90
|
|
73
91
|
def add!(object)
|
74
|
-
|
92
|
+
elements << object
|
75
93
|
end
|
76
94
|
|
77
95
|
def allow?(object, list_name)
|
78
|
-
!
|
96
|
+
!filter.respond_to?(:call) ||
|
79
97
|
# If there is a filter, then it must return true to proceed
|
80
|
-
|
98
|
+
filter.run(object, list_name)
|
81
99
|
end
|
82
100
|
|
83
101
|
def bump_fill_tracker!(list_name)
|
84
|
-
|
85
|
-
self.
|
102
|
+
fill_tracker[list_name] += 1
|
103
|
+
self.total_count += 1
|
86
104
|
self.current_count += 1
|
87
105
|
end
|
88
106
|
|
89
107
|
# Does the queue being pushed into match one of the list_ratios
|
90
108
|
def valid?(list_name)
|
91
|
-
|
109
|
+
list_ratios.key?(list_name)
|
92
110
|
end
|
93
111
|
|
94
112
|
def validate!(list_name)
|
95
|
-
|
113
|
+
if validate && !valid?(list_name)
|
114
|
+
raise ArgumentError,
|
115
|
+
"#{self.class}: #{list_name} is an invalid list_name. Valid list_names are: #{list_ratios.keys}"
|
116
|
+
end
|
96
117
|
end
|
97
118
|
|
98
119
|
def use_validation?
|
99
|
-
!
|
120
|
+
!list_ratios.empty?
|
100
121
|
end
|
101
122
|
|
102
123
|
def list_ratio_as_array
|
103
124
|
# [["high",0.4],["medium",0.4],["low",0.2]]
|
104
|
-
@list_ratio_as_array ||=
|
125
|
+
@list_ratio_as_array ||= list_ratios.to_a
|
105
126
|
end
|
106
127
|
|
107
128
|
def current_list_ratio
|
108
|
-
|
129
|
+
list_ratio_as_array[current_list_ratio_index]
|
109
130
|
end
|
110
131
|
|
111
132
|
def set_next_as_current!
|
112
|
-
next_index =
|
113
|
-
if (next_index) ==
|
133
|
+
next_index = current_list_ratio_index + 1
|
134
|
+
if (next_index) == list_ratio_as_array.length
|
114
135
|
# If we have iterated through all the list_ratios, then we reset
|
115
|
-
|
136
|
+
reset!
|
116
137
|
else
|
117
138
|
self.current_list_ratio_index = next_index
|
118
139
|
end
|
@@ -124,11 +145,11 @@ module Qfill
|
|
124
145
|
end
|
125
146
|
|
126
147
|
def is_full?
|
127
|
-
self.
|
148
|
+
self.total_count >= max
|
128
149
|
end
|
129
150
|
|
130
151
|
def to_s
|
131
|
-
"Qfill::Result: ratio: #{
|
152
|
+
"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}"
|
132
153
|
end
|
133
154
|
end
|
134
155
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qfill
|
4
|
+
# A Strategy defines how to process the elements in the queues
|
5
|
+
module Strategy
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'qfill/strategy/base'
|
10
|
+
require 'qfill/strategy/drain_to_empty'
|
11
|
+
require 'qfill/strategy/drain_to_limit'
|
12
|
+
require 'qfill/strategy/sample'
|
13
|
+
require 'qfill/strategy/time_slice'
|
@@ -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
|