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 CHANGED
@@ -21,3 +21,6 @@ pkg
21
21
  ## PROJECT::SPECIFIC
22
22
  test/test.log
23
23
  Gemfile.lock
24
+
25
+ .yardoc
26
+ doc/
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --no-private
2
+ --readme README.markdown
data/CHANGELOG CHANGED
@@ -1,3 +1,12 @@
1
+ 2.0.0 / 2012-05-29
2
+
3
+ * Enhancements
4
+
5
+ * Add Arel::SelectManager#weighted_average[_relation]
6
+ * Add Arel::Table#weighted_average[_relation]
7
+ * Simpler SQL output
8
+ * Per-method documentation
9
+
1
10
  1.1.0 / 2012-05-21
2
11
 
3
12
  * Bug fixes
@@ -1,8 +1,8 @@
1
- = weighted_average
1
+ # weighted_average
2
2
 
3
- Do weighted averages in ActiveRecord.
3
+ Do weighted averages in ARel.
4
4
 
5
- == Rationale
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
- == Usage
16
+ ## Usage
17
17
 
18
- Using <tt>FlightSegment</tt> from {Brighter Planet's}[http://brighterplanet.com] {earth gem}[http://rubygems.org/gems/earth]:
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
- == Copyright
28
+ ## Copyright
29
29
 
30
- Copyright (c) 2010 Seamus Abshere, Andy Rossmeissl, Ian Hough, and Matt Kling. See LICENSE for details.
30
+ Copyright (c) 2012 Brighter Planet, Inc.
data/Rakefile CHANGED
@@ -7,3 +7,8 @@ Rake::TestTask.new(:test) do |test|
7
7
  end
8
8
 
9
9
  task :default => :test
10
+
11
+ require 'yard'
12
+ YARD::Rake::YardocTask.new do |y|
13
+ y.options << '--no-private'
14
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module WeightedAverage
2
- VERSION = '1.1.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -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
- # Returns a number.
7
- def weighted_average(*args)
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
- # [ `aircraft`.`foo`, `aircraft`.`baz` ]
51
- data_column_names = ::Array.wrap(data_column_names).map do |data_column_name|
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
- relation = select("(SUM(1.0 * (#{data_column_names.join(' + ')}) #{"/ #{disaggregate_by_column_name} " if disaggregate_by_column_name}* #{weighted_by_column_name}) / SUM(#{weighted_by_column_name})) AS weighted_average")
56
- data_column_names.each do |data_column_name|
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
- # avoid division by zero
61
- relation = relation.where("#{weighted_by_column_name} > 0")
62
- relation = relation.where("#{disaggregate_by_column_name} > 0") if disaggregate_by_column_name
63
-
64
- # FIXME this will break on through relationships, where it has to be :aircraft => :aircraft_class
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
- (defined?(::ActiveRecord::Associations::CollectionProxy) ? ::ActiveRecord::Associations::CollectionProxy : ::ActiveRecord::Associations::AssociationCollection).class_eval do
71
- def self.weighted_average(*args) # :nodoc:
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
@@ -1,6 +1,9 @@
1
+ require 'rubygems'
1
2
  require 'bundler/setup'
2
3
  require 'minitest/spec'
3
4
  require 'minitest/autorun'
5
+
6
+ require 'active_record'
4
7
  require 'cohort_analysis'
5
8
 
6
9
  require 'weighted_average'
@@ -48,14 +48,14 @@ describe WeightedAverage do
48
48
 
49
49
  it "does default weighting" do
50
50
  should_have_same_sql(
51
- "SELECT (SUM(1.0 * (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) AND (airline_aircraft_seat_classes.weighting > 0)",
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 (SUM(1.0 * (segments.distance) * segments.passengers) / SUM(segments.passengers)) AS weighted_average FROM segments WHERE (segments.distance IS NOT NULL) AND (segments.passengers > 0)",
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 (SUM(1.0 * (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) AND (airline_aircraft_seat_classes.weighting > 0)",
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 = 'aircraft_id = 1'
83
+ conditions = "aircraft_id = #{rand(1e11)}"
84
84
 
85
85
  should_have_same_sql(
86
- "SELECT (SUM(1.0 * (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) AND (airline_aircraft_seat_classes.weighting > 0)",
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 (SUM(1.0 * (segments.load_factor) * segments.passengers) / SUM(segments.passengers)) AS weighted_average FROM segments WHERE (#{conditions}) AND (segments.load_factor IS NOT NULL) AND (segments.passengers > 0)",
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 (SUM(1.0 * (aircraft.seats) * airline_aircraft_seat_classes.weighting) / SUM(airline_aircraft_seat_classes.weighting)) AS weighted_average 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)",
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 (SUM(1.0 * (aircraft.m3) * segments.passengers) / SUM(segments.passengers)) AS weighted_average FROM aircraft INNER JOIN segments ON segments.bts_aircraft_type = aircraft.bts_aircraft_type WHERE (aircraft.m3 IS NOT NULL) AND (segments.passengers > 0)",
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 (SUM(1.0 * (aircraft_deux.m3) * segments.passengers) / SUM(segments.passengers)) AS weighted_average 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)",
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 (SUM(1.0 * (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) AND (airline_aircraft_seat_classes.weighting > 0)",
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 (SUM(1.0 * (segments.load_factor) * segments.passengers) / SUM(segments.passengers)) AS weighted_average FROM segments WHERE (#{conditions}) AND (segments.load_factor IS NOT NULL) AND (segments.passengers > 0)",
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 (SUM(1.0 * (aircraft.seats) * airline_aircraft_seat_classes.weighting) / SUM(airline_aircraft_seat_classes.weighting)) AS weighted_average FROM aircraft INNER JOIN airline_aircraft_seat_classes ON airline_aircraft_seat_classes.aircraft_id = aircraft.id WHERE (454 != 999) AND (aircraft.seats IS NOT NULL) AND (airline_aircraft_seat_classes.weighting > 0)",
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 (SUM(1.0 * (aircraft.m3) * segments.passengers) / SUM(segments.passengers)) AS weighted_average FROM aircraft INNER JOIN segments ON segments.bts_aircraft_type = aircraft.bts_aircraft_type WHERE (aircraft.m3 > 1) AND (aircraft.m3 IS NOT NULL) AND (segments.passengers > 0)",
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 (SUM(1.0 * (segments.load_factor) / segments.departures_performed * segments.passengers) / SUM(segments.passengers)) AS weighted_average FROM segments WHERE (segments.load_factor IS NOT NULL) AND (segments.passengers > 0) AND (segments.departures_performed > 0)",
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 (SUM(1.0 * (segments.seats) * segments.passengers) / SUM(segments.passengers)) AS weighted_average 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 aircraft.aircraft_class_id = aircraft_classes.id AND (segments.seats IS NOT NULL) AND (segments.passengers > 0)",
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 cohort_scope gem)
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 (SUM(1.0 * (segments.load_factor) * segments.passengers) / SUM(segments.passengers)) AS weighted_average FROM segments WHERE (segments.payload = 5) AND (segments.load_factor IS NOT NULL) AND (segments.passengers > 0)",
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
- it "properly picks up table name" do
195
- c = Segment.connection
196
- c.execute %{
197
- CREATE TEMPORARY TABLE moo LIKE #{Segment.quoted_table_name}
198
- }
199
- relation = ActiveRecord::Relation.new(Segment, Arel::Table.new(:moo))
200
- should_have_same_sql(
201
- "SELECT (SUM(1.0 * (moo.distance) * moo.passengers) / SUM(moo.passengers)) AS weighted_average FROM moo WHERE (moo.distance IS NOT NULL) AND (moo.passengers > 0)",
202
- relation.weighted_average_relation('distance', :weighted_by => 'passengers')
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
@@ -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: 1.1.0
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-21 00:00:00.000000000 Z
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.rdoc
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