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