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/Gemfile CHANGED
@@ -1,4 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in qfill.gemspec
4
6
  gemspec
7
+
8
+ ruby_version = Gem::Version.new(RUBY_VERSION)
9
+
10
+ gem 'yard', '~> 0.9.24', require: false
11
+
12
+ ### deps for rdoc.info
13
+ group :documentation do
14
+ gem 'github-markup', platform: :mri
15
+ gem 'redcarpet', platform: :mri
16
+ end
17
+
18
+ group :development, :test do
19
+ if ruby_version >= Gem::Version.new('2.4')
20
+ # No need to run byebug / pry on earlier versions
21
+ gem 'byebug', platform: :mri
22
+ gem 'pry', platform: :mri
23
+ gem 'pry-byebug', platform: :mri
24
+ end
25
+
26
+ if ruby_version >= Gem::Version.new('2.7')
27
+ # No need to run rubocop or simplecov on earlier versions
28
+ gem 'rubocop', '~> 1.9', platform: :mri
29
+ gem 'rubocop-md', platform: :mri
30
+ gem 'rubocop-packaging', platform: :mri
31
+ gem 'rubocop-performance', platform: :mri
32
+ gem 'rubocop-rake', platform: :mri
33
+ gem 'rubocop-rspec', platform: :mri
34
+
35
+ gem 'simplecov', '~> 0.21', platform: :mri
36
+ end
37
+ end
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard 'rspec', version: 2 do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
+ watch('spec/spec_helper.rb') { 'spec' }
7
+ end
8
+
9
+ guard 'bundler' do
10
+ watch('Gemfile')
11
+ watch(/^.+\.gemspec/)
12
+ end
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Peter Boling
1
+ Copyright (c) 2013-2021 Peter Boling
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Qfill - Advanced Queue Tranformations
1
+ # Qfill - Advanced Queue Transformations
2
2
 
3
3
  This gem takes a dynamic number of queues (arrays) of things, and manages the transformation into a new set of queues,
4
4
  according to a dynamic set of guidelines.
@@ -23,7 +23,7 @@ There will be a dynamic number of origination queues each containing a set of sn
23
23
  some matching criteria.
24
24
  There is a Popper which is called to pop the next object from the next origination queue.
25
25
  There is a Filter which is optionally called to validate any object that is popped from the origin.
26
- Origin keeps popping until an object is validated for as a result-worthy object.
26
+ Origin keeps popping until an object is validated as a result-worthy object.
27
27
 
28
28
  Example:
29
29
 
data/Rakefile CHANGED
@@ -1 +1,24 @@
1
- require "bundler/gem_tasks"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ begin
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ rescue LoadError
9
+ task :spec do
10
+ warn 'RSpec is disabled'
11
+ end
12
+ end
13
+ task test: :spec
14
+
15
+ begin
16
+ require 'rubocop/rake_task'
17
+ RuboCop::RakeTask.new
18
+ rescue LoadError
19
+ task :rubocop do
20
+ warn 'RuboCop is disabled'
21
+ end
22
+ end
23
+
24
+ task default: [:test]
data/lib/qfill.rb CHANGED
@@ -1,13 +1,16 @@
1
- require "qfill/version"
2
- require "qfill/filter"
3
- require "qfill/list"
4
- require "qfill/list_set"
5
- require "qfill/origin"
6
- require "qfill/result"
7
- require "qfill/popper"
8
- require "qfill/pusher"
9
- require "qfill/manager"
1
+ # frozen_string_literal: true
2
+
3
+ require 'qfill/version'
4
+ require 'qfill/filter'
5
+ require 'qfill/list'
6
+ require 'qfill/list_set'
7
+ require 'qfill/origin'
8
+ require 'qfill/popper'
9
+ require 'qfill/pusher'
10
+ require 'qfill/manager'
11
+ require 'qfill/result'
12
+ require 'qfill/strategy'
10
13
 
11
14
  module Qfill
