weighted_average 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ test/test.log
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Seamus Abshere
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,21 @@
1
+ = weighted_average
2
+
3
+ Description goes here.
4
+
5
+ = Rails 3 only
6
+
7
+ We use ARel so we need Rails 3. Please let me know if you want Rails 2, because I an (ungemified) version of that.
8
+
9
+ == Note on Patches/Pull Requests
10
+
11
+ * Fork the project.
12
+ * Make your feature addition or bug fix.
13
+ * Add tests for it. This is important so I don't break it in a
14
+ future version unintentionally.
15
+ * Commit, do not mess with rakefile, version, or history.
16
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
17
+ * Send me a pull request. Bonus points for topic branches.
18
+
19
+ == Copyright
20
+
21
+ Copyright (c) 2010 Seamus Abshere, Andy Rossmeissl, Ian Hough, and Matt Kling. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "weighted_average"
8
+ gem.summary = %Q{Perform weighted averages. Rails 3 only.}
9
+ gem.description = %Q{Perform weighted averages, even across associations. Rails 3 only because it uses ARel.}
10
+ gem.email = "seamus@abshere.net"
11
+ gem.homepage = "http://github.com/seamusabshere/weighted_average"
12
+ gem.authors = ["Seamus Abshere", "Andy Rossmeissl", "Ian Hough", "Matt Kling"]
13
+ gem.add_dependency 'activerecord', '>=3.0.0.beta2'
14
+ gem.add_dependency 'activesupport', '>=3.0.0.beta2'
15
+ gem.add_dependency 'arel', '>=0.3.3'
16
+ gem.add_development_dependency 'cohort_scope', '>=0.0.2'
17
+ gem.add_development_dependency "shoulda", ">= 2.10.3"
18
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'rake/testtask'
26
+ Rake::TestTask.new(:test) do |test|
27
+ test.libs << 'lib' << 'test'
28
+ test.pattern = 'test/**/test_*.rb'
29
+ test.verbose = true
30
+ end
31
+
32
+ begin
33
+ require 'rcov/rcovtask'
34
+ Rcov::RcovTask.new do |test|
35
+ test.libs << 'test'
36
+ test.pattern = 'test/**/test_*.rb'
37
+ test.verbose = true
38
+ end
39
+ rescue LoadError
40
+ task :rcov do
41
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
42
+ end
43
+ end
44
+
45
+ task :test => :check_dependencies
46
+
47
+ task :default => :test
48
+
49
+ require 'rake/rdoctask'
50
+ Rake::RDocTask.new do |rdoc|
51
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
52
+
53
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "weighted_average #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,75 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+
4
+ module ActiveRecord
5
+ module WeightedAverage
6
+ # Returns a number
7
+ def weighted_average(*args)
8
+ connection.select_value(weighted_average_relation(*args).to_sql, 'weighted_average').to_f
9
+ end
10
+
11
+ # Returns the ARel relation
12
+ def weighted_average_relation(column_names, options = {})
13
+ raise ArgumentError, "Only use array form if the weighting column in the foreign table is not called 'weighting'" if options[:weighted_by].is_a?(Array) and options[:weighted_by].length != 2
14
+ raise ArgumentError, "No nil values in weighted_by, please" if Array.wrap(options[:weighted_by]).any?(&:nil?)
15
+
16
+ # aircraft['seats'] or (aircraft['seats'] + aircraft['payload'])
17
+ columns = Array.wrap(column_names).map { |column_name| arel_table[column_name.to_s] }
18
+
19
+ # :airline_aircraft_seat_class
20
+ association = if options[:weighted_by].present?
21
+ options[:weighted_by].is_a?(Array) ? reflect_on_association(options[:weighted_by].first.to_sym) : reflect_on_association(options[:weighted_by].to_sym)
22
+ end
23
+
24
+ # AirlineAircraftSeatClass
25
+ association_class = association.klass if association
26
+
27
+ # AirlineAircraftSeatClass.arel_table
28
+ foreign_arel_table = association_class.arel_table if association_class
29
+
30
+ # set up join ON
31
+ join_on = if association_class
32
+ raise ArgumentError, "#{association.primary_key_name} isn't a column in the #{association_class.table_name} table" unless association_class.column_names.include?(association.primary_key_name)
33
+ foreign_arel_table[association.primary_key_name].eq arel_table[primary_key]
34
+ end
35
+
36
+ # 'weighting'
37
+ weighted_by_column = if association_class and options[:weighted_by].is_a?(Array)
38
+ options[:weighted_by].last.to_s
39
+ elsif !association_class and (options[:weighted_by].is_a?(String) or options[:weighted_by].is_a?(Symbol))
40
+ options[:weighted_by].to_s
41
+ else
42
+ 'weighting'
43
+ end
44
+
45
+ # [foreign_]arel_table['weighting']
46
+ weighted_by = if foreign_arel_table
47
+ foreign_arel_table[weighted_by_column]
48
+ else
49
+ arel_table[weighted_by_column]
50
+ end
51
+
52
+ disaggregate_by = if options[:disaggregate_by].present?
53
+ raise ArgumentError, "Disaggregating by a foreign table isn't supported right now" if options[:disaggregate_by].is_a?(Array)
54
+ arel_table[options[:disaggregate_by].to_s]
55
+ end
56
+
57
+ # FIXME
58
+ # projecting "12345" so that we don't get any other fields back
59
+ if foreign_arel_table
60
+ foreign_arel_table = foreign_arel_table.project('12345')
61
+ end
62
+
63
+ relation = select("(SUM((#{columns.map { |column| column.to_sql }.join(' + ')}) #{"/ #{disaggregate_by.to_sql} " if disaggregate_by}* #{weighted_by.to_sql}) / SUM(#{weighted_by.to_sql})) AS weighted_average")
64
+ columns.each do |column|
65
+ relation = relation.where("#{column.to_sql} IS NOT NULL")
66
+ end
67
+ relation = relation.outer_join(foreign_arel_table).on(join_on) if foreign_arel_table
68
+ relation
69
+ end
70
+ end
71
+ end
72
+
73
+ ActiveRecord::Base.class_eval do
74
+ extend ActiveRecord::WeightedAverage
75
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,220 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'ruby-debug'
5
+ require 'logger'
6
+ require 'cohort_scope'
7
+
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
10
+ require 'weighted_average'
11
+
12
+ class Test::Unit::TestCase
13
+ end
14
+
15
+ $logger = Logger.new STDOUT #'test/test.log'
16
+
17
+ ActiveSupport::Notifications.subscribe do |*args|
18
+ event = ActiveSupport::Notifications::Event.new(*args)
19
+ $logger.debug "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
20
+ end
21
+
22
+ ActiveRecord::Base.establish_connection(
23
+ 'adapter' => 'mysql',
24
+ 'database' => 'weighted_average_test',
25
+ 'username' => 'root',
26
+ 'password' => ''
27
+ )
28
+
29
+ ActiveSupport::Inflector.inflections do |inflect|
30
+ inflect.uncountable %w{aircraft airline_aircraft}
31
+ inflect.uncountable 'commons'
32
+ inflect.uncountable 'food'
33
+ inflect.uncountable 'shelter'
34
+ inflect.uncountable 'transportation'
35
+ inflect.uncountable 'press_coverage'
36
+ inflect.irregular 'foot', 'feet'
37
+ end
38
+
39
+ ActiveRecord::Schema.define(:version => 20090819143429) do
40
+ create_table "segments", :force => true, :options => 'ENGINE=InnoDB default charset=utf8', :id => false do |t|
41
+ t.integer "aircraft_id" # should this be here?
42
+ t.integer "departures_performed"
43
+ t.integer "payload"
44
+ t.integer "seats"
45
+ t.integer "passengers"
46
+ t.integer "freight"
47
+ t.integer "mail"
48
+ t.integer "ramp_to_ramp"
49
+ t.integer "air_time"
50
+ t.float "load_factor"
51
+ t.float "freight_share"
52
+ t.integer "distance"
53
+ t.integer "departures_scheduled"
54
+ t.string "unique_carrier"
55
+ t.integer "dot_airline_id"
56
+ t.string "unique_carrier_name"
57
+ t.string "unique_carrier_entity"
58
+ t.string "region"
59
+ t.string "carrier"
60
+ t.string "carrier_name"
61
+ t.integer "carrier_group"
62
+ t.integer "carrier_group_new"
63
+ t.string "origin_airport_iata"
64
+ t.string "origin_city_name"
65
+ t.integer "origin_city_num"
66
+ t.string "origin_state_abr"
67
+ t.string "origin_state_fips"
68
+ t.string "origin_state_nm"
69
+ t.string "origin_country_iso_3166"
70
+ t.string "origin_country_name"
71
+ t.integer "origin_wac"
72
+ t.string "dest_airport_iata"
73
+ t.string "dest_city_name"
74
+ t.integer "dest_city_num"
75
+ t.string "dest_state_abr"
76
+ t.string "dest_state_fips"
77
+ t.string "dest_state_nm"
78
+ t.string "dest_country_iso_3166"
79
+ t.string "dest_country_name"
80
+ t.integer "dest_wac"
81
+ t.integer "bts_aircraft_group"
82
+ t.integer "bts_aircraft_type"
83
+ t.integer "bts_aircraft_config"
84
+ t.integer "year"
85
+ t.integer "quarter"
86
+ t.integer "month"
87
+ t.integer "bts_distance_group"
88
+ t.string "bts_service_class"
89
+ t.string "data_source"
90
+ t.float "seats_per_departure"
91
+
92
+ t.string 'payload_units'
93
+ t.string 'freight_units'
94
+ t.string 'mail_units'
95
+ t.string 'distance_units'
96
+
97
+ t.datetime "created_at"
98
+ t.datetime "updated_at"
99
+
100
+ t.string "row_hash"
101
+ end
102
+ execute 'ALTER TABLE segments ADD PRIMARY KEY (row_hash);'
103
+
104
+ create_table "aircraft", :force => true do |t|
105
+ t.string "name"
106
+ t.integer "seats"
107
+ t.integer "fuel_type_id"
108
+ t.float "endpoint_fuel"
109
+ t.integer "manufacturer_id"
110
+ t.datetime "updated_at"
111
+ t.datetime "created_at"
112
+ t.date "bts_begin_date"
113
+ t.date "bts_end_date"
114
+ t.float "load_factor"
115
+ t.float "freight_share"
116
+ t.float "m3"
117
+ t.float "m2"
118
+ t.float "m1"
119
+ t.float "distance"
120
+ t.float "payload"
121
+ t.integer "aircraft_class_id"
122
+ t.float "multiplier"
123
+ t.string "manufacturer_name"
124
+ t.string "brighter_planet_aircraft_class_code"
125
+ t.integer "weighting"
126
+ t.integer "bts_aircraft_type"
127
+ end
128
+
129
+ create_table "aircraft_classes", :force => true do |t|
130
+ t.string "name"
131
+ t.integer "seats"
132
+ t.integer "fuel_type_id"
133
+ t.float "endpoint_fuel"
134
+ t.string "brighter_planet_aircraft_class_code"
135
+ t.float "m1"
136
+ t.float "m2"
137
+ t.float "m3"
138
+ end
139
+
140
+ create_table "aircraft_seat_classes", :force => true do |t|
141
+ t.integer "aircraft_id"
142
+ t.integer "seat_class_id"
143
+ t.integer "seats"
144
+ t.datetime "created_at"
145
+ t.datetime "updated_at"
146
+ t.float "multiplier"
147
+ t.boolean "fresh"
148
+ end
149
+
150
+ create_table "airline_aircraft", :force => true do |t|
151
+ t.integer "airline_id"
152
+ t.integer "aircraft_id"
153
+ t.integer "seats"
154
+ t.datetime "updated_at"
155
+ t.datetime "created_at"
156
+ t.float "total_seat_area"
157
+ t.float "average_seat_area"
158
+ t.boolean "fresh"
159
+ t.float "multiplier"
160
+ end
161
+
162
+ create_table "airline_aircraft_seat_classes", :force => true do |t|
163
+ t.integer "seats"
164
+ t.float "pitch"
165
+ t.float "width"
166
+ t.float "multiplier"
167
+ t.datetime "created_at"
168
+ t.datetime "updated_at"
169
+ t.float "seat_area"
170
+ t.string "name"
171
+ t.integer "airline_id"
172
+ t.integer "aircraft_id"
173
+ t.integer "seat_class_id"
174
+ t.integer "weighting"
175
+ t.integer "peers"
176
+ end
177
+
178
+ end
179
+
180
+ class Segment < ActiveRecord::Base
181
+ set_primary_key :row_hash
182
+ validates_presence_of :row_hash
183
+ extend CohortScope
184
+ self.minimum_cohort_size = 1
185
+ end
186
+
187
+ (1..10).each do |i|
188
+ a = Segment.new
189
+ a.payload = i
190
+ a.row_hash = ActiveSupport::SecureRandom.hex(10)
191
+ a.save!
192
+ end
193
+
194
+ # Segment.create! :payload => 4, :row_hash => '19290oaijsoidjvlaioshdiluas'
195
+ # Segment.create! :payload => 5, :row_hash => 'sojsdlviuahsdlvahliruhvailsuf'
196
+ # Segment.create! :payload => 6, :row_hash => 'dsiauhiluashdliufhalsidfhuailsd'
197
+
198
+ class AirlineAircraftSeatClass < ActiveRecord::Base
199
+ # include CohortScope
200
+
201
+ # belongs_to :airline, :class_name => 'Airline', :foreign_key => 'airline_id'
202
+ belongs_to :aircraft, :class_name => 'Aircraft', :foreign_key => 'aircraft_id'
203
+ # belongs_to :seat_class, :class_name => 'SeatClass', :foreign_key => 'seat_class_id'
204
+ has_one :aircraft_class, :class_name => 'AircraftClass', :through => :aircraft
205
+ end
206
+
207
+
208
+ class Aircraft < ActiveRecord::Base
209
+ belongs_to :aircraft_class, :class_name => 'AircraftClass', :foreign_key => 'aircraft_class_id'
210
+ # belongs_to :manufacturer, :class_name => 'Manufacturer', :foreign_key => 'manufacturer_id'
211
+ # has_many :airline_aircraft, :class_name => 'AirlineAircraft'
212
+ # has_many :seat_classes, :class_name => 'AircraftSeatClass'
213
+ has_many :segments, :class_name => "Segment"
214
+ has_many :airline_aircraft_seat_classes, :class_name => 'AirlineAircraftSeatClass'
215
+ end
216
+
217
+ class AircraftClass < ActiveRecord::Base
218
+ has_many :aircraft, :class_name => 'Aircraft'
219
+ has_many :airline_aircraft_seat_classes, :through => :aircraft
220
+ end
@@ -0,0 +1,180 @@
1
+ require 'helper'
2
+
3
+ class TestWeightedAverage < Test::Unit::TestCase
4
+ # should "update all weighted averages, has_many through" do
5
+ # should_have_same_sql(
6
+ # AircraftClass.construct_update_all_weighted_averages_sql(:seats, :association => :airline_aircraft_seat_classes),
7
+ # "UPDATE `aircraft_classes` SET `aircraft_classes`.seats = (SELECT (SUM((`airline_aircraft_seat_classes`.seats * `airline_aircraft_seat_classes`.weighting) / SUM(`airline_aircraft_seat_classes`.weighting)) AS weighted_average FROM `airline_aircraft_seat_classes` LEFT OUTER JOIN `aircraft` ON `aircraft`.id = `airline_aircraft_seat_classes`.aircraft_id WHERE ((`aircraft`.aircraft_class_id = `aircraft_classes`.id AND `airline_aircraft_seat_classes`.aircraft_id = `aircraft`.id) AND (`airline_aircraft_seat_classes`.seats IS NOT NULL)) )"
8
+ # )
9
+ # end
10
+ #
11
+ # should "update all weighted averages" do
12
+ # should_have_same_sql(
13
+ # Aircraft.construct_update_all_weighted_averages_sql(:seats, :association => :airline_aircraft_seat_classes),
14
+ # "UPDATE `aircraft` SET `aircraft`.seats = (SELECT (SUM((`airline_aircraft_seat_classes`.seats * `airline_aircraft_seat_classes`.weighting) / SUM(`airline_aircraft_seat_classes`.weighting)) AS weighted_average FROM `airline_aircraft_seat_classes` WHERE ((`airline_aircraft_seat_classes`.aircraft_id = `aircraft`.id) AND (`airline_aircraft_seat_classes`.seats IS NOT NULL)) )"
15
+ # )
16
+ #
17
+ # should_have_same_sql(
18
+ # Aircraft.construct_update_all_weighted_averages_sql(:seats, :association => :airline_aircraft_seat_classes),
19
+ # "UPDATE `aircraft` SET `aircraft`.seats = (#{AirlineAircraftSeatClass.construct_weighted_average_sql(:seats, {}, :conditions => '`airline_aircraft_seat_classes`.aircraft_id = `aircraft`.id')})"
20
+ # )
21
+ # end
22
+ #
23
+ # should "update all weighted averages, custom weighting" do
24
+ # should_have_same_sql(
25
+ # Aircraft.construct_update_all_weighted_averages_sql(:distance, :by => :passengers, :association => :segments),
26
+ # "UPDATE `aircraft` SET `aircraft`.distance = (SELECT (SUM((`segments`.distance * `segments`.passengers) / SUM(`segments`.passengers)) AS weighted_average FROM `segments` WHERE ((`segments`.aircraft_id = `aircraft`.id) AND (`segments`.distance IS NOT NULL)) )"
27
+ # )
28
+ #
29
+ # should_have_same_sql(
30
+ # Aircraft.construct_update_all_weighted_averages_sql(:distance, :by => :passengers, :association => :segments),
31
+ # "UPDATE `aircraft` SET `aircraft`.distance = (#{Segment.construct_weighted_average_sql(:distance, { :by => :passengers }, :conditions => '`segments`.aircraft_id = `aircraft`.id' )})"
32
+ # )
33
+ # end
34
+ #
35
+ # should "update all weighted averages, custom weighting and disaggregator" do
36
+ # should_have_same_sql(
37
+ # Aircraft.construct_update_all_weighted_averages_sql(:payload, :by => :passengers, :association => :segments, :disaggregator => :departures_performed),
38
+ # "UPDATE `aircraft` SET `aircraft`.payload = (SELECT (SUM((`segments`.payload / `segments`.departures_performed * `segments`.passengers) / SUM(`segments`.passengers)) AS weighted_average FROM `segments` WHERE ((`segments`.aircraft_id = `aircraft`.id) AND (`segments`.payload IS NOT NULL)) )"
39
+ # )
40
+ #
41
+ # should_have_same_sql(
42
+ # Aircraft.construct_update_all_weighted_averages_sql(:payload, :by => :passengers, :association => :segments, :disaggregator => :departures_performed),
43
+ # "UPDATE `aircraft` SET `aircraft`.payload = (#{Segment.construct_weighted_average_sql(:payload, { :by => :passengers, :disaggregator => :departures_performed }, :conditions => '`segments`.aircraft_id = `aircraft`.id')})"
44
+ # )
45
+ # end
46
+
47
+ # plain
48
+
49
+ should "do default weighting" do
50
+ should_have_same_sql(
51
+ "SELECT (SUM((`airline_aircraft_seat_classes`.`seats`) * `airline_aircraft_seat_classes`.`weighting`) / SUM(`airline_aircraft_seat_classes`.`weighting`)) AS weighted_average FROM `airline_aircraft_seat_classes` WHERE (`airline_aircraft_seat_classes`.`seats` IS NOT NULL)",
52
+ AirlineAircraftSeatClass.weighted_average_relation('seats')
53
+ )
54
+ end
55
+
56
+ should "do custom weighting" do
57
+ should_have_same_sql(
58
+ "SELECT (SUM((`segments`.`distance`) * `segments`.`passengers`) / SUM(`segments`.`passengers`)) AS weighted_average FROM `segments` WHERE (`segments`.`distance` IS NOT NULL)",
59
+ Segment.weighted_average_relation('distance', :weighted_by => 'passengers')
60
+ )
61
+ end
62
+
63
+ # multiple columns
64
+
65
+ should "add multiple columns before averaging" do
66
+ should_have_same_sql(
67
+ "SELECT (SUM((`airline_aircraft_seat_classes`.`seats` + `airline_aircraft_seat_classes`.`pitch`) * `airline_aircraft_seat_classes`.`weighting`) / SUM(`airline_aircraft_seat_classes`.`weighting`)) AS weighted_average FROM `airline_aircraft_seat_classes` WHERE (`airline_aircraft_seat_classes`.`seats` IS NOT NULL) AND (`airline_aircraft_seat_classes`.`pitch` IS NOT NULL)",
68
+ AirlineAircraftSeatClass.weighted_average_relation(['seats', 'pitch'])
69
+ )
70
+ end
71
+
72
+ # conditions
73
+
74
+ # a subquery used in Aircraft.update_all_seats
75
+ should "do default weighting with conditions" do
76
+ conditions = 'aircraft_id = 1'
77
+
78
+ should_have_same_sql(
79
+ "SELECT (SUM((`airline_aircraft_seat_classes`.`seats`) * `airline_aircraft_seat_classes`.`weighting`) / SUM(`airline_aircraft_seat_classes`.`weighting`)) AS weighted_average FROM `airline_aircraft_seat_classes` WHERE (#{conditions}) AND (`airline_aircraft_seat_classes`.`seats` IS NOT NULL)",
80
+ AirlineAircraftSeatClass.where(conditions).weighted_average_relation('seats')
81
+ )
82
+ end
83
+
84
+ # note fake condition
85
+ should "do custom weighting with conditions" do
86
+ conditions = '456 = 456'
87
+ should_have_same_sql(
88
+ "SELECT (SUM((`segments`.`load_factor`) * `segments`.`passengers`) / SUM(`segments`.`passengers`)) AS weighted_average FROM `segments` WHERE (#{conditions}) AND (`segments`.`load_factor` IS NOT NULL)",
89
+ Segment.where(conditions).weighted_average_relation('load_factor', :weighted_by => 'passengers')
90
+ )
91
+ end
92
+
93
+ # foreign weightings
94
+
95
+ # fake! we would never calc seats this way
96
+ should "do foreign default weighting" do
97
+ should_have_same_sql(
98
+ "SELECT (SUM((`aircraft`.`seats`) * `airline_aircraft_seat_classes`.`weighting`) / SUM(`airline_aircraft_seat_classes`.`weighting`)) AS weighted_average, 12345 FROM `aircraft` LEFT OUTER JOIN `airline_aircraft_seat_classes` ON `airline_aircraft_seat_classes`.`aircraft_id` = `aircraft`.`id` WHERE (`aircraft`.`seats` IS NOT NULL)",
99
+ Aircraft.weighted_average_relation('seats', :weighted_by => :airline_aircraft_seat_classes)
100
+ )
101
+ end
102
+
103
+ # Aircraft#m3 fallback value
104
+ # a subquery used in Aircraft.update_all_m3s
105
+ should "do foreign custom weighting" do
106
+ should_have_same_sql(
107
+ "SELECT (SUM((`aircraft`.`m3`) * `segments`.`passengers`) / SUM(`segments`.`passengers`)) AS weighted_average, 12345 FROM `aircraft` LEFT OUTER JOIN `segments` ON `segments`.`aircraft_id` = `aircraft`.`id` WHERE (`aircraft`.`m3` IS NOT NULL)",
108
+ Aircraft.weighted_average_relation(:m3, :weighted_by => [:segments, :passengers])
109
+ )
110
+ end
111
+
112
+ # scoped
113
+
114
+ should "do default weighting, scoped" do
115
+ conditions = '456 = 456'
116
+ should_have_same_sql(
117
+ "SELECT (SUM((`airline_aircraft_seat_classes`.`seats`) * `airline_aircraft_seat_classes`.`weighting`) / SUM(`airline_aircraft_seat_classes`.`weighting`)) AS weighted_average FROM `airline_aircraft_seat_classes` WHERE (#{conditions}) AND (`airline_aircraft_seat_classes`.`seats` IS NOT NULL)",
118
+ AirlineAircraftSeatClass.scoped(:conditions => conditions).weighted_average_relation(:seats)
119
+ )
120
+ end
121
+
122
+ should "do custom weighting, scoped" do
123
+ conditions = '999 = 999'
124
+ should_have_same_sql(
125
+ "SELECT (SUM((`segments`.`load_factor`) * `segments`.`passengers`) / SUM(`segments`.`passengers`)) AS weighted_average FROM `segments` WHERE (#{conditions}) AND (`segments`.`load_factor` IS NOT NULL)",
126
+ Segment.scoped(:conditions => conditions).weighted_average_relation(:load_factor, :weighted_by => :passengers)
127
+ )
128
+ end
129
+
130
+ # scoped foreign weightings
131
+
132
+ should "do foreign default weighting, scoped" do
133
+ conditions = '454 != 999'
134
+ should_have_same_sql(
135
+ "SELECT (SUM((`aircraft`.`seats`) * `airline_aircraft_seat_classes`.`weighting`) / SUM(`airline_aircraft_seat_classes`.`weighting`)) AS weighted_average, 12345 FROM `aircraft` LEFT OUTER JOIN `airline_aircraft_seat_classes` ON `airline_aircraft_seat_classes`.`aircraft_id` = `aircraft`.`id` WHERE (#{conditions}) AND (`aircraft`.`seats` IS NOT NULL)",
136
+ Aircraft.scoped(:conditions => conditions).weighted_average_relation(:seats, :weighted_by => :airline_aircraft_seat_classes)
137
+ )
138
+ end
139
+
140
+ should "do foreign custom weighting, scoped" do
141
+ conditions = '`aircraft`.`m3` > 1'
142
+ should_have_same_sql(
143
+ "SELECT (SUM((`aircraft`.`m3`) * `segments`.`passengers`) / SUM(`segments`.`passengers`)) AS weighted_average, 12345 FROM `aircraft` LEFT OUTER JOIN `segments` ON `segments`.`aircraft_id` = `aircraft`.`id` WHERE (#{conditions}) AND (`aircraft`.`m3` IS NOT NULL)",
144
+ Aircraft.scoped(:conditions => conditions).weighted_average_relation(:m3, :weighted_by => [:segments, :passengers])
145
+ )
146
+ end
147
+
148
+ # disaggregation
149
+
150
+ should "do custom weighting with disaggregation" do
151
+ should_have_same_sql(
152
+ "SELECT (SUM((`segments`.`load_factor`) / `segments`.`departures_performed` * `segments`.`passengers`) / SUM(`segments`.`passengers`)) AS weighted_average FROM `segments` WHERE (`segments`.`load_factor` IS NOT NULL)",
153
+ Segment.weighted_average_relation(:load_factor, :weighted_by => :passengers, :disaggregate_by => :departures_performed)
154
+ )
155
+ end
156
+
157
+ # cohorts (requires the cohort_scope gem)
158
+
159
+ should "do custom weighting, with a cohort" do
160
+ should_have_same_sql(
161
+ "SELECT (SUM((`segments`.`load_factor`) * `segments`.`passengers`) / SUM(`segments`.`passengers`)) AS weighted_average FROM `segments` WHERE (`segments`.`payload` = 5) AND (`segments`.`load_factor` IS NOT NULL)",
162
+ Segment.big_cohort(:payload => 5).weighted_average_relation(:load_factor, :weighted_by => :passengers)
163
+ )
164
+ end
165
+
166
+ private
167
+
168
+ def should_have_same_sql(*args)
169
+ # make sure everything is an SQL string
170
+ args.map! { |arg| arg.is_a?(String) ? arg : arg.to_sql }
171
+ # clean up whitespace
172
+ args.map! { |arg| arg.to_s.gsub /\s+/, ' ' }
173
+ # treat the first arg as the "known good"
174
+ best = args.shift
175
+ # compare everything to the known good
176
+ args.each { |arg| assert_equal best, arg }
177
+ # make sure the SQL is valid
178
+ assert_nothing_raised { ActiveRecord::Base.connection.execute best }
179
+ end
180
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: weighted_average
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 2
9
+ version: 0.0.2
10
+ platform: ruby
11
+ authors:
12
+ - Seamus Abshere
13
+ - Andy Rossmeissl
14
+ - Ian Hough
15
+ - Matt Kling
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2010-04-07 00:00:00 -04:00
21
+ default_executable:
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: activerecord
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 0
34
+ - beta2
35
+ version: 3.0.0.beta2
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: activesupport
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ segments:
46
+ - 3
47
+ - 0
48
+ - 0
49
+ - beta2
50
+ version: 3.0.0.beta2
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: arel
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ segments:
61
+ - 0
62
+ - 3
63
+ - 3
64
+ version: 0.3.3
65
+ type: :runtime
66
+ version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ name: cohort_scope
69
+ prerelease: false
70
+ requirement: &id004 !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ - 0
77
+ - 2
78
+ version: 0.0.2
79
+ type: :development
80
+ version_requirements: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ name: shoulda
83
+ prerelease: false
84
+ requirement: &id005 !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 2
90
+ - 10
91
+ - 3
92
+ version: 2.10.3
93
+ type: :development
94
+ version_requirements: *id005
95
+ description: Perform weighted averages, even across associations. Rails 3 only because it uses ARel.
96
+ email: seamus@abshere.net
97
+ executables: []
98
+
99
+ extensions: []
100
+
101
+ extra_rdoc_files:
102
+ - LICENSE
103
+ - README.rdoc
104
+ files:
105
+ - .document
106
+ - .gitignore
107
+ - LICENSE
108
+ - README.rdoc
109
+ - Rakefile
110
+ - VERSION
111
+ - lib/weighted_average.rb
112
+ - test/helper.rb
113
+ - test/test_weighted_average.rb
114
+ has_rdoc: true
115
+ homepage: http://github.com/seamusabshere/weighted_average
116
+ licenses: []
117
+
118
+ post_install_message:
119
+ rdoc_options:
120
+ - --charset=UTF-8
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ segments:
135
+ - 0
136
+ version: "0"
137
+ requirements: []
138
+
139
+ rubyforge_project:
140
+ rubygems_version: 1.3.6
141
+ signing_key:
142
+ specification_version: 3
143
+ summary: Perform weighted averages. Rails 3 only.
144
+ test_files:
145
+ - test/helper.rb
146
+ - test/test_weighted_average.rb