weighted_average 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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