ar_aggregate_by_interval 1.1.3 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4beebc54e103e64f0d2ea5b3dc8947a033b4a217
4
- data.tar.gz: d3f4008258eba648f009f37006d16b44d31475b6
3
+ metadata.gz: d67fb1f746590e8801a28802ab2fd1aba5175f11
4
+ data.tar.gz: c5618c06679af82e4d078ed02ddc150040cad40f
5
5
  SHA512:
6
- metadata.gz: 5812bb1f2cbd9e838940e9e119db08126f244166d760796370b100e6913245b8665d3d4d3c6e4ca7a2f9fb5d7b8b37d1a7390dd1e345fde36d8a27b901f915d1
7
- data.tar.gz: ccbb37c325b454a1fe4759a4f392fa8657054095fc13e2a3bd2eddc4b0307d351617b1243a4d07139efc8e4068334da1cea25a97c7ae208bcf7243f4a4f2d43e
6
+ metadata.gz: ccdd2612b245ebac746c3e5bb3756a8a021488de55325b5a378fe868ed08c5f1f1987bfd67612257c281d9d5f13cd8997de764fe9c1f00d9f3572037ab0945d0
7
+ data.tar.gz: 9ccfddcab1829beec3c274ca62bea99f886ef6e9750a3a729d9353cd45e1ca30c588af7212e70fe3bd371ed82098c3b1154e2af7606743f1c1230eb5c73cd3f4
data/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.
3
3
  This project adheres to [Semantic Versioning](http://semver.org/).
4
4
  This file adheres to [Keep a changelog](http://keepachangelog.com/).
5
5
 
6
+ ## [1.1.4] - 2015-03-08 (maintenance)
7
+ ### Changed
8
+ - Moved functionality from constructors to methods
9
+ - Build hash from `select_rows` instead of AR objects (performance, simplicity)
10
+ - Driver of functionality now in method_missing (decoupling)
11
+
6
12
  ## [1.1.3] - 2015-03-02
7
13
  ### Fixed
8
14
  - Fix Postgres queries due to AR injecting order clause
@@ -30,11 +30,24 @@ module ArAggregateByInterval
30
30
  hash_args[col] = hash_args[col].intern if hash_args[col]
31
31
  end
32
32
 
33
- QueryRunner.new(self, {
33
+ # build query object
34
+ query_runner = QueryRunner.new(self, {
34
35
  aggregate_function: aggregate_function,
35
36
  interval: interval
36
37
  }.merge(hash_args))
37
38
 
39
+ # actually run SQL and return a hash of dates => vals
40
+ date_values_hash = query_runner.run_query
41
+
42
+ # takes hash and fills in missing dates
43
+ # this QueryResult object has 2 attributes: values_and_dates, values
44
+ QueryResult.new({
45
+ date_values_hash: date_values_hash,
46
+ from: query_runner.from,
47
+ to: query_runner.to,
48
+ interval: query_runner.interval
49
+ })
50
+
38
51
  end
39
52
 
40
53
  end
@@ -9,10 +9,10 @@ module ArAggregateByInterval
9
9
  class QueryResult
10
10
 
11
11
  VALID_HASH_ARGS = {
12
- ar_result: -> (v) { v.respond_to?(:to_a) },
12
+ date_values_hash: [Hash],
13
13
 
14
- # hash with 1 key where the key is a date column and value is the column being aggegated
15
- ar_result_select_col_mapping: -> (v) { v.is_a?(Hash) && v.size == 1 },
14
+ # # hash with 1 key where the key is a date column and value is the column being aggegated
15
+ # ar_result_select_col_mapping: -> (v) { v.is_a?(Hash) && v.size == 1 },
16
16
 
17
17
  from: [Date, DateTime, Time, ActiveSupport::TimeWithZone],
18
18
  to: [Date, DateTime, Time, ActiveSupport::TimeWithZone],
@@ -20,17 +20,18 @@ module ArAggregateByInterval
20
20
  interval: -> (v) { Utils.ruby_strftime_map.keys.include?(v) }
21
21
  }
22
22
 
23
- def initialize(args)
24
- validate_args!(args)
23
+ def initialize(hash_args)
24
+ ClassyHash.validate(hash_args, VALID_HASH_ARGS)
25
25
 
26
- @dates_values_hash = Utils.ar_to_hash(args[:ar_result], args[:ar_result_select_col_mapping])
27
- @date_iterator_method = Utils::DATE_ITERATOR_METHOD_MAP[args[:interval]]
26
+ # @dates_values_hash = Utils.ar_to_hash(args[:ar_result], args[:ar_result_select_col_mapping])
27
+ @dates_values_hash = hash_args[:date_values_hash]
28
+ @date_iterator_method = Utils::DATE_ITERATOR_METHOD_MAP[hash_args[:interval]]
28
29
 
29
30
  # strformat to match the format out of the database
30
- @strftime_format = Utils.ruby_strftime_map[args[:interval]]
31
+ @strftime_format = Utils.ruby_strftime_map[hash_args[:interval]]
31
32
 
32
- @from = args[:from]
33
- @to = args[:to]
33
+ @from = hash_args[:from]
34
+ @to = hash_args[:to]
34
35
  end
35
36
 
36
37
  def values_and_dates
@@ -42,16 +43,20 @@ module ArAggregateByInterval
42
43
  end
43
44
  end
44
45
 
46
+ def values
47
+ @values ||= values_and_dates.collect { |hash| hash[:value] }
48
+ end
49
+
45
50
  private
46
51
 
47
- def validate_args!(hash_args)
48
- ClassyHash.validate(hash_args, VALID_HASH_ARGS)
49
- first_res = hash_args[:ar_result].first
50
- keys = hash_args[:ar_result_select_col_mapping].to_a.flatten
51
- if first_res && keys.any? { |key| !first_res.respond_to?(key) }
52
- raise RuntimeError.new("the collection passed does not respond to all attribs: #{keys}")
53
- end
54
- end
52
+ # def validate_args!(hash_args)
53
+ # ClassyHash.validate(hash_args, VALID_HASH_ARGS)
54
+ # first_res = hash_args[:ar_result].first
55
+ # keys = hash_args[:ar_result_select_col_mapping].to_a.flatten
56
+ # if first_res && keys.any? { |key| !first_res.respond_to?(key) }
57
+ # raise RuntimeError.new("the collection passed does not respond to all attribs: #{keys}")
58
+ # end
59
+ # end
55
60
 
56
61
  def array_of_dates
57
62
  @array_of_dates ||= @from.to_date.send(@date_iterator_method, @to.to_date).map do |date|
@@ -19,41 +19,58 @@ module ArAggregateByInterval
19
19
  aggregate_column: [:optional, Symbol, NilClass] # required when using sum (as opposed to count)
20
20
  }
21
21
 
22
- attr_reader :values, :values_and_dates
22
+ attr_reader :values, :values_and_dates, :from, :to, :interval
23
23
 
24
24
  def initialize(ar_model, hash_args)
25
25
 
26
26
  validate_args!(hash_args)
27
27
 
28
- from = normalize_from(hash_args[:from], hash_args[:interval])
29
- to = normalize_to(hash_args[:to] || Time.zone.try(:now) || Time.now, hash_args[:interval])
28
+ @ar_model = ar_model
30
29
 
31
- db_vendor_select_for_date_function =
30
+ @from = normalize_from(hash_args[:from], hash_args[:interval])
31
+ @to = normalize_to(hash_args[:to] || Time.zone.try(:now) || Time.now, hash_args[:interval])
32
+
33
+ @db_vendor_select =
32
34
  Utils.select_for_grouping_column(hash_args[:group_by_column])[hash_args[:interval]]
33
35
 
34
- ar_result = ar_model.
35
- select("#{hash_args[:aggregate_function]}(#{hash_args[:aggregate_column] || '*'}) as totalchunked__").
36
- select("#{db_vendor_select_for_date_function} as datechunk__").
37
- group('datechunk__').
38
- where(["#{hash_args[:group_by_column]} >= ? and #{hash_args[:group_by_column]} <= ?", from, to]).
39
- order(nil)
40
-
41
- # fill the gaps of the sql results
42
- agg_int = QueryResult.new({
43
- ar_result: ar_result,
44
- ar_result_select_col_mapping: {'datechunk__' => 'totalchunked__'},
45
- from: from,
46
- to: to,
47
- interval: hash_args[:interval]
48
- })
49
-
50
- @values_and_dates = agg_int.values_and_dates
51
- @values = @values_and_dates.collect { |hash| hash[:value] }
36
+ @aggregate_function = hash_args[:aggregate_function]
37
+ @aggregate_column = hash_args[:aggregate_column]
38
+ @group_by_column = hash_args[:group_by_column]
39
+
40
+ @interval = hash_args[:interval]
41
+ end
42
+
43
+ def run_query
44
+ # actually run query
45
+ array_of_pairs = ActiveRecord::Base.connection.select_rows(to_sql)
46
+
47
+ # workaround ActiveRecord's automatic casting to Date objects
48
+ # (ideally we could return raw values straight from ActiveRecord to avoid this expensive N)
49
+ array_of_pairs.collect! do |date_val_pair|
50
+ date_val_pair.collect(&:to_s)
51
+ end
52
52
 
53
+ # convert the array of key/values to a hash
54
+ Hash[array_of_pairs]
53
55
  end
54
56
 
55
57
  private
56
58
 
59
+ def to_sql
60
+ # first col is date, second col is actual value
61
+ query = @ar_model.
62
+ select("#{@db_vendor_select} as datechunk__").
63
+ select("#{@aggregate_function}(#{@aggregate_column || '*'}) as totalchunked__").
64
+ where(["#{@group_by_column} >= ? and #{@group_by_column} <= ?", @from, @to]).
65
+ group('datechunk__')
66
+
67
+ # workaround Postgres adapter's insistence of adding an order clause
68
+ query = query.order(nil)
69
+
70
+ # an string of the query to run
71
+ query.to_sql
72
+ end
73
+
57
74
  def validate_args!(hash_args)
58
75
  ClassyHash.validate(hash_args, VALID_HASH_ARGS)
59
76
  if hash_args[:aggregate_function] != 'count' && hash_args[:aggregate_column].blank?
@@ -1,3 +1,3 @@
1
1
  module ArAggregateByInterval
2
- VERSION = '1.1.3'
2
+ VERSION = '1.1.4'
3
3
  end
@@ -5,47 +5,41 @@ module ArAggregateByInterval
5
5
 
6
6
  describe Utils do
7
7
 
8
- context 'converting ar to hash' do
8
+ describe 'interval_inflector' do
9
9
 
10
- subject do
11
- described_class.ar_to_hash(ar_objs, mapping)
10
+ shared_examples 'working inflector' do |int, beg_end, expected|
11
+ it "works for #{beg_end}_#{int}" do
12
+ expect(described_class.interval_inflector(int, beg_end)).to eq expected
13
+ end
12
14
  end
13
15
 
14
- context 'normal values' do
15
- let(:ar_objs) do
16
- [
17
- OpenStruct.new({
18
- date_chunk__: '2014-01-01',
19
- value: 5
20
- })
21
- ]
22
- end
16
+ include_examples 'working inflector', 'daily', 'beginning', 'beginning_of_day'
17
+ include_examples 'working inflector', 'daily', 'end', 'end_of_day'
23
18
 
24
- let(:mapping) { { 'date_chunk__' => 'value' } }
19
+ include_examples 'working inflector', 'weekly', 'beginning', 'beginning_of_week'
20
+ include_examples 'working inflector', 'weekly', 'end', 'end_of_week'
25
21
 
26
- it 'works' do
27
- expect(subject).to eq({ '2014-01-01' => 5 })
28
- end
29
- end
22
+ include_examples 'working inflector', 'monthly', 'beginning', 'beginning_of_month'
23
+ include_examples 'working inflector', 'monthly', 'end', 'end_of_month'
30
24
 
31
- context 'arbitrary values' do
32
- let(:ar_objs) do
33
- [
34
- OpenStruct.new({
35
- id: OpenStruct.new({}),
36
- something: 1
37
- })
38
- ]
39
- end
40
- let(:mapping) { { 'id' => 'something' } }
25
+ end
26
+
27
+ describe 'to_f_or_i_or_s' do
41
28
 
42
- it 'does not cast or change any objects' do
43
- expect(subject.keys.first).to be_a(OpenStruct)
44
- expect(subject.values.first).to be_a(Integer)
29
+ shared_examples 'working Numeric converter' do |arg1, expected_class|
30
+ it "works for #{arg1.class.name} to #{expected_class.name}" do
31
+ expect(described_class.to_f_or_i_or_s(arg1)).to be_instance_of(expected_class)
45
32
  end
46
33
  end
47
34
 
35
+ include_examples 'working Numeric converter', '1.1', Float
36
+ include_examples 'working Numeric converter', 1.1, Float
37
+ include_examples 'working Numeric converter', '1.0', Fixnum
38
+ include_examples 'working Numeric converter', 1.0, Fixnum
39
+ include_examples 'working Numeric converter', 1, Fixnum
40
+
48
41
  end
42
+
49
43
  end
50
44
 
51
- end
45
+ end
@@ -5,25 +5,27 @@ describe ArAggregateByInterval do
5
5
  before(:all) do |example|
6
6
  @from = DateTime.parse '2013-08-05'
7
7
  @to = @from
8
- blog = Blog.create arbitrary_number: 10, created_at: @from
9
- blog.page_views.create date: @from
8
+ blog1 = Blog.create arbitrary_number: 10, created_at: @from
9
+ blog2 = Blog.create arbitrary_number: 20, created_at: @from
10
+ blog1.page_views.create date: @from
11
+ blog1.page_views.create date: @from
10
12
  end
11
13
 
12
14
  shared_examples_for 'count .values_and_dates' do
13
15
  it 'returns value and date with expected values' do
14
- expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 1])
16
+ expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 2])
15
17
  end