12
- VERBOSE = false
15
+ VERBOSE = ENV['QFILL_VERBOSE'] == 'true'
13
16
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qfill
4
+ module Errors
5
+ class InvalidIndex < StandardError
6
+ end
7
+ end
8
+ end
data/lib/qfill/filter.rb CHANGED
@@ -1,5 +1,7 @@
1
- #filter1 = Qfill::Filter.new( -> (object, stuff, stank) { object.is_awesome_enough_to_be_in_results?(stuff, stank) }, stuff, stank)
2
- #filter2 = Qfill::Filter.new( -> (object, rank, bank) { object.is_awesome_enough_to_be_in_results?(rank, bank) }, rank, bank)
1
+ # frozen_string_literal: true
2
+
3
+ # filter1 = Qfill::Filter.new( -> (object, stuff, stank) { object.is_awesome_enough_to_be_in_results?(stuff, stank) }, stuff, stank)
4
+ # filter2 = Qfill::Filter.new( -> (object, rank, bank) { object.is_awesome_enough_to_be_in_results?(rank, bank) }, rank, bank)
3
5
  #
4
6
  # Filters are destructive. If an item is filtered from a Result list it is lost, since it has already been popped off the origin list, and won't be coming back
5
7
  module Qfill
@@ -12,7 +14,7 @@ module Qfill
12
14
  end
13
15
 
14
16
  def run(*args)
15
- self.processor.call(*args, *self.processor_arguments)
17
+ processor.call(*args, *processor_arguments)
16
18
  end
17
19
  end
18
20
  end
data/lib/qfill/list.rb CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This is the base queue class for Origin queues and Result queues.
2
4
  #
3
- #Qfill::List.new(:name => "High List",
5
+ # Qfill::List.new(:name => "High List",
4
6
  # :elements => [Thing1, Thing3],
5
7
  # :filter => filter1),
6
8
  module Qfill
@@ -9,10 +11,10 @@ module Qfill
9
11
 
10
12
  def initialize(options = {})
11
13
  raise ArgumentError, "Missing required option :name for #{self.class}.new()" unless options && options[:name]
14
+
12
15
  @name = options[:name]
13
16
  @elements = options[:elements] || []
14
17
  @filter = options[:filter]
15
18
  end
16
-
17
19
  end
18
20
  end
@@ -1,35 +1,39 @@
1
- #popper = Qfill::ListSet.new(
1
+ # frozen_string_literal: true
2
+
3
+ # This is the base queues class for Popper queues and Pusher queues.
4
+ #
5
+ # popper = Qfill::ListSet.new(
2
6
  # Qfill::List.new( :name => "High List",
3
7
  # :elements => [Thing1, Thing3],
4
8
  # :filter => filter1 ) )
5
9
  module Qfill
6
10
  class ListSet
7
-
8
11
  attr_accessor :queues, :current_index
9
12
 
10
13
  def initialize(*args)
11
- raise ArgumentError, "Missing required arguments for #{self.class}.new(queues)" unless args.length > 0
14
+ raise ArgumentError, "Missing required arguments for #{self.class}.new(queues)" unless args.length.positive?
15
+
12
16
  @queues = args
13
17
  @current_index = 0
14
18
  end
15
19
 
16
20
  def [](key)
17
- return self.queues.find { |queue| queue.name == key }
21
+ queues.find { |queue| queue.name == key }
18
22
  end
19
23
 
20
24
  def index_of(queue_name)
21
- index = self.queues.index { |queue| queue.name == queue_name }
25
+ index = queues.index { |queue| queue.name == queue_name }
22
26
  return index if index
23
- raise "Fuck #{index} index is missing for #{queue_name}"
27
+
28
+ raise Qfill::Errors::InvalidIndex, "Cannot locate index of #{queue_name}"
24
29
  end
25
30
 
26
31
  def reset!
27
32
  self.current_index = 0
28
33
  end
29
34
 
30
- def get_total_elements
31
- self.queues.inject(0) {|counter, queue| counter += queue.elements.length}
35
+ def count_all_elements
36
+ queues.inject(0) { |counter, queue| counter += queue.elements.length }
32
37
  end
33
-
34
38
  end
35
39
  end
