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
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
|