16
18
  end
17
19
 
18
20
  shared_examples_for 'sum .values_and_dates' do
19
21
  it 'returns value and date with expected values' do
20
- expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 10])
22
+ expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 30])
21
23
  end
22
24
  end
23
25
 
24
26
  shared_examples_for 'avg .values_and_dates' do
25
27
  it 'returns value and date with expected values' do
26
- expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 10])
28
+ expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 15])
27
29
  end
28
30
  end
29
31
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_aggregate_by_interval
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Otto
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-03 00:00:00.000000000 Z
11
+ date: 2015-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -161,7 +161,6 @@ files:
161
161
  - lib/ar_aggregate_by_interval/version.rb
162
162
  - spec/ar_bootstrap/init.rb
163
163
  - spec/ar_bootstrap/schema.rb
164
- - spec/lib/ar_aggregate_by_interval/query_runner_spec.rb
165
164
  - spec/lib/ar_aggregate_by_interval/utils_spec.rb
166
165
  - spec/lib/ar_aggregate_by_interval_spec.rb
167
166
  - spec/spec_helper.rb
@@ -191,7 +190,6 @@ summary: add [sum|count]_[daily|weekly|monthly] to your AR models for MySQL AND
191
190
  test_files:
192
191
  - spec/ar_bootstrap/init.rb
