qfill 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in qfill.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Peter Boling
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,152 @@
1
+ # Qfill - Advanced Queue Tranformations
2
+
3
+ This gem takes a dynamic number of queues (arrays) of things, and manages the transformation into a new set of queues,
4
+ according to a dynamic set of guidelines.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'qfill'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install qfill
19
+
20
+ ## Usage & List Fill Methodology
21
+
22
+ There will be a dynamic number of origination queues each containing a set of snapshots which are grouped together by
23
+ some matching criteria.
24
+ There is a Popper which is called to pop the next object from the next origination queue.
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.
27
+
28
+ Example:
29
+
30
+ filter1 = Qfill::Filter.new( -> (object, stuff, stank) { object.is_awesome_enough_to_be_in_results?(stuff, stank) }, stuff, stank)
31
+ filter2 = Qfill::Filter.new( -> (object, rank, bank) { object.is_awesome_enough_to_be_in_results?(rank, bank) }, rank, bank)
32
+
33
+ popper = Qfill::Popper.new(
34
+ Qfill::Origin.new( :name => "High List",
35
+ :elements => [Thing1, Thing3],
36
+ :backfill => "Medium List",
37
+ :filter => filter1),
38
+ Qfill::Origin.new( :name => "Medium List",
39
+ :elements => [Thing2, Thing6],
40
+ :backfill => "Low List",
41
+ :filter => filter2),
42
+ Qfill::Origin.new( :name => "Low List",
43
+ :elements => [Thing4, Thing5],
44
+ :backfill => nil,
45
+ :filter => filter1),
46
+ )
47
+
48
+ Or:
49
+
50
+ popper = Qfill::Popper.from_array_of_hashes([
51
+ { :name => "High List",
52
+ :elements => [Thing1, Thing3, Thing7, Thing8, Thing12, Thing15, Thing17],
53
+ :backfill => "Medium List",
54
+ :filter => filter1},
55
+ { :name => "Medium List",
56
+ :elements => [Thing2, Thing6, Thing11, Thing 16],
57
+ :backfill => "Low List",
58
+ :filter => filter2},
59
+ { :name => "Low List",
60
+ :elements => [Thing4, Thing5, Thing9, Thing10, Thing13, Thing14, Thing18, Thing19, Thing20],
61
+ :backfill => false,
62
+ :filter => filter1}
63
+ ])
64
+
65
+ There are a dynamic number of result queues that need to be filled with objects from the origination queues.
66
+ There is a Pusher which is called to add the object from the Popper to the next result queue.
67
+ A filter can be given to perform additional check to verify that the object should be added to a particular result queue.
68
+ At least one result queue should be left with no filter, or you risk a result set that is completely empty.
69
+ A filter_alternate can be given to indicate which alternate result queue objects failing the filter should be placed in.
70
+ A ratio can be given to indicate the portion of the total results which should go into the result queue.
71
+ A set of queue ratios can be defined to indicate the rate at which the result queue will be filled from each origin queue.
72
+ When queue ratios are not given an even split is assumed.
73
+
74
+ Example:
75
+
76
+ filter3 = Qfill::Filter.new( -> (object, stuff, stank) { object.can_be_best_results?(stuff, stank) }, stuff, stank)
77
+
78
+ pusher = Qfill::Pusher.new(
79
+ Qfill::Result.new( :name => "Best Results",
80
+ :filter => filter3,
81
+ :ratio => 0.5,
82
+ :list_ratios => {
83
+ "High List" => 0.4,
84
+ "Medium List" => 0.2,
85
+ "Low List" => 0.4
86
+ }
87
+ ),
88
+ Qfill::Result.new( :name => "More Results",
89
+ :ratio => 0.5,
90
+ :list_ratios => {
91
+ "High List" => 0.2,
92
+ "Medium List" => 0.4,
93
+ "Low List" => 0.4
94
+ }
95
+ )
96
+ )
97
+
98
+ Or:
99
+
100
+ pusher = Qfill::Pusher.from_array_of_hashes([
101
+ { :name => "First Result",
102
+ :ratio => 0.125,
103
+ :filter => filter3,
104
+ :ratios => {
105
+ "High List" => 0.4,
106
+ "Medium List" => 0.2,
107
+ "Low List" => 0.4
108
+ }
109
+ },
110
+ { :name => "Second Result",
111
+ :ratio => 0.25 },
112
+ { :name => "Third Result",
113
+ :ratio => 0.125 },
114
+ { :name => "Fourth Result",
115
+ :ratio => 0.50 },
116
+ ])
117
+
118
+ There is a Manager which maintains state: always knows which queue to pop from next, and which queue to push onto next.
119
+
120
+ manager = Qfill::Manager.new(
121
+ :all_list_max => 40,
122
+ :popper => popper,
123
+ :pusher => pusher,
124
+ )
125
+ manager.fill!
126
+
127
+ For the best usage please look in `spec/qfill/manager_spec.rb` and the other spec files.
128
+
129
+ ## Contributing
130
+
131
+ 1. Fork it
132
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
133
+ 3. Commit your changes (`git commit -am ‘Added some feature’`)
134
+ 4. Push to the branch (`git push origin my-new-feature`)
135
+ 5. Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.
136
+ 6. Create new Pull Request
137
+
138
+ ## Versioning
139
+
140
+ This library aims to adhere to [Semantic Versioning 2.0.0][semver].
141
+ Violations of this scheme should be reported as bugs. Specifically,
142
+ if a minor or patch version is released that breaks backward
143
+ compatibility, a new version should be immediately released that
144
+ restores compatibility. Breaking changes to the public API will
145
+ only be introduced with new major versions.
146
+ As a result of this policy, you can (and should) specify a
147
+ dependency on this gem using the [Pessimistic Version Constraint][pvc] with two digits of precision.
148
+ For example:
149
+ spec.add_dependency 'qfill', '~> 0.0'
150
+
151
+ [semver]: http://semver.org/
152
+ [pvc]: http://docs.rubygems.org/read/chapter/16#page74
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,13 @@
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"
10
+
11
+ module Qfill
12
+ # Your code goes here...
13
+ end
@@ -0,0 +1,18 @@
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)
3
+ #
4
+ # 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
+ module Qfill
6
+ class Filter
7
+ attr_accessor :processor, :processor_arguments
8
+
9
+ def initialize(proc, *params)
10
+ @processor = proc
11
+ @processor_arguments = params
12
+ end
13
+
14
+ def run(*args)
15
+ self.processor.call(*args, *self.processor_arguments)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # This is the base queue class for Origin queues and Result queues.
2
+ #
3
+ #Qfill::List.new(:name => "High List",
4
+ # :elements => [Thing1, Thing3],
5
+ # :filter => filter1),
6
+ module Qfill
7
+ class List
8
+ attr_accessor :name, :elements, :filter
9
+
10
+ def initialize(options = {})
11
+ raise ArgumentError, "Missing required option :name for #{self.class}.new()" unless options && options[:name]
12
+ @name = options[:name]
13
+ @elements = options[:elements] || []
14
+ @filter = options[:filter]
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ #popper = Qfill::ListSet.new(
2
+ # Qfill::List.new( :name => "High List",
3
+ # :elements => [Thing1, Thing3],
4
+ # :filter => filter1 ) )
5
+ module Qfill
6
+ class ListSet
7
+
8
+ attr_accessor :queues, :current_index
9
+
10
+ def initialize(*args)
11
+ raise ArgumentError, "Missing required arguments for #{self.class}.new(queues)" unless args.length > 0
12
+ @queues = args
13
+ @current_index = 0
14
+ end
15
+
16
+ def [](key)
17
+ return self.queues.find { |queue| queue.name == key }
18
+ end
19
+
20
+ def reset!
21
+ self.current_index = 0
22
+ end
23
+
24
+ def get_total_elements
25
+ self.queues.inject(0) {|counter, queue| counter += queue.elements.length}
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,91 @@
1
+ #Qfill::Manager.new(
2
+ # :all_list_max => 40,
3
+ # :popper => popper,
4
+ # :pusher => pusher,
5
+ #)
6
+ module Qfill
7
+ class Manager
8
+ attr_accessor :all_list_max, :popper, :pusher, :fill_count, :strategy
9
+
10
+ STRATEGY_OPTIONS = [:drain, :sample]
11
+
12
+ 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
+ unless options[:strategy].nil? || STRATEGY_OPTIONS.include?(options[:strategy])
17
+ raise ArgumentError, "#{self.class}: strategy is optional, but must be one of #{STRATEGY_OPTIONS.inspect} if provided"
18
+ end
19
+ @popper = options[:popper]
20
+ @pusher = options[:pusher]
21
+ # Provided by user, or defaults to the total number of primary elements in popper list set
22
+ @all_list_max = options[:all_list_max] ? [options[:all_list_max], self.popper.get_primary_elements].min : self.popper.get_primary_elements
23
+ @fill_count = 0
24
+ @strategy = options[:strategy] || :drain # or :sample
25
+ end
26
+
27
+ def fill!
28
+ while !is_full? && !self.popper.primary_empty? && (result = self.pusher.current_list)
29
+ self.fill_to_ratio!(result, self.all_list_max)
30
+ self.pusher.set_next_as_current!
31
+ end
32
+ end
33
+
34
+ def fill_to_ratio!(result, all_list_max)
35
+ result.max = Qfill::Result.get_limit_from_max_and_ratio(all_list_max, result.ratio)
36
+ if !result.list_ratios.empty?
37
+ self.fill_according_to_list_ratios!(result)
38
+ else
39
+ self.fill_up_to_ratio!(result)
40
+ end
41
+ end
42
+
43
+ def fill_according_to_list_ratios!(result)
44
+ added = 0
45
+ if self.strategy == :drain
46
+ result.list_ratios.each do |list_name, list_ratio|
47
+ #puts "fill_according_to_list_ratios!, :drain, #{list_name}: Primary remaining => #{self.popper.get_primary_elements}"
48
+ max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio)
49
+ array_to_push = self.popper.next_objects!(list_name, max_from_list)
50
+ added = result.push(array_to_push, list_name)
51
+ end
52
+ self.fill_count += added
53
+ elsif self.strategy == :sample
54
+ while !is_full? && !result.is_full? && !self.popper.totally_empty? && (list_ratio_tuple = result.current_list_ratio)
55
+ #puts "fill_according_to_list_ratios!, :sample, #{list_ratio_tuple[0]}: Primary remaining => #{self.popper.get_primary_elements}"
56
+ max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, list_ratio_tuple[1])
57
+ array_to_push = self.popper.next_objects!(list_ratio_tuple[0], max_from_list)
58
+ added = result.push(array_to_push, list_ratio_tuple[0])
59
+ self.fill_count += added
60
+ result.set_next_as_current!
61
+ end
62
+ end
63
+ end
64
+
65
+ def fill_up_to_ratio!(result)
66
+ ratio = 1.0 / self.popper.primary.length
67
+ max_from_list = Qfill::Result.get_limit_from_max_and_ratio(result.max, ratio)
68
+ added = 0
69
+ if self.strategy == :drain
70
+ self.popper.primary.each do |queue|
71
+ #puts "fill_up_to_ratio!, :drain max #{max_from_list}, #{queue.name}: Primary remaining => #{self.popper.get_primary_elements}"
72
+ array_to_push = self.popper.next_objects!(queue.name, max_from_list)
73
+ added = result.push(array_to_push, queue.name)
74
+ end
75
+ self.fill_count += added
76
+ elsif self.strategy == :sample
77
+ while !is_full? && !result.is_full? && !self.popper.totally_empty? && (origin_list = self.popper.current_list)
78
+ #puts "fill_up_to_ratio!, :sample max #{max_from_list}, #{origin_list.name}: Primary remaining => #{self.popper.get_primary_elements}"
79
+ array_to_push = self.popper.next_objects!(origin_list.name, max_from_list)
80
+ added = result.push(array_to_push, origin_list.name)
81
+ self.fill_count += added
82
+ self.popper.set_next_as_current!
83
+ end
84
+ end
85
+ end
86
+
87
+ def is_full?
88
+ self.fill_count >= self.all_list_max
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,19 @@
1
+ #Qfill::Origin.new(:name => "High List",
2
+ # :elements => [Thing1, Thing3],
3
+ # :backfill => "Medium List",
4
+ # :filter => filter1),
5
+ module Qfill
6
+ class Origin < Qfill::List
7
+ attr_accessor :backfill
8
+
9
+ def initialize(options = {})
10
+ super(options)
11
+ @backfill = options[:backfill]
12
+ end
13
+
14
+ def has_backfill?
15
+ !!self.backfill
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,96 @@
1
+ #popper = Qfill::Popper.new(
2
+ # Qfill::Origin.new( :name => "High List",
3
+ # :elements => [Thing1, Thing3],
4
+ # :backfill => "Medium List",
5
+ # :filter => filter1),
6
+ # Qfill::Origin.new( :name => "Medium List",
7
+ # :elements => [Thing2, Thing6],
8
+ # :backfill => "Low List",
9
+ # :filter => filter2),
10
+ # Qfill::Origin.new( :name => "Low List",
11
+ # :elements => [Thing4, Thing5],
12
+ # :backfill => nil,
13
+ # :filter => filter1),
14
+ #)
15
+ #
16
+ #popper = Qfill::Popper.from_array_of_hashes([
17
+ # { :name => "High List",
18
+ # :elements => [Thing1, Thing3, Thing7, Thing8, Thing12, Thing15, Thing17],
19
+ # :backfill => "Medium List",
20
+ # :filter => filter1},
21
+ # { :name => "Medium List",
22
+ # :elements => [Thing2, Thing6, Thing11, Thing 16],
23
+ # :backfill => "Low List",
24
+ # :filter => filter2},
25
+ # { :name => "Low List",
26
+ # :elements => [Thing4, Thing5, Thing9, Thing10, Thing13, Thing14, Thing18, Thing19, Thing20],
27
+ # :backfill => nil,
28
+ # :filter => filter1},
29
+ #])
30
+ #
31
+ # Popper is made up of an array (called queues) of Origin objects.
32
+ module Qfill
33
+ class Popper < Qfill::ListSet
34
+
35
+ attr_accessor :total_elements
36
+
37
+ def initialize(*args)
38
+ super(*args)
39
+ @total_elements = get_total_elements
40
+ end
41
+
42
+ def primary
43
+ @primary ||= self.queues.select {|x| x.backfill != true}
44
+ end
45
+
46
+ def current_list
47
+ self.primary[self.current_index]
48
+ end
49
+
50
+ def set_next_as_current!
51
+ next_index = self.current_index + 1
52
+ if (next_index) == self.primary.length
53
+ # If we have iterated through all the queues, then we reset
54
+ self.reset!
55
+ else
56
+ self.current_index = next_index
57
+ end
58
+ end
59
+
60
+ def next_objects!(list_name, n = 1)
61
+ origin_list = self[list_name]
62
+ if origin_list.elements.length >= n
63
+ return origin_list.elements.pop(n)
64
+ else
65
+ result = origin_list.elements.pop(n)
66
+ while result.length < n && origin_list.has_backfill?
67
+ secondary_list = self[origin_list.backfill]
68
+ remaining = n - result.length
69
+ result += secondary_list.elements.pop(remaining)
70
+ origin_list = secondary_list
71
+ end
72
+ return result
73
+ end
74
+ end
75
+
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
+ def primary_empty?
84
+ self.get_primary_elements == 0
85
+ end
86
+
87
+ def totally_empty?
88
+ self.get_total_elements == 0
89
+ end
90
+
91
+ def get_primary_elements
92
+ self.primary.inject(0) {|counter, queue| counter += queue.elements.length}
93
+ end
94
+
95
+ end
96
+ end