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.
- 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/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
|
data/{LICENSE.txt → LICENSE}
RENAMED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Qfill - Advanced Queue
|
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
|
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
|
-
|
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
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
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 =
|
15
|
+
VERBOSE = ENV['QFILL_VERBOSE'] == 'true'
|
13
16
|
end
|
data/lib/qfill/filter.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
#
|
2
|
-
|
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
|
-
|
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
|
data/lib/qfill/list_set.rb
CHANGED
@@ -1,35 +1,39 @@
|
|
1
|
-
#
|
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
|
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
|
-
|
21
|
+
queues.find { |queue| queue.name == key }
|
18
22
|
end
|
19
23
|
|
20
24
|
def index_of(queue_name)
|
21
|
-
index =
|
25
|
+
index = queues.index { |queue| queue.name == queue_name }
|
22
26
|
return index if index
|
23
|
-
|
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
|
31
|
-
|
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
|
-
#
|
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
|
-
|
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 = [
|
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
|
-
|
18
|
-
|
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
|
-
|
27
|
-
@
|
28
|
-
|
29
|
-
|
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? && !
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
87
|
+
fill_according_to_list_ratios!
|
67
88
|
end
|
68
89
|
end
|
69
90
|
|
70
91
|
def remaining_to_fill
|
71
|
-
|
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
|
-
|
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
|
-
|
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
|
155
|
-
|
105
|
+
def is_full?
|
106
|
+
fill_count >= all_list_max
|
156
107
|
end
|
157
108
|
|
158
|
-
def
|
159
|
-
|
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
|
-
"[#{
|
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
|