data/lib/qfill/manager.rb CHANGED
@@ -1,166 +1,120 @@
1
- #Qfill::Manager.new(
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
- attr_accessor :all_list_max, :popper, :pusher, :fill_count, :strategy, :result
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 = [:drain_to_limit, :drain_to_empty, :sample]
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
- if options[:strategy] == :drain
18
- warn "Qfill strategy :drain has been renamed :drain_to_limit, please update your code."
19
- options[:strategy] = :drain_to_limit
20
- else
21
- raise ArgumentError, "#{self.class}: strategy is optional, but must be one of #{STRATEGY_OPTIONS.inspect} if provided"
22
- end
33
+ raise ArgumentError,
34
+ "#{self.class}: strategy is optional, but must be one of #{STRATEGY_OPTIONS.inspect} if provided"
23
35
  end
36
+
37
+ @fill_count = 0
38
+
24
39
  @popper = options[:popper]
25
40
  @pusher = options[:pusher]
26
- # Provided by user, or defaults to the total number of primary elements in popper list set
27
- @all_list_max = options[:all_list_max] ? [options[:all_list_max], self.popper.count_primary_elements].min : self.popper.count_primary_elements
28
- @fill_count = 0
29
- @strategy = options[:strategy] || :drain_to_limit # or :drain_to_empty or :sample
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
30
71
  end
31
72
 
32
73
  def fill!
33
- while !is_full? && !self.popper.primary_empty? && (self.result = self.pusher.current_list)
34
- if self.current_strategy == :drain_to_empty
35
- preferred_potential_ratio = 0
36
- preferred_potential = 0
37
- self.result.list_ratios.each do |list_name, list_ratio|
38
- poppy = self.result.preferred.select {|x| x == list_name}
39
- if poppy
40
- preferred_potential_ratio += list_ratio
41
- num = self.popper[list_name].elements.length
42
- preferred_potential += num
43
- self.result.max_tracker[list_name] = num
44
- end
45
- end
46
- self.result.preferred_potential = preferred_potential
47
- self.result.preferred_potential_ratio = preferred_potential_ratio
48
- end
49
- self.fill_to_ratio!
50
- self.pusher.set_next_as_current!
51
- self.result.elements.shuffle! if self.result.shuffle
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
52
79
  end
53
80
  end
54
81
 
55
82
  def fill_to_ratio!
56
- case self.current_strategy
57
- when :drain_to_empty then
58
- result.max = self.result.preferred_potential_ratio > 0 ? [(self.result.preferred_potential / self.result.preferred_potential_ratio), self.remaining_to_fill].min : self.remaining_to_fill
59
- when :drain_to_limit, :sample then
60
- result.max = Qfill::Result.get_limit_from_max_and_ratio(self.remaining_to_fill, result.ratio)
61
- end
62
- #result.max = Qfill::Result.get_limit_from_max_and_ratio(self.all_list_max, result.ratio)
63
- if !result.list_ratios.empty?
64
- self.fill_according_to_list_ratios!
83
+ strategy.result_max!
84
+ if result.list_ratios.empty?
85
+ fill_up_to_ratio!
65
86
  else
66
- self.fill_up_to_ratio!
87
+ fill_according_to_list_ratios!
67
88
  end
68
89
  end
69
90
 
70
91
  def remaining_to_fill
71
- self.all_list_max - self.fill_count
92
+ primary_list_total - fill_count
72
93
  end
73
94
 
74
95
  # Go through the queues this result should be filled from and push elements from them onto the current result list.
75
96
  def fill_according_to_list_ratios!
