qfill 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +8 -0
  3. data/.github/workflows/style.yml +37 -0
  4. data/.github/workflows/test.yml +55 -0
  5. data/.rspec +3 -2
  6. data/.rubocop.yml +26 -0
  7. data/.rubocop_todo.yml +163 -0
  8. data/.simplecov +6 -0
  9. data/CODE_OF_CONDUCT.md +133 -0
  10. data/Gemfile +33 -0
  11. data/Guardfile +12 -0
  12. data/{LICENSE.txt → LICENSE} +1 -1
  13. data/README.md +2 -2
  14. data/Rakefile +24 -1
  15. data/lib/qfill.rb +13 -10
  16. data/lib/qfill/errors/invalid_index.rb +8 -0
  17. data/lib/qfill/filter.rb +5 -3
  18. data/lib/qfill/list.rb +4 -2
  19. data/lib/qfill/list_set.rb +13 -9
  20. data/lib/qfill/manager.rb +78 -124
  21. data/lib/qfill/origin.rb +4 -3
  22. data/lib/qfill/popper.rb +30 -25
  23. data/lib/qfill/pusher.rb +33 -22
  24. data/lib/qfill/result.rb +63 -42
  25. data/lib/qfill/strategy.rb +13 -0
  26. data/lib/qfill/strategy/base.rb +65 -0
  27. data/lib/qfill/strategy/drain_to_empty.rb +73 -0
  28. data/lib/qfill/strategy/drain_to_limit.rb +46 -0
  29. data/lib/qfill/strategy/sample.rb +42 -0
  30. data/lib/qfill/strategy/time_slice.rb +106 -0
  31. data/lib/qfill/version.rb +3 -1
  32. data/maintenance-branch +1 -0
  33. data/qfill.gemspec +15 -13
  34. data/spec/qfill/filter_spec.rb +35 -26
  35. data/spec/qfill/list_set_spec.rb +28 -23
  36. data/spec/qfill/list_spec.rb +35 -27
  37. data/spec/qfill/manager_spec.rb +670 -434
  38. data/spec/qfill/origin_spec.rb +45 -35
  39. data/spec/qfill/popper_spec.rb +36 -30
  40. data/spec/qfill/pusher_spec.rb +32 -26
  41. data/spec/qfill/result_spec.rb +49 -38
  42. data/spec/qfill_spec.rb +6 -5
  43. data/spec/spec_helper.rb +11 -38
  44. data/spec/support/helper.rb +13 -0
  45. data/spec/support/random_object.rb +30 -0
  46. metadata +52 -23
data/lib/qfill/origin.rb CHANGED
@@ -1,4 +1,6 @@
1
- #Qfill::Origin.new(:name => "High List",
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
- !!self.backfill
17
+ !!backfill
16
18
  end
17
-
18
19
  end
19
20
  end
data/lib/qfill/popper.rb CHANGED
@@ -1,4 +1,10 @@
1
- #popper = Qfill::Popper.new(
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 = get_total_elements
52
+ @total_elements = count_all_elements
40
53
  end
41
54
 
42
55
  def primary
43
- @primary ||= self.queues.select {|x| x.backfill != true}
56
+ @primary ||= queues.reject { |x| x.backfill == true }
44
57
  end
45
58
 
46
59
  def current_list
47
- self.primary[self.current_index]
60
+ primary[current_index]
48
61
  end
49
62
 
50
63
  def set_next_as_current!
51
- next_index = self.current_index + 1
52
- if (next_index) >= self.primary.length
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
- self.reset!
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
- return origin_list.elements.pop(n)
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
- return result
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
- self.count_primary_elements == 0
90
+ count_primary_elements.zero?
85
91
  end
86
92
 
87
93
  def totally_empty?
88
- self.get_total_elements == 0
94
+ count_all_elements.zero?
89
95
  end
90
96
 
91
97
  def count_primary_elements
92
- self.primary.inject(0) {|counter, queue| counter += queue.elements.length}
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
- #pusher = Qfill::Pusher.new(
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 = self.queues.map {|x| x.ratio}.compact
46
- ratio_to_split = (1 - with_ratio.inject(0, :+))
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 = self.queues.length - with_ratio.length
51
- if num_without_ratio > 0 && ratio_to_split <= 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
- self.queues.each do |queue|
54
- if queue.ratio.nil?
55
- queue.tap do |q|
56
- q.ratio = equal_portion
57
- end
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
- self.queues[self.current_index]
69
+ queues[current_index]
65
70
  end
