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