76
- added = 0
77
- tally = 0
78
- ratio_modifier = 1
79
- case self.current_strategy
80
- when :drain_to_empty then
81
- # Are there any elements in preferred queues that we should add?
82
- if self.result.preferred_potential > 0
83
- # Setup a ratio modifier for the non-preferred queues
84
- result.list_ratios.each do |list_name, list_ratio|
85
- max_from_list = self.result.max_tracker[list_name] || Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio)
86
- array_to_push = self.popper.next_objects!(list_name, max_from_list)
87
- self.popper.current_index = self.popper.index_of(list_name)
88
- added = result.push(array_to_push, list_name)
89
- puts "[fill_according_to_list_ratios!]#{self}[#{list_name}][added:#{added}]" if Qfill::VERBOSE
90
- tally += added
91
- end
92
- self.fill_count += tally
93
- end
94
- when :drain_to_limit
95
- result.list_ratios.each do |list_name, list_ratio|
96
- max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio)
97
- array_to_push = self.popper.next_objects!(list_name, max_from_list)
98
- self.popper.current_index = self.popper.index_of(list_name)
99
- added = result.push(array_to_push, list_name)
100
- puts "[fill_according_to_list_ratios!]#{self}[#{list_name}][added:#{added}]" if Qfill::VERBOSE
101
- tally += added
102
- end
103
- self.fill_count += tally
104
- when :sample then
105
- #puts "#{!is_full?} && #{result.fill_count} >= #{result.max} && #{!self.popper.totally_empty?} && #{(list_ratio_tuple = result.current_list_ratio)}"
106
- while !is_full? && !result.is_full? && !self.popper.totally_empty? && (list_ratio_tuple = result.current_list_ratio)
107
- max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio_tuple[1])
108
- array_to_push = self.popper.next_objects!(list_ratio_tuple[0], max_from_list)
109
- added = result.push(array_to_push, list_ratio_tuple[0])
110
- self.fill_count += added
111
- puts "[fill_according_to_list_ratios!]#{self}[#{list_ratio_tuple[0]}][added:#{added}]" if Qfill::VERBOSE
112
- result.set_next_as_current!
113
- end
114
- end
97
+ strategy.fill_according_to_list_ratios!
115
98
  end
116
99
 
117
100
  # Go through the primary (non backfill) queues in the popper and push elements from them onto the current result list.
118
101
  def fill_up_to_ratio!
119
- added = 0
120
- tally = 0
121
- if self.current_strategy == :drain_to_empty
122
- self.popper.primary.each do |queue|
123
- array_to_push = self.popper.next_objects!(queue.name, result.max)
124
- added = result.push(array_to_push, queue.name)
125
- self.popper.current_index = self.popper.index_of(queue.name)
126
- puts "[fill_up_to_ratio!]#{self}[Q:#{queue.name}][added:#{added}]" if Qfill::VERBOSE
127
- tally += added
128
- end
129
- self.fill_count += added
130
- else
131
- ratio = 1.0 / self.popper.primary.length # 1 divided by the number of queues
132
- max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, ratio)
133
- if self.current_strategy == :drain_to_limit
134
- self.popper.primary.each do |queue|
135
- array_to_push = self.popper.next_objects!(queue.name, max_from_list)
136
- added = result.push(array_to_push, queue.name)
137
- self.popper.current_index = self.popper.index_of(queue.name)
138
- puts "[fill_up_to_ratio!]#{self}[Q:#{queue.name}][added:#{added}]" if Qfill::VERBOSE
139
- tally += added
140
- end
141
- self.fill_count += tally
142
- elsif self.current_strategy == :sample
143
- while !is_full? && !result.is_full? && !self.popper.totally_empty? && (origin_list = self.popper.current_list)
144
- array_to_push = self.popper.next_objects!(origin_list.name, max_from_list)
145
- added = result.push(array_to_push, origin_list.name)
146
- self.fill_count += added
147
- puts "[fill_up_to_ratio!]#{self}[Added:#{added}][Max List:#{max_from_list}][ratio:#{ratio}][added:#{added}]" if Qfill::VERBOSE
148
- self.popper.set_next_as_current!
149
- end
150
- end
151
- end
102
+ strategy.fill_up_to_ratio!
152
103
  end
153
104
 
154
- def current_strategy
155
- (result.strategy || self.strategy)
105
+ def is_full?
106
+ fill_count >= all_list_max
156
107
  end
157
108
 
158
- def is_full?
159
- self.fill_count >= self.all_list_max
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)
160
114
  end
161
115
 
162
116
  def to_s
163
- "[#{self.current_strategy}][Result Max:#{result.max}][All Max:#{self.all_list_max}][Current Max:#{self.result.max}][Filled:#{self.fill_count}][Primary #:#{self.popper.count_primary_elements}]"
117
+ "[#{strategy_name}][Result Max:#{result.max}][All Max:#{all_list_max}][Current Max:#{result.max}][Filled:#{fill_count}][Primary #:#{popper.count_primary_elements}]"
164
118
  end
165
119
  end
166
120
  end