ar_aggregate_by_interval 1.1.3 → 1.1.4

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.
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