weighted_average 1.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.yardopts +2 -0
- data/CHANGELOG +9 -0
- data/{README.rdoc → README.markdown} +7 -7
- data/Rakefile +5 -0
- data/lib/weighted_average/active_record_base_class_methods.rb +17 -0
- data/lib/weighted_average/active_record_relation_instance_methods.rb +62 -0
- data/lib/weighted_average/arel_select_manager_instance_methods.rb +72 -0
- data/lib/weighted_average/arel_table_instance_methods.rb +17 -0
- data/lib/weighted_average/version.rb +1 -1
- data/lib/weighted_average.rb +14 -81
- data/test/helper.rb +3 -0
- data/test/test_weighted_average.rb +40 -30
- data/weighted_average.gemspec +2 -1
- metadata +40 -19
data/.gitignore
CHANGED
data/.yardopts
ADDED
data/CHANGELOG
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
|
1
|
+
# weighted_average
|
2
2
|
|
3
|
-
Do weighted averages in
|
3
|
+
Do weighted averages in ARel.
|
4
4
|
|
5
|
-
|
5
|
+
## Rationale
|
6
6
|
|
7
7
|
You have a bunch of flight records with passenger count and distance.
|
8
8
|
|
@@ -13,9 +13,9 @@ The average distance is <tt>(10_000 + 500) / 2 = 5250</tt>.
|
|
13
13
|
|
14
14
|
The average distance weighted by passenger count is <tt>(30_000 * 500 + 15 * 10_000) / (10_500) = 1442</tt>.
|
15
15
|
|
16
|
-
|
16
|
+
## Usage
|
17
17
|
|
18
|
-
Using <tt>FlightSegment</tt> from
|
18
|
+
Using <tt>FlightSegment</tt> from [Brighter Planet's earth library](http://rubygems.org/gems/earth):
|
19
19
|
|
20
20
|
>> FlightSegment.weighted_average(:distance, :weighted_by => :passengers)
|
21
21
|
=> 2436.1959
|
@@ -25,6 +25,6 @@ You can also see the SQL that is generated:
|
|
25
25
|
>> FlightSegment.weighted_average_relation(:distance, :weighted_by => :passengers).to_sql
|
26
26
|
=> "SELECT (SUM((`flight_segments`.`distance`) * `flight_segments`.`passengers`) / SUM(`flight_segments`.`passengers`)) AS weighted_average FROM `flight_segments` WHERE (`flight_segments`.`distance` IS NOT NULL)"
|
27
27
|
|
28
|
-
|
28
|
+
## Copyright
|
29
29
|
|
30
|
-
Copyright (c)
|
30
|
+
Copyright (c) 2012 Brighter Planet, Inc.
|
data/Rakefile
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
module WeightedAverage
|
2
|
+
module ActiveRecordBaseClassMethods
|
3
|
+
# @see WeightedAverage::ActiveRecordRelationInstanceMethods#weighted_average
|
4
|
+
#
|
5
|
+
# @return [Float,nil]
|
6
|
+
def weighted_average(*args)
|
7
|
+
scoped.weighted_average(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @see WeightedAverage::ActiveRecordRelationInstanceMethods#weighted_average_relation
|
11
|
+
#
|
12
|
+
# @return [Arel::SelectManager]
|
13
|
+
def weighted_average_relation(*args)
|
14
|
+
scoped.weighted_average_relation(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module WeightedAverage
|
2
|
+
module ActiveRecordRelationInstanceMethods
|
3
|
+
# Get the weighted average of column(s).
|
4
|
+
#
|
5
|
+
# In addition to the options available on WeightedAverage::ArelSelectManagerInstanceMethods#weighted_average, this ActiveRecord-specific method understands associations.
|
6
|
+
#
|
7
|
+
# @param [Symbol,Array<Symbol>] data_column_names One or more column names whose average should be calculated. Added together before being multiplied by the weighting if more than one.
|
8
|
+
# @param [Hash] options
|
9
|
+
#
|
10
|
+
# @option options [Symbol] :weighted_by The name of an association to weight against OR a column name just like in the pure ARel version.
|
11
|
+
# @option options [Array{Symbol,Symbol}] :weighted_by The name of an association and a weighting column inside that association table to weight against. Not available in the pure ARel version.
|
12
|
+
# @option options [Symbol] :disaggregate_by Same as its meaning in the pure ARel version.
|
13
|
+
#
|
14
|
+
# @example Get the average m3 of all aircraft, weighted by a column named :weighting in flight segments table. But wait... there is no column called :weighting! So see the next example.
|
15
|
+
# Aircraft.weighted_average(:m3, :weighted_by => :segments)
|
16
|
+
#
|
17
|
+
# @example Get the average m3 of all aircraft, weighted by how many :passengers flew in a particular aircraft.
|
18
|
+
# Aircraft.weighted_average(:m3, :weighted_by => [:segments, :passengers])
|
19
|
+
#
|
20
|
+
# @see WeightedAverage::ArelSelectManagerInstanceMethods#weighted_average The pure ARel version of this method, which doesn't know about associations
|
21
|
+
#
|
22
|
+
# @return [Float,nil]
|
23
|
+
def weighted_average(data_column_names, options = {})
|
24
|
+
weighted_average = connection.select_value weighted_average_relation(data_column_names, options).to_sql
|
25
|
+
weighted_average.nil? ? nil : weighted_average.to_f
|
26
|
+
end
|
27
|
+
|
28
|
+
# Same as WeightedAverage::ArelSelectManagerInstanceMethods#weighted_average, except it can interpret associations.
|
29
|
+
#
|
30
|
+
# @see WeightedAverage::ArelSelectManagerInstanceMethods#weighted_average_relation The pure ARel version of this method.
|
31
|
+
#
|
32
|
+
# @return [Arel::SelectManager]
|
33
|
+
def weighted_average_relation(data_column_names, options = {})
|
34
|
+
if weighted_by_option = options[:weighted_by]
|
35
|
+
case weighted_by_option
|
36
|
+
when Array
|
37
|
+
# :weighted_by specifies a custom column on an association table (least common)
|
38
|
+
unless association = reflect_on_association(weighted_by_option.first)
|
39
|
+
raise ArgumentError, "#{name} does not have association #{weighted_by_option.first.inspect}"
|
40
|
+
end
|
41
|
+
weighted_by_column = association.klass.arel_table[weighted_by_option.last]
|
42
|
+
when Symbol, String
|
43
|
+
if association = reflect_on_association(weighted_by_option)
|
44
|
+
# :weighted_by specifies an association table with a column named "weighting"
|
45
|
+
weighted_by_column = association.klass.arel_table[DEFAULT_WEIGHTED_BY_COLUMN_NAME]
|
46
|
+
else
|
47
|
+
# :weighted_by specifies a custom column on the same table
|
48
|
+
weighted_by_column = arel_table[weighted_by_option]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if association
|
52
|
+
joins(association.name).arel.weighted_average_relation data_column_names, options.merge(:weighted_by => weighted_by_column)
|
53
|
+
else
|
54
|
+
arel.weighted_average_relation data_column_names, options.merge(:weighted_by => weighted_by_column)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
arel.weighted_average_relation data_column_names, options
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module WeightedAverage
|
2
|
+
module ArelSelectManagerInstanceMethods
|
3
|
+
# Calculate the weighted average of column(s).
|
4
|
+
#
|
5
|
+
# @param [Symbol,Array<Symbol>] data_column_names One or more column names whose average should be calculated. Added together before being multiplied by the weighting if more than one.
|
6
|
+
# @param [Hash] options
|
7
|
+
#
|
8
|
+
# @option options [Symbol] :weighted_by The name of the weighting column if it's not :weighting (the default)
|
9
|
+
# @option options [Symbol] :disaggregate_by The name of a column to disaggregate by. Usually not necessary.
|
10
|
+
#
|
11
|
+
# @see WeightedAverage::ActiveRecordRelationInstanceMethods The ActiveRecord-specific version of this method, which knows about associations.
|
12
|
+
#
|
13
|
+
# @example Weighted average of load factor in flight stage data
|
14
|
+
# Arel::Table.new(:flight_segments).weighted_average(:load_factor, :weighted_by => :passengers)
|
15
|
+
#
|
16
|
+
# @return [Float,nil]
|
17
|
+
def weighted_average(data_column_names, options = {})
|
18
|
+
weighted_average = @engine.connection.select_value(weighted_average_relation(data_column_names, options).to_sql)
|
19
|
+
weighted_average.nil? ? nil : weighted_average.to_f
|
20
|
+
end
|
21
|
+
|
22
|
+
# In case you want to get the relation and/or the SQL of the calculation query before actually runnnig it.
|
23
|
+
#
|
24
|
+
# @example Get the SQL
|
25
|
+
# Arel::Table.new(:flight_segments).weighted_average_relation(:load_factor, :weighted_by => :passengers).to_sql
|
26
|
+
#
|
27
|
+
# @return [Arel::SelectManager] A relation you can play around with.
|
28
|
+
def weighted_average_relation(data_column_names, options = {})
|
29
|
+
left = self.source.left
|
30
|
+
|
31
|
+
weighted_by_column = case options[:weighted_by]
|
32
|
+
when Arel::Attribute
|
33
|
+
options[:weighted_by]
|
34
|
+
when Symbol, String
|
35
|
+
left[options[:weighted_by]]
|
36
|
+
when NilClass
|
37
|
+
left[DEFAULT_WEIGHTED_BY_COLUMN_NAME]
|
38
|
+
else
|
39
|
+
raise ArgumentError, ":weighted_by => #{options[:weighted_by].inspect} must be a column on #{left.inspect}"
|
40
|
+
end
|
41
|
+
|
42
|
+
disaggregate_by_column = if options[:disaggregate_by]
|
43
|
+
left[options[:disaggregate_by]]
|
44
|
+
end
|
45
|
+
|
46
|
+
data_columns = ::Array.wrap(data_column_names).map do |data_column_name|
|
47
|
+
left[data_column_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
if disaggregate_by_column
|
51
|
+
self.projections = [Arel::Nodes::Division.new(Arel::Nodes::Sum.new(weighted_by_column * (data_columns.inject(:+)) / disaggregate_by_column * 1.0), Arel::Nodes::Sum.new([weighted_by_column]))]
|
52
|
+
else
|
53
|
+
self.projections = [Arel::Nodes::Division.new(Arel::Nodes::Sum.new(weighted_by_column * (data_columns.inject(:+)) * 1.0), Arel::Nodes::Sum.new([weighted_by_column]))]
|
54
|
+
end
|
55
|
+
|
56
|
+
data_columns_not_eq_nil = data_columns.inject(nil) do |memo, data_column|
|
57
|
+
if memo
|
58
|
+
memo.and(data_column.not_eq(nil))
|
59
|
+
else
|
60
|
+
data_column.not_eq(nil)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if disaggregate_by_column
|
65
|
+
where data_columns_not_eq_nil.and(weighted_by_column.gt(0)).and(disaggregate_by_column.gt(0))
|
66
|
+
else
|
67
|
+
where data_columns_not_eq_nil.and(weighted_by_column.gt(0))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module WeightedAverage
|
2
|
+
module ArelTableInstanceMethods
|
3
|
+
# @see WeightedAverage::ArelSelectManagerInstanceMethods#weighted_average
|
4
|
+
#
|
5
|
+
# @return [Float,nil]
|
6
|
+
def weighted_average(*args)
|
7
|
+
from(self).weighted_average(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @see WeightedAverage::ArelSelectManagerInstanceMethods#weighted_average_relation
|
11
|
+
#
|
12
|
+
# @return [Arel::SelectManager]
|
13
|
+
def weighted_average_relation(*args)
|
14
|
+
from(self).weighted_average_relation(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/weighted_average.rb
CHANGED
@@ -1,89 +1,22 @@
|
|
1
|
+
require 'arel'
|
1
2
|
require 'active_support/core_ext'
|
2
|
-
require 'active_record'
|
3
|
-
require "weighted_average/version"
|
4
3
|
|
5
4
|
module WeightedAverage
|
6
|
-
|
7
|
-
|
8
|
-
weighted_average = connection.select_value(weighted_average_relation(*args).to_sql, 'weighted_average')
|
9
|
-
weighted_average.nil? ? nil : weighted_average.to_f
|
10
|
-
end
|
11
|
-
|
12
|
-
# Returns the ARel relation for a weighted average query.
|
13
|
-
def weighted_average_relation(data_column_names, options = {})
|
14
|
-
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
|
15
|
-
raise ::ArgumentError, "No nil values in weighted_by, please" if ::Array.wrap(options[:weighted_by]).any?(&:nil?)
|
16
|
-
|
17
|
-
# :airline_aircraft_seat_class
|
18
|
-
association = if options[:weighted_by].present?
|
19
|
-
options[:weighted_by].is_a?(::Array) ? reflect_on_association(options[:weighted_by].first.to_sym) : reflect_on_association(options[:weighted_by].to_sym)
|
20
|
-
end
|
21
|
-
|
22
|
-
# AirlineAircraftSeatClass
|
23
|
-
association_class = association.klass if association
|
24
|
-
|
25
|
-
# `aircraft`
|
26
|
-
table_name = connection.quote_table_name table.name
|
27
|
-
|
28
|
-
# `airline_aircraft_seat_classes`
|
29
|
-
weighted_by_table_name = if association_class
|
30
|
-
association_class.quoted_table_name
|
31
|
-
else
|
32
|
-
table_name
|
33
|
-
end
|
34
|
-
|
35
|
-
# `airline_aircraft_seat_classes`.`weighting`
|
36
|
-
weighted_by_column_name = if association_class and options[:weighted_by].is_a?(::Array)
|
37
|
-
options[:weighted_by].last.to_s
|
38
|
-
elsif !association_class and (options[:weighted_by].is_a?(::String) or options[:weighted_by].is_a?(::Symbol))
|
39
|
-
options[:weighted_by].to_s
|
40
|
-
else
|
41
|
-
'weighting'
|
42
|
-
end
|
43
|
-
weighted_by_column_name = [ weighted_by_table_name, connection.quote_column_name(weighted_by_column_name) ].join '.'
|
44
|
-
|
45
|
-
# `aircraft`.`passengers`
|
46
|
-
disaggregate_by_column_name = if options[:disaggregate_by]
|
47
|
-
[ table_name, connection.quote_column_name(options[:disaggregate_by]) ].join '.'
|
48
|
-
end
|
5
|
+
DEFAULT_WEIGHTED_BY_COLUMN_NAME = :weighting
|
6
|
+
end
|
49
7
|
|
50
|
-
|
51
|
-
|
52
|
-
[ table_name, connection.quote_column_name(data_column_name) ].join '.'
|
53
|
-
end
|
8
|
+
require 'weighted_average/arel_select_manager_instance_methods'
|
9
|
+
Arel::SelectManager.send :include, WeightedAverage::ArelSelectManagerInstanceMethods
|
54
10
|
|
55
|
-
|
56
|
-
|
57
|
-
relation = relation.where("#{data_column_name} IS NOT NULL")
|
58
|
-
end
|
11
|
+
require 'weighted_average/arel_table_instance_methods'
|
12
|
+
Arel::Table.send :include, WeightedAverage::ArelTableInstanceMethods
|
59
13
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
relation = relation.joins(association.name) if association_class
|
66
|
-
relation
|
67
|
-
end
|
68
|
-
end
|
14
|
+
if defined?(ActiveRecord)
|
15
|
+
require 'weighted_average/active_record_base_class_methods'
|
16
|
+
ActiveRecord::Base.extend WeightedAverage::ActiveRecordBaseClassMethods
|
17
|
+
proxy_class = defined?(ActiveRecord::Associations::CollectionProxy) ? ActiveRecord::Associations::CollectionProxy : ActiveRecord::Associations::AssociationCollection
|
18
|
+
proxy_class.extend WeightedAverage::ActiveRecordBaseClassMethods
|
69
19
|
|
70
|
-
|
71
|
-
|
72
|
-
scoped.weighted_average(*args)
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.weighted_average_relation(*args) # :nodoc:
|
76
|
-
scoped.weighted_average_relation(*args)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
::ActiveRecord::Base.class_eval do
|
80
|
-
def self.weighted_average(*args) # :nodoc:
|
81
|
-
scoped.weighted_average(*args)
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.weighted_average_relation(*args) # :nodoc:
|
85
|
-
scoped.weighted_average_relation(*args)
|
86
|
-
end
|
20
|
+
require 'weighted_average/active_record_relation_instance_methods'
|
21
|
+
ActiveRecord::Relation.send :include, WeightedAverage::ActiveRecordRelationInstanceMethods
|
87
22
|
end
|
88
|
-
|
89
|
-
::ActiveRecord::Relation.send :include, ::WeightedAverage
|
data/test/helper.rb
CHANGED
@@ -48,14 +48,14 @@ describe WeightedAverage do
|
|
48
48
|
|
49
49
|
it "does default weighting" do
|
50
50
|
should_have_same_sql(
|
51
|
-
"SELECT
|
51
|
+
"SELECT SUM(airline_aircraft_seat_classes.weighting * airline_aircraft_seat_classes.seats * 1.0) / SUM(airline_aircraft_seat_classes.weighting) FROM airline_aircraft_seat_classes WHERE airline_aircraft_seat_classes.seats IS NOT NULL AND airline_aircraft_seat_classes.weighting > 0",
|
52
52
|
AirlineAircraftSeatClass.weighted_average_relation('seats')
|
53
53
|
)
|
54
54
|
end
|
55
55
|
|
56
56
|
it "does custom weighting" do
|
57
57
|
should_have_same_sql(
|
58
|
-
"SELECT
|
58
|
+
"SELECT SUM(segments.passengers * segments.distance * 1.0) / SUM(segments.passengers) FROM segments WHERE segments.distance IS NOT NULL AND segments.passengers > 0",
|
59
59
|
Segment.weighted_average_relation('distance', :weighted_by => 'passengers')
|
60
60
|
)
|
61
61
|
end
|
@@ -71,7 +71,7 @@ describe WeightedAverage do
|
|
71
71
|
|
72
72
|
it "adds multiple columns before averaging" do
|
73
73
|
should_have_same_sql(
|
74
|
-
"SELECT
|
74
|
+
"SELECT SUM(airline_aircraft_seat_classes.weighting * (airline_aircraft_seat_classes.seats + airline_aircraft_seat_classes.pitch) * 1.0) / SUM(airline_aircraft_seat_classes.weighting) FROM airline_aircraft_seat_classes WHERE airline_aircraft_seat_classes.seats IS NOT NULL AND airline_aircraft_seat_classes.pitch IS NOT NULL AND airline_aircraft_seat_classes.weighting > 0",
|
75
75
|
AirlineAircraftSeatClass.weighted_average_relation(['seats', 'pitch'])
|
76
76
|
)
|
77
77
|
end
|
@@ -80,10 +80,10 @@ describe WeightedAverage do
|
|
80
80
|
|
81
81
|
# a subquery used in Aircraft.update_all_seats
|
82
82
|
it "does default weighting with conditions" do
|
83
|
-
conditions =
|
83
|
+
conditions = "aircraft_id = #{rand(1e11)}"
|
84
84
|
|
85
85
|
should_have_same_sql(
|
86
|
-
"SELECT
|
86
|
+
"SELECT SUM(airline_aircraft_seat_classes.weighting * airline_aircraft_seat_classes.seats * 1.0) / SUM(airline_aircraft_seat_classes.weighting) FROM airline_aircraft_seat_classes WHERE (#{conditions}) AND airline_aircraft_seat_classes.seats IS NOT NULL AND airline_aircraft_seat_classes.weighting > 0",
|
87
87
|
AirlineAircraftSeatClass.where(conditions).weighted_average_relation('seats')
|
88
88
|
)
|
89
89
|
end
|
@@ -92,7 +92,7 @@ describe WeightedAverage do
|
|
92
92
|
it "does custom weighting with conditions" do
|
93
93
|
conditions = '456 = 456'
|
94
94
|
should_have_same_sql(
|
95
|
-
"SELECT
|
95
|
+
"SELECT SUM(segments.passengers * segments.load_factor * 1.0) / SUM(segments.passengers) FROM segments WHERE (456 = 456) AND segments.load_factor IS NOT NULL AND segments.passengers > 0",
|
96
96
|
Segment.where(conditions).weighted_average_relation('load_factor', :weighted_by => 'passengers')
|
97
97
|
)
|
98
98
|
end
|
@@ -102,7 +102,7 @@ describe WeightedAverage do
|
|
102
102
|
# fake! we would never calc seats this way
|
103
103
|
it "does foreign default weighting" do
|
104
104
|
should_have_same_sql(
|
105
|
-
"SELECT
|
105
|
+
"SELECT SUM(airline_aircraft_seat_classes.weighting * aircraft.seats * 1.0) / SUM(airline_aircraft_seat_classes.weighting) FROM aircraft INNER JOIN airline_aircraft_seat_classes ON airline_aircraft_seat_classes.aircraft_id = aircraft.id WHERE aircraft.seats IS NOT NULL AND airline_aircraft_seat_classes.weighting > 0",
|
106
106
|
Aircraft.weighted_average_relation('seats', :weighted_by => :airline_aircraft_seat_classes)
|
107
107
|
)
|
108
108
|
end
|
@@ -111,14 +111,14 @@ describe WeightedAverage do
|
|
111
111
|
# a subquery used in Aircraft.update_all_m3s
|
112
112
|
it "does foreign custom weighting" do
|
113
113
|
should_have_same_sql(
|
114
|
-
"SELECT
|
114
|
+
"SELECT SUM(segments.passengers * aircraft.m3 * 1.0) / SUM(segments.passengers) FROM aircraft INNER JOIN segments ON segments.bts_aircraft_type = aircraft.bts_aircraft_type WHERE aircraft.m3 IS NOT NULL AND segments.passengers > 0",
|
115
115
|
Aircraft.weighted_average_relation(:m3, :weighted_by => [:segments, :passengers])
|
116
116
|
)
|
117
117
|
end
|
118
118
|
|
119
119
|
it "does foreign custom weighting with custom join keys" do
|
120
120
|
should_have_same_sql(
|
121
|
-
"SELECT
|
121
|
+
"SELECT SUM(segments.passengers * aircraft_deux.m3 * 1.0) / SUM(segments.passengers) FROM aircraft_deux INNER JOIN segments ON segments.bts_aircraft_type = aircraft_deux.my_bts_aircraft_type_code WHERE aircraft_deux.m3 IS NOT NULL AND segments.passengers > 0",
|
122
122
|
AircraftDeux.weighted_average_relation(:m3, :weighted_by => [:segments, :passengers])
|
123
123
|
)
|
124
124
|
end
|
@@ -128,7 +128,7 @@ describe WeightedAverage do
|
|
128
128
|
it "does default weighting, scoped" do
|
129
129
|
conditions = '456 = 456'
|
130
130
|
should_have_same_sql(
|
131
|
-
"SELECT
|
131
|
+
"SELECT SUM(airline_aircraft_seat_classes.weighting * airline_aircraft_seat_classes.seats * 1.0) / SUM(airline_aircraft_seat_classes.weighting) FROM airline_aircraft_seat_classes WHERE (#{conditions}) AND airline_aircraft_seat_classes.seats IS NOT NULL AND airline_aircraft_seat_classes.weighting > 0",
|
132
132
|
AirlineAircraftSeatClass.scoped(:conditions => conditions).weighted_average_relation(:seats)
|
133
133
|
)
|
134
134
|
end
|
@@ -136,17 +136,17 @@ describe WeightedAverage do
|
|
136
136
|
it "does custom weighting, scoped" do
|
137
137
|
conditions = '999 = 999'
|
138
138
|
should_have_same_sql(
|
139
|
-
"SELECT
|
139
|
+
"SELECT SUM(segments.passengers * segments.load_factor * 1.0) / SUM(segments.passengers) FROM segments WHERE (#{conditions}) AND segments.load_factor IS NOT NULL AND segments.passengers > 0",
|
140
140
|
Segment.scoped(:conditions => conditions).weighted_average_relation(:load_factor, :weighted_by => :passengers)
|
141
141
|
)
|
142
142
|
end
|
143
143
|
|
144
|
-
# scoped foreign weightings
|
144
|
+
# # scoped foreign weightings
|
145
145
|
|
146
146
|
it "does foreign default weighting, scoped" do
|
147
147
|
conditions = '454 != 999'
|
148
148
|
should_have_same_sql(
|
149
|
-
"SELECT
|
149
|
+
"SELECT SUM(airline_aircraft_seat_classes.weighting * aircraft.seats * 1.0) / SUM(airline_aircraft_seat_classes.weighting) FROM aircraft INNER JOIN airline_aircraft_seat_classes ON airline_aircraft_seat_classes.aircraft_id = aircraft.id WHERE (#{conditions}) AND aircraft.seats IS NOT NULL AND airline_aircraft_seat_classes.weighting > 0",
|
150
150
|
Aircraft.scoped(:conditions => conditions).weighted_average_relation(:seats, :weighted_by => :airline_aircraft_seat_classes)
|
151
151
|
)
|
152
152
|
end
|
@@ -154,21 +154,21 @@ describe WeightedAverage do
|
|
154
154
|
it "does foreign custom weighting, scoped" do
|
155
155
|
conditions = 'aircraft.m3 > 1'
|
156
156
|
should_have_same_sql(
|
157
|
-
"SELECT
|
157
|
+
"SELECT SUM(segments.passengers * aircraft.m3 * 1.0) / SUM(segments.passengers) FROM aircraft INNER JOIN segments ON segments.bts_aircraft_type = aircraft.bts_aircraft_type WHERE (#{conditions}) AND aircraft.m3 IS NOT NULL AND segments.passengers > 0",
|
158
158
|
Aircraft.scoped(:conditions => conditions).weighted_average_relation(:m3, :weighted_by => [:segments, :passengers])
|
159
159
|
)
|
160
160
|
end
|
161
161
|
|
162
|
-
# disaggregation
|
162
|
+
# # disaggregation
|
163
163
|
|
164
164
|
it "does custom weighting with disaggregation" do
|
165
165
|
should_have_same_sql(
|
166
|
-
"SELECT
|
166
|
+
"SELECT SUM(segments.passengers * segments.load_factor / segments.departures_performed * 1.0) / SUM(segments.passengers) FROM segments WHERE segments.load_factor IS NOT NULL AND segments.passengers > 0 AND segments.departures_performed > 0",
|
167
167
|
Segment.weighted_average_relation(:load_factor, :weighted_by => :passengers, :disaggregate_by => :departures_performed)
|
168
168
|
)
|
169
169
|
end
|
170
170
|
|
171
|
-
# more complicated stuff
|
171
|
+
# # more complicated stuff
|
172
172
|
|
173
173
|
it "construct weightings across has_many through associations (that can be used for updating all)" do
|
174
174
|
aircraft_class = AircraftClass.arel_table
|
@@ -176,31 +176,41 @@ describe WeightedAverage do
|
|
176
176
|
segment = Segment.arel_table
|
177
177
|
|
178
178
|
should_have_same_sql(
|
179
|
-
"SELECT
|
179
|
+
"SELECT SUM(segments.passengers * segments.seats * 1.0) / SUM(segments.passengers) FROM segments INNER JOIN aircraft ON aircraft.bts_aircraft_type = segments.bts_aircraft_type INNER JOIN aircraft_classes ON aircraft_classes.id = aircraft.aircraft_class_id WHERE segments.seats IS NOT NULL AND segments.passengers > 0 AND aircraft.aircraft_class_id = aircraft_classes.id",
|
180
180
|
Segment.joins(:aircraft => :aircraft_class).weighted_average_relation(:seats, :weighted_by => :passengers).where(aircraft[:aircraft_class_id].eq(aircraft_class[:id]))
|
181
181
|
)
|
182
182
|
end
|
183
183
|
|
184
184
|
|
185
|
-
# cohorts (requires the
|
185
|
+
# # cohorts (requires the cohort_analysis gem)
|
186
186
|
|
187
187
|
it "does custom weighting, with a cohort" do
|
188
188
|
should_have_same_sql(
|
189
|
-
"SELECT
|
189
|
+
"SELECT SUM(segments.passengers * segments.load_factor * 1.0) / SUM(segments.passengers) FROM segments WHERE (segments.payload = 5) AND segments.load_factor IS NOT NULL AND segments.passengers > 0",
|
190
190
|
Segment.cohort(:payload => 5).weighted_average_relation(:load_factor, :weighted_by => :passengers)
|
191
191
|
)
|
192
192
|
end
|
193
193
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
194
|
+
describe "on Arel::Table" do
|
195
|
+
it "works on plain tables" do
|
196
|
+
c = Segment.connection
|
197
|
+
table_name = "plain_arel_table_#{rand(1e11).to_s}"
|
198
|
+
c.execute %{
|
199
|
+
CREATE TEMPORARY TABLE #{table_name} AS SELECT * FROM #{Segment.quoted_table_name} LIMIT 1
|
200
|
+
}
|
201
|
+
table = Arel::Table.new(table_name)
|
202
|
+
should_have_same_sql(
|
203
|
+
"SELECT SUM(#{table_name}.passengers * #{table_name}.distance * 1.0) / SUM(#{table_name}.passengers) FROM #{table_name} WHERE #{table_name}.distance IS NOT NULL AND #{table_name}.passengers > 0",
|
204
|
+
table.weighted_average_relation('distance', :weighted_by => 'passengers')
|
205
|
+
)
|
206
|
+
end
|
207
|
+
|
208
|
+
it "takes complex args" do
|
209
|
+
should_have_same_sql(
|
210
|
+
"SELECT SUM(airline_aircraft_seat_classes.weighting * (airline_aircraft_seat_classes.seats + airline_aircraft_seat_classes.pitch) * 1.0) / SUM(airline_aircraft_seat_classes.weighting) FROM airline_aircraft_seat_classes WHERE airline_aircraft_seat_classes.seats IS NOT NULL AND airline_aircraft_seat_classes.pitch IS NOT NULL AND airline_aircraft_seat_classes.weighting > 0",
|
211
|
+
AirlineAircraftSeatClass.arel_table.weighted_average_relation(['seats', 'pitch'])
|
212
|
+
)
|
213
|
+
end
|
204
214
|
end
|
205
215
|
|
206
216
|
private
|
data/weighted_average.gemspec
CHANGED
@@ -17,7 +17,6 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
|
20
|
-
s.add_runtime_dependency 'activerecord', '~>3'
|
21
20
|
s.add_runtime_dependency 'activesupport', '~>3'
|
22
21
|
s.add_runtime_dependency 'arel', '>= 2'
|
23
22
|
|
@@ -26,6 +25,8 @@ Gem::Specification.new do |s|
|
|
26
25
|
s.add_development_dependency 'rake'
|
27
26
|
s.add_development_dependency 'mysql'
|
28
27
|
s.add_development_dependency 'pg'
|
28
|
+
s.add_development_dependency 'activerecord', '~>3'
|
29
|
+
s.add_development_dependency 'yard'
|
29
30
|
end
|
30
31
|
|
31
32
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: weighted_average
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -12,24 +12,8 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2012-05-
|
15
|
+
date: 2012-05-29 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
|
-
- !ruby/object:Gem::Dependency
|
18
|
-
name: activerecord
|
19
|
-
requirement: !ruby/object:Gem::Requirement
|
20
|
-
none: false
|
21
|
-
requirements:
|
22
|
-
- - ~>
|
23
|
-
- !ruby/object:Gem::Version
|
24
|
-
version: '3'
|
25
|
-
type: :runtime
|
26
|
-
prerelease: false
|
27
|
-
version_requirements: !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
|
-
requirements:
|
30
|
-
- - ~>
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '3'
|
33
17
|
- !ruby/object:Gem::Dependency
|
34
18
|
name: activesupport
|
35
19
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,6 +126,38 @@ dependencies:
|
|
142
126
|
- - ! '>='
|
143
127
|
- !ruby/object:Gem::Version
|
144
128
|
version: '0'
|
129
|
+
- !ruby/object:Gem::Dependency
|
130
|
+
name: activerecord
|
131
|
+
requirement: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ~>
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '3'
|
137
|
+
type: :development
|
138
|
+
prerelease: false
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
140
|
+
none: false
|
141
|
+
requirements:
|
142
|
+
- - ~>
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '3'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: yard
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
type: :development
|
154
|
+
prerelease: false
|
155
|
+
version_requirements: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ! '>='
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
145
161
|
description: Perform weighted averages, even across associations. Rails 3 only because
|
146
162
|
it uses ARel.
|
147
163
|
email:
|
@@ -151,12 +167,17 @@ extensions: []
|
|
151
167
|
extra_rdoc_files: []
|
152
168
|
files:
|
153
169
|
- .gitignore
|
170
|
+
- .yardopts
|
154
171
|
- CHANGELOG
|
155
172
|
- Gemfile
|
156
173
|
- LICENSE
|
157
|
-
- README.
|
174
|
+
- README.markdown
|
158
175
|
- Rakefile
|
159
176
|
- lib/weighted_average.rb
|
177
|
+
- lib/weighted_average/active_record_base_class_methods.rb
|
178
|
+
- lib/weighted_average/active_record_relation_instance_methods.rb
|
179
|
+
- lib/weighted_average/arel_select_manager_instance_methods.rb
|
180
|
+
- lib/weighted_average/arel_table_instance_methods.rb
|
160
181
|
- lib/weighted_average/version.rb
|
161
182
|
- test/helper.rb
|
162
183
|
- test/test_weighted_average.rb
|