66
71
 
67
72
  def set_next_as_current!
68
- next_index = self.current_index + 1
69
- if (next_index) == self.queues.length
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
- self.reset!
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
- !self.queues.select {|x| !x.is_full?}.empty?
90
+ !queues.reject(&:is_full?).empty?
86
91
  end
87
92
 
88
93
  def next_to_fill
89
- self.queues.select {|x| !x.is_full?}.first
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
- # :preferred is used for :draim_to_empty
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, :list_ratios, :fill_tracker, :fill_count, :current_count, :validate, :current_list_ratio_index, :max,
15
- :strategy, :shuffle, :preferred, :preferred_potential, :preferred_potential_ratio, :max_tracker
16
-
17
- def self.get_limit_from_max_and_ratio(all_list_max, 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)
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 == 0
22
- limit += 1
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 = self.list_ratio_as_array.map {|tuple| tuple[1]}.compact
31
- ratio_leftover = (1 - with_ratio.inject(0, :+))
32
- if ratio_leftover < 0
33
- raise ArgumentError, "#{self.class}: invalid list_ratios for queue '#{self.name}'. List Ratios (#{with_ratio.join(' + ')}) must not total more than 1"
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
- @fill_count = 0
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 = self.use_validation?
61
+ @validate = use_validation?
48
62
  end
49
63
 
50
64
  def list_ratio_full?(list_name, max_from_list)
51
- self.fill_tracker[list_name] >= max_from_list
65
+ fill_tracker[list_name] >= max_from_list
52
66
  end
53
67
 
54
68
  def push(objects, list_name)
55
- self.validate!(list_name)
69
+ validate!(list_name)
56
70
  added = 0
57
- self.fill_tracker[list_name] ||= 0
71
+ fill_tracker[list_name] ||= 0
58
72
  objects.each do |object|
59
- if self.allow?(object, list_name)
60
- self.bump_fill_tracker!(list_name)
61
- self.add!(object)
62
- added += 1
63
- #self.print(list_name)
64
- end
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
- return added
84
+ added
67
85
  end
68
86
 
69
87
  def print(list_name)
70
- puts "Added to #{list_name}.\nResult List #{self.name} now has #{self.elements.length} total objects.\nSources:\n #{self.fill_tracker.inspect} "
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
- self.elements << object
92
+ elements << object
75
93
  end
76
94
 
77
95
  def allow?(object, list_name)
78
- !self.filter.respond_to?(:call) ||
96
+ !filter.respond_to?(:call) ||
79
97
  # If there is a filter, then it must return true to proceed
80
- self.filter.run(object, list_name)
98
+ filter.run(object, list_name)
81
99
  end
82
100
 
83
101
  def bump_fill_tracker!(list_name)
84
- self.fill_tracker[list_name] += 1
85
- self.fill_count += 1
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
- self.list_ratios.has_key?(list_name)
109
+ list_ratios.key?(list_name)
92
110
  end
93
111
 
94
112
  def validate!(list_name)
95
- raise ArgumentError, "#{self.class}: #{list_name} is an invalid list_name. Valid list_names are: #{self.list_ratios.keys}" if self.validate && !self.valid?(list_name)
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
- !self.list_ratios.empty?
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 ||= self.list_ratios.to_a
125
+ @list_ratio_as_array ||= list_ratios.to_a
105
126
  end
106
127
 
107
128
  def current_list_ratio
108
- self.list_ratio_as_array[self.current_list_ratio_index]
129
+ list_ratio_as_array[current_list_ratio_index]
109
130
  end
110
131
 
111
132
  def set_next_as_current!
112
- next_index = self.current_list_ratio_index + 1
113
- if (next_index) == self.list_ratio_as_array.length
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
- self.reset!
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.current_count >= self.max
148
+ self.total_count >= max
128
149
  end
129
150
 
130
151
  def to_s
131
- "Qfill::Result: ratio: #{self.ratio}, list_ratios: #{self.list_ratios}, fill_tracker: #{self.fill_tracker}, fill_count: #{self.fill_count}, current_count: #{self.current_count}, filter: #{!!self.filter ? 'Yes' : 'No'}, current_list_ratio_index: #{self.current_list_ratio_index}, max: #{self.max}"
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