193
192
  - spec/ar_bootstrap/schema.rb
194
- - spec/lib/ar_aggregate_by_interval/query_runner_spec.rb
195
193
  - spec/lib/ar_aggregate_by_interval/utils_spec.rb
196
194
  - spec/lib/ar_aggregate_by_interval_spec.rb
197
195
  - spec/spec_helper.rb
@@ -1,66 +0,0 @@
1
- require 'ar_aggregate_by_interval/query_runner'
2
-
3
- module ArAggregateByInterval
4
- describe QueryRunner do
5
-
6
- subject do
7
- described_class.new(Blog, {
8
- aggregate_function: aggregate_function,
9
- aggregate_column: (aggregate_column rescue nil),
10
- interval: interval,
11
- group_by_column: :created_at,
12
- from: from,
13
- to: to
14
- })
15
- end
16
-
17
- context 'one week duration' do
18
-
19
- # monday
20
- let(:from) { DateTime.parse '2013-08-05' }
21
- # sunday
22
- let(:to) { DateTime.parse '2013-08-11' }
23
-
24
- before do |example|
25
- Blog.create [
26
- {arbitrary_number: 10, created_at: from},
27
- {arbitrary_number: 20, created_at: from}
28
- ]
29
- end
30
-
31
- context 'avg daily' do
32
- let(:interval) { 'daily' }
33
- let(:aggregate_function) { 'avg' }
34
- let(:aggregate_column) { :arbitrary_number }
35
-
36
- describe '.values' do
37
- it 'returns an array of size 7' do
38
- expect(subject.values.size).to eq 7
39
- end
40
- it 'returns actual averages' do
41
- expect(subject.values).to eq([15, 0, 0, 0, 0, 0, 0])
42
- end
43
- end
44
- end
45
-
46
- context 'count weekly' do
47
- let(:interval) { 'weekly' }
48
- let(:aggregate_function) { 'count' }
49
-
50
- describe '.values' do
51
- it 'returns exactly 1 element array with 1' do
52
- expect(subject.values).to eq([2])
53
- end
54
- end
55
-
56
- describe '.value_and_dates' do
57
- it 'returns value and date with expected values' do
58
- expect(subject.values_and_dates).to eq([date: from.beginning_of_week.to_date, value: 2])
59
- end
60
- end
61
- end
62
-
63
- end
64
- end
65
-
66
- end