event-counter 0.1.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0bd855f0622ba4699ac98ae5f11d635fff270bbc
4
- data.tar.gz: 996454629b26cf7eb0ce21338f5a5c443fd79713
3
+ metadata.gz: a5e4803060e3164a0eaf04624b6d5002c0789dd4
4
+ data.tar.gz: 58e249fde274f8d405e6d9d872fbdef6ca799a87
5
5
  SHA512:
6
- metadata.gz: 16a2277eeb66b50ca1110700ab4fc40497714b5fd928599e4310da1055ff62e38b44a4db6cd004f447bea28169219bb628f1a32c43265bd8e74f404a20a423f3
7
- data.tar.gz: c67d8bcab89dcf2bdfd2f99b466e439269af57e13647c7fd13f93594efe4a19e1d49d4845e0d591080b5c31e0b4230e5c1f3e4ae0bdd6d8f20af31df2a5bb882
6
+ metadata.gz: 9002dad388634f8afd70f9c8f57591e32eb1e8ed0ac859daee6b15609e2699c3cc90ee1103e5c15f2a3aa2430a02df6a00bea150f11f21b34e52022e71266120
7
+ data.tar.gz: e321c6a69d69f52b1b1b7b53c65fa14865723ed46e037a8ad3e3b4b4ebbf0017577c0d627f1c69d82d403753886dd0a254454ca31c94740196a7789265f6d81d
data/.travis.yml CHANGED
@@ -9,7 +9,7 @@ gemfile:
9
9
  - gemfiles/pg_ar_32.gemfile
10
10
  - gemfiles/pg_ar_40.gemfile
11
11
 
12
- script: RUN_ALL=true bundle exec rspec spec
12
+ script: bundle exec rspec spec
13
13
 
14
14
  services:
15
15
  - postgresql
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # EventCounter Changelog
2
2
 
3
+ 0.3.0 (2014-11-08)
4
+
5
+ * counter definition method name changed
6
+
7
+ 0.2.0 (2014-11-05)
8
+
9
+ * Improved timezones support
10
+
3
11
  0.1.1 (2014-10-22)
4
12
 
5
13
  * Fixed issue with creating time in time zone
data/README.md CHANGED
@@ -127,7 +127,7 @@ Article.data_for(:views, interval: :day, range: range)
127
127
  Add gem to Gemfile
128
128
 
129
129
  ```ruby
130
- gem 'event_counter'
130
+ gem 'event-counter'
131
131
  ```
132
132
 
133
133
  Create migration `rails g migration create_event_counters` with the
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- event-counter (0.1.0)
4
+ event-counter (0.3.0)
5
5
  activerecord (>= 3)
6
6
  activesupport (>= 3)
7
7
  pg (~> 0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- event-counter (0.1.0)
4
+ event-counter (0.3.0)
5
5
  activerecord (>= 3)
6
6
  activesupport (>= 3)
7
7
  pg (~> 0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- event-counter (0.1.0)
4
+ event-counter (0.3.0)
5
5
  activerecord (>= 3)
6
6
  activesupport (>= 3)
7
7
  pg (~> 0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- event-counter (0.1.0)
4
+ event-counter (0.3.0)
5
5
  activerecord (>= 3)
6
6
  activesupport (>= 3)
7
7
  pg (~> 0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- event-counter (0.1.0)
4
+ event-counter (0.3.0)
5
5
  activerecord (>= 3)
6
6
  activesupport (>= 3)
7
7
  pg (~> 0)
data/lib/event_counter.rb CHANGED
@@ -31,20 +31,20 @@ class EventCounter < ActiveRecord::Base
31
31
 
32
32
  attrs = { created_at: on_time }
33
33
 
34
- if force && (found = scoped_relatiion.where(attrs).first)
34
+ if force && (found = scoped_relation.where(attrs).first)
35
35
  found.reset_value(val)
36
36
  else
37
37
  attrs.merge!(value: val)
38
- scoped_relatiion.create!(attrs)
38
+ scoped_relation.create!(attrs)
39
39
  end
40
40
  end
41
41
 
42
42
  def self.current_interval
43
- scoped_relatiion.proxy_association.owner.event_counters[counter_name]
43
+ scoped_relation.proxy_association.owner.event_counters[counter_name]
44
44
  end
45
45
 
46
46
  def self.counter_name
47
- scoped_relatiion.proxy_association.reflection.name
47
+ scoped_relation.proxy_association.reflection.name
48
48
  end
49
49
 
50
50
  def self.change(val = 1, vector: :up, on_time: nil, force: nil)
@@ -71,7 +71,7 @@ class EventCounter < ActiveRecord::Base
71
71
  end
72
72
  end
73
73
 
74
- def self.scoped_relatiion
74
+ def self.scoped_relation
75
75
  ActiveRecord::VERSION::MAJOR > 3 ? where(nil) : scoped
76
76
  end
77
77
 
@@ -10,7 +10,7 @@ class EventCounter < ActiveRecord::Base
10
10
 
11
11
  # :nodoc:
12
12
  module ClassMethods
13
- def event_counter_for(name, interval)
13
+ def has_counter(name, interval)
14
14
  event_counters[name] = interval
15
15
 
16
16
  clause = { name: name.to_s }
@@ -68,16 +68,16 @@ class EventCounter < ActiveRecord::Base
68
68
  day: 1.day
69
69
  }.freeze
70
70
 
71
- def data_for(name, id = nil, interval: nil, range: nil, raw: nil, tz: nil)
71
+ def data_for(name, id = nil, interval: nil, range: nil, raw: nil)
72
72
  interval = normalize_interval!(name, interval)
73
73
 
74
74
  range = normalize_range!(range, interval) if range
75
75
 
76
- tz ||= (Time.zone || 'UTC')
77
- tz_abbr = tz.now.zone
76
+ tz = Time.zone.tzinfo.identifier
77
+ tz_storage = (default_timezone == :utc ? 'UTC' : Time.now.zone)
78
78
 
79
79
  subq = EventCounter
80
- .select(subq_select(interval, tz_abbr))
80
+ .select(subq_select(interval, tz))
81
81
  .where(name: name, countable_type: self)
82
82
  .where(id && { countable_id: id })
83
83
  .within(range)
@@ -85,16 +85,16 @@ class EventCounter < ActiveRecord::Base
85
85
  .order("1")
86
86
  .to_sql
87
87
 
88
- sql = <<-SQL.squish!
88
+ q = <<-SQL.squish!
89
89
  SELECT created_at, value
90
- FROM (#{series(interval, range, tz_abbr)}) intervals
90
+ FROM (#{series(interval, tz, range)}) intervals
91
91
  LEFT OUTER JOIN (#{subq}) counters USING (created_at)
92
92
  ORDER BY 1
93
93
  SQL
94
94
 
95
- result = connection.execute(sql).to_a
95
+ result = connection.execute(q).to_a
96
96
 
97
- raw ? result : normalize_counters_data(result, tz)
97
+ raw ? result : normalize_counters_data!(result)
98
98
  end
99
99
 
100
100
  def subq_select(interval, tz)
@@ -104,69 +104,107 @@ class EventCounter < ActiveRecord::Base
104
104
  def subq_extract(interval, tz)
105
105
  case interval
106
106
  when Symbol
107
- "date_trunc(#{sanitize(interval)}, #{tstamp_tz('created_at', tz)})"
107
+ dtrunc(interval, 'created_at', tz)
108
108
  else
109
- time = <<-SQL
110
- floor(EXTRACT(EPOCH FROM created_at) /
111
- #{sanitize(interval)})::int * #{sanitize(interval)}
112
- SQL
113
- tstamp_tz("to_timestamp(#{time})", tz)
109
+ time = floor_tstamp('created_at', interval)
110
+ if default_timezone == :utc
111
+ "to_timestamp(#{time})"
112
+ else
113
+ at_tz("to_timestamp(#{time})::timestamp", Time.new.zone)
114
+ end
114
115
  end
115
116
  end
116
117
 
117
- def series(interval, range, tz)
118
- a =
119
- case interval
120
- when Symbol
121
- series_for_symbol(interval, range, tz)
122
- else
123
- series_for_integer(interval, range, tz)
124
- end
125
- EventCounter.within(range).select(<<-SQL).to_sql
126
- count(*), generate_series(#{a[0]}, #{a[1] }, #{a[2]}) AS created_at
118
+ def floor_tstamp(tstamp, interval)
119
+ <<-SQL
120
+ floor(EXTRACT(EPOCH FROM #{tstamp}) /
121
+ #{sanitize(interval)})::int * #{sanitize(interval)}
127
122
  SQL
128
123
  end
129
124
 
130
- def series_for_symbol(interval, range, tz)
131
- interval_sql = "interval '1 #{interval}'"
125
+ def series(*args)
126
+ args.first.is_a?(Symbol) ? series_symbol(*args) : series_integer(*args)
127
+ end
128
+
129
+ def series_symbol(interval, tz, range = nil)
132
130
  if range
133
- a = [
134
- dtrunc(interval, sanitize(range.min).to_s, tz),
135
- dtrunc(interval, sanitize(range.max).to_s, tz),
136
- interval_sql
137
- ]
131
+ series_symbol_with_range(interval, tz, range)
138
132
  else
139
- a = [
140
- dtrunc(interval, 'min(created_at)', tz),
141
- dtrunc(interval, 'max(created_at)', tz),
142
- interval_sql
143
- ]
133
+ series_symbol_without_range(interval, tz)
144
134
  end
145
135
  end
146
136
 
147
- def series_for_integer(interval, range, tz)
148
- interval_sql = %Q(#{sanitize(interval)} * interval '1 seconds')
137
+ def series_symbol_with_range(interval, tz, range)
138
+ range_min, range_max = range.min, range.max
139
+ a = [
140
+ dtrunc(interval, sanitize(range_min.to_s(:db)), tz),
141
+ dtrunc(interval, sanitize(range_max.to_s(:db)), tz),
142
+ interval_symbol(interval)
143
+ ]
144
+
145
+ "SELECT generate_series(#{a[0]}, #{a[1]}, #{a[2]}) AS created_at"
146
+ end
147
+
148
+ def series_symbol_without_range(interval, tz)
149
+ a = [
150
+ dtrunc(interval, 'min(created_at)', tz),
151
+ dtrunc(interval, 'max(created_at)', tz),
152
+ interval_symbol(interval)
153
+ ]
154
+ EventCounter.select(<<-SQL).to_sql
155
+ generate_series(#{a[0]}, #{a[1]}, #{a[2]}) AS created_at
156
+ SQL
157
+ end
158
+
159
+ def series_integer(interval, tz, range = nil)
149
160
  if range
161
+ series_integer_with_range(interval, tz, range)
162
+ else
163
+ series_integer_without_range(interval, tz)
164
+ end
165
+ end
166
+
167
+ def series_integer_with_range(interval, tz, range = nil)
168
+ interval_sql = %Q(#{sanitize(interval)} * interval '1 seconds')
169
+ range_min, range_max = range.min.to_s(:db), range.max.to_s(:db)
170
+
171
+ a = [ sanitize(range_min), sanitize(range_max), interval_sql ]
172
+ <<-SQL
173
+ SELECT generate_series(#{a[0]}, #{a[1]}, #{a[2]}) AS created_at
174
+ SQL
175
+ end
176
+
177
+ def series_integer_without_range(interval, tz)
178
+ interval_sql = sanitize(interval)
179
+ if default_timezone == :utc
150
180
  a = [
151
- tstamp_tz("to_timestamp(#{sanitize(range.min.to_i)})", tz),
152
- tstamp_tz("to_timestamp(#{sanitize(range.max.to_i)})", tz),
181
+ floor_tstamp('min(created_at)', interval),
182
+ floor_tstamp('max(created_at)', interval),
153
183
  interval_sql
154
184
  ]
155
185
  else
186
+ z = Time.new.zone
156
187
  a = [
157
- tstamp_tz('min(created_at)', tz),
158
- tstamp_tz('max(created_at)', tz),
188
+ floor_tstamp(at_tz('min(created_at)', z), interval),
189
+ floor_tstamp(at_tz('max(created_at)', z), interval),
159
190
  interval_sql
160
191
  ]
161
192
  end
193
+ EventCounter.select(<<-SQL).to_sql
194
+ to_timestamp(generate_series(#{a[0]}, #{a[1]}, #{a[2]})) AS created_at
195
+ SQL
196
+ end
197
+
198
+ def interval_symbol(interval)
199
+ "interval #{sanitize(interval).insert(1, '1 ')}"
162
200
  end
163
201
 
164
- def dtrunc(interval, value, tz)
165
- "date_trunc(#{sanitize(interval)}, #{tstamp_tz(value, tz)})"
202
+ def dtrunc(interval, str, tz)
203
+ "date_trunc(#{sanitize(interval)}, #{at_tz("#{str}::timestamptz", tz)})"
166
204
  end
167
205
 
168
- def tstamp_tz(str, tz)
169
- "#{str}::timestamptz AT TIME ZONE #{sanitize(tz)}"
206
+ def at_tz(str, tz)
207
+ "#{str} AT TIME ZONE #{sanitize(tz)}"
170
208
  end
171
209
 
172
210
  def counter_error!(*args)
@@ -209,9 +247,9 @@ class EventCounter < ActiveRecord::Base
209
247
  interval.is_a?(Symbol) ? INTERVALS[interval] : interval
210
248
  end
211
249
 
212
- def normalize_counters_data(data, tz)
213
- Time.use_zone(tz) do
214
- data.map { |i| [ Time.zone.parse(i['created_at']), i['value'].to_i ] }
250
+ def normalize_counters_data!(data)
251
+ data.map do |i|
252
+ [ Time.zone.parse(i['created_at']), i['value'].to_i ]
215
253
  end
216
254
  end
217
255
 
@@ -1,4 +1,4 @@
1
1
  # This module contains VERSION
2
2
  module EventCounterVersion
3
- VERSION = '0.1.1'
3
+ VERSION = '0.3.0'
4
4
  end
@@ -75,311 +75,402 @@ end
75
75
  describe Ball do
76
76
  let(:ball) { Ball.create! }
77
77
 
78
- it 'creates a new counter while incrementing' do
79
- expect {
80
- expect(ball.up!(:rotations)).to be_a(EventCounter)
81
- }.to change { EventCounter.count }.by(1)
78
+ shared_examples 'default behavior' do
82
79
 
83
- on_time = Time.zone.local(2011, 11, 11, 11, 11)
84
- expect {
85
- expect(ball.up!(:rotations, on_time: on_time))
86
- .to be_a(EventCounter)
87
- }.to change { EventCounter.count }.by(1)
80
+ it 'creates a new counter while incrementing' do
81
+ expect {
82
+ expect(ball.up!(:rotations)).to be_a(EventCounter)
83
+ }.to change { EventCounter.count }.by(1)
88
84
 
89
- on_time = Time.zone.local(2012, 12, 12, 12, 12)
90
- expect {
91
- expect(ball.up!(:rotations, 5, on_time: on_time))
92
- .to be_a(EventCounter)
93
- }.to change { EventCounter.count }.by(1)
94
- end
85
+ on_time = Time.zone.local(2011, 11, 11, 11, 11)
86
+ expect {
87
+ expect(ball.up!(:rotations, on_time: on_time))
88
+ .to be_a(EventCounter)
89
+ }.to change { EventCounter.count }.by(1)
95
90
 
96
- it 'creates a new counter while decrementing' do
97
- expect {
98
- expect(ball.down!(:rotations)).to be_a(EventCounter)
99
- }.to change { EventCounter.count }.by(1)
91
+ on_time = Time.zone.local(2012, 12, 12, 12, 12)
92
+ expect {
93
+ expect(ball.up!(:rotations, 5, on_time: on_time))
94
+ .to be_a(EventCounter)
95
+ }.to change { EventCounter.count }.by(1)
96
+ end
100
97
 
101
- on_time = Time.zone.local(2011, 11, 11, 11, 11)
102
- expect {
103
- expect(ball.down!(:rotations, on_time: on_time))
104
- .to be_a(EventCounter)
105
- }.to change { EventCounter.count }.by(1)
98
+ it 'creates a new counter while decrementing' do
99
+ expect {
100
+ expect(ball.down!(:rotations)).to be_a(EventCounter)
101
+ }.to change { EventCounter.count }.by(1)
106
102
 
107
- on_time = Time.zone.local(2012, 12, 12, 12, 12)
108
- expect {
109
- expect(ball.down!(:rotations, 5, on_time: on_time))
110
- .to be_a(EventCounter)
111
- }.to change { EventCounter.count }.by(1)
112
- end
103
+ on_time = Time.zone.local(2011, 11, 11, 11, 11)
104
+ expect {
105
+ expect(ball.down!(:rotations, on_time: on_time))
106
+ .to be_a(EventCounter)
107
+ }.to change { EventCounter.count }.by(1)
113
108
 
114
- it 'increments existent counter with default value' do
115
- counter = ball.rotations.make
109
+ on_time = Time.zone.local(2012, 12, 12, 12, 12)
110
+ expect {
111
+ expect(ball.down!(:rotations, 5, on_time: on_time))
112
+ .to be_a(EventCounter)
113
+ }.to change { EventCounter.count }.by(1)
114
+ end
115
+
116
+ it 'increments existent counter with default value' do
117
+ counter = ball.rotations.make
116
118
 
117
- expect {
118
119
  expect {
119
- expect(ball.up!(:rotations)).to be_a(EventCounter)
120
- }.to change { counter.reload.value }.from(1).to(2)
121
- }.to_not change { EventCounter.count }
122
- end
120
+ expect {
121
+ expect(ball.up!(:rotations)).to be_a(EventCounter)
122
+ }.to change { counter.reload.value }.from(1).to(2)
123
+ }.to_not change { EventCounter.count }
124
+ end
123
125
 
124
- it 'decrements existent counter with default value' do
125
- counter = ball.rotations.make(- 1)
126
+ it 'decrements existent counter with default value' do
127
+ counter = ball.rotations.make(- 1)
126
128
 
127
- expect {
128
129
  expect {
129
- expect(ball.down!(:rotations)).to be_a(EventCounter)
130
- }.to change { counter.reload.value }.from(-1).to(-2)
131
- }.to_not change { EventCounter.count }
132
- end
130
+ expect {
131
+ expect(ball.down!(:rotations)).to be_a(EventCounter)
132
+ }.to change { counter.reload.value }.from(-1).to(-2)
133
+ }.to_not change { EventCounter.count }
134
+ end
133
135
 
134
- it 'increments existent counter by a specified value' do
135
- counter = ball.rotations.make
136
+ it 'increments existent counter by a specified value' do
137
+ counter = ball.rotations.make
136
138
 
137
- expect {
138
139
  expect {
139
- expect(ball.up!(:rotations, 3)).to be_a(EventCounter)
140
- }.to change { counter.reload.value }.from(1).to(4)
141
- }.to_not change { EventCounter.count }
142
- end
140
+ expect {
141
+ expect(ball.up!(:rotations, 3)).to be_a(EventCounter)
142
+ }.to change { counter.reload.value }.from(1).to(4)
143
+ }.to_not change { EventCounter.count }
144
+ end
143
145
 
144
- it 'decrements existent counter by a specified value' do
145
- counter = ball.rotations.make 3
146
+ it 'decrements existent counter by a specified value' do
147
+ counter = ball.rotations.make 3
146
148
 
147
- expect {
148
149
  expect {
149
- expect(ball.down!(:rotations, 5)).to be_a(EventCounter)
150
- }.to change { counter.reload.value }.from(3).to(-2)
151
- }.to_not change { EventCounter.count }
152
- end
150
+ expect {
151
+ expect(ball.down!(:rotations, 5)).to be_a(EventCounter)
152
+ }.to change { counter.reload.value }.from(3).to(-2)
153
+ }.to_not change { EventCounter.count }
154
+ end
153
155
 
154
- it 'increments existent counter on time with default value' do
155
- on_time = Time.zone.local(2012, 12, 12, 12, 12)
156
- counter = ball.rotations.make on_time: on_time
156
+ it 'increments existent counter on time with default value' do
157
+ on_time = Time.zone.local(2012, 12, 12, 12, 12)
158
+ counter = ball.rotations.make on_time: on_time
157
159
 
158
- expect {
159
160
  expect {
160
- expect(ball.up!(:rotations, on_time: on_time.change(min: 14)))
161
- }.to change { counter.reload.value }.from(1).to(2)
162
- }.to_not change { EventCounter.count }
163
- end
161
+ expect {
162
+ expect(ball.up!(:rotations, on_time: on_time.change(min: 14)))
163
+ }.to change { counter.reload.value }.from(1).to(2)
164
+ }.to_not change { EventCounter.count }
165
+ end
164
166
 
165
- it 'decrements existent counter on time with default value' do
166
- on_time = Time.zone.local(2012, 12, 12, 12, 12)
167
- counter = ball.rotations.make on_time: on_time
167
+ it 'decrements existent counter on time with default value' do
168
+ on_time = Time.zone.local(2012, 12, 12, 12, 12)
169
+ counter = ball.rotations.make on_time: on_time
168
170
 
169
- expect {
170
171
  expect {
171
- expect(ball.down!(:rotations, on_time: on_time.change(min: 14)))
172
- }.to change { counter.reload.value }.from(1).to(0)
173
- }.to_not change { EventCounter.count }
174
- end
172
+ expect {
173
+ expect(ball.down!(:rotations, on_time: on_time.change(min: 14)))
174
+ }.to change { counter.reload.value }.from(1).to(0)
175
+ }.to_not change { EventCounter.count }
176
+ end
175
177
 
176
- it 'increments existent counter on time with specified value' do
177
- on_time = Time.zone.local(2012, 12, 12, 12, 12)
178
- counter = ball.rotations.make 2, on_time: on_time
178
+ it 'increments existent counter on time with specified value' do
179
+ on_time = Time.zone.local(2012, 12, 12, 12, 12)
180
+ counter = ball.rotations.make 2, on_time: on_time
179
181
 
180
- expect {
181
182
  expect {
182
- expect(ball.up!(:rotations, 3, on_time: on_time.change(min: 14)))
183
- }.to change { counter.reload.value }.from(2).to(5)
184
- }.to_not change { EventCounter.count }
185
- end
183
+ expect {
184
+ expect(ball.up!(:rotations, 3, on_time: on_time.change(min: 14)))
185
+ }.to change { counter.reload.value }.from(2).to(5)
186
+ }.to_not change { EventCounter.count }
187
+ end
186
188
 
187
- it 'decrements existent counter on time with specified value' do
188
- on_time = Time.zone.local(2012, 12, 12, 12, 12)
189
- counter = ball.rotations.make 2, on_time: on_time
189
+ it 'decrements existent counter on time with specified value' do
190
+ on_time = Time.zone.local(2012, 12, 12, 12, 12)
191
+ counter = ball.rotations.make 2, on_time: on_time
190
192
 
191
- expect {
192
193
  expect {
193
- expect(ball.down!(:rotations, 3, on_time: on_time.change(min: 14)))
194
- }.to change { counter.reload.value }.from(2).to(-1)
195
- }.to_not change { EventCounter.count }
196
- end
194
+ expect {
195
+ expect(ball.down!(:rotations, 3, on_time: on_time.change(min: 14)))
196
+ }.to change { counter.reload.value }.from(2).to(-1)
197
+ }.to_not change { EventCounter.count }
198
+ end
197
199
 
198
- it 'forces existent counter with new value' do
199
- counter = ball.rotations.make
200
+ it 'forces existent counter with new value' do
201
+ counter = ball.rotations.make
200
202
 
201
- expect {
202
203
  expect {
203
- expect(ball.rotations.make(5, force: true))
204
- .to be_a(EventCounter)
205
- }.to change { counter.reload.value }.from(1).to(5)
206
- }.to_not change { EventCounter.count }
207
- end
204
+ expect {
205
+ expect(ball.rotations.make(5, force: true))
206
+ .to be_a(EventCounter)
207
+ }.to change { counter.reload.value }.from(1).to(5)
208
+ }.to_not change { EventCounter.count }
209
+ end
208
210
 
209
- it 'forces existent counter on time with new value' do
210
- on_time = Time.zone.local(2012, 12, 12, 12, 12)
211
- counter = ball.rotations.make 2, on_time: on_time
211
+ it 'forces existent counter on time with new value' do
212
+ on_time = Time.zone.local(2012, 12, 12, 12, 12)
213
+ counter = ball.rotations.make 2, on_time: on_time
212
214
 
213
- expect {
214
215
  expect {
215
- expect(ball.rotations.make(5, force: true, on_time: on_time))
216
- .to be_a(EventCounter)
217
- }.to change { counter.reload.value }.from(2).to(5)
218
- }.to_not change { EventCounter.count }
219
- end
216
+ expect {
217
+ expect(ball.rotations.make(5, force: true, on_time: on_time))
218
+ .to be_a(EventCounter)
219
+ }.to change { counter.reload.value }.from(2).to(5)
220
+ }.to_not change { EventCounter.count }
221
+ end
220
222
 
221
- it 'raises error on wrong direction foc counter' do
222
- expect { ball.send(:rotate_counter, *[:rotations, vector: :wrong_direction]) }
223
- .to raise_error(EventCounter::CounterError, /wrong direction/i)
224
- end
223
+ it 'raises error on wrong direction foc counter' do
224
+ expect { ball.send(:rotate_counter, *[:rotations, vector: :wrong_direction]) }
225
+ .to raise_error(EventCounter::CounterError, /wrong direction/i)
226
+ end
225
227
 
226
- it 'raises error on unable to find counter' do
227
- expect { ball.up!(:unknown) }
228
- .to raise_error(EventCounter::CounterError, /unable to find/i)
229
- end
228
+ it 'raises error on unable to find counter' do
229
+ expect { ball.up!(:unknown) }
230
+ .to raise_error(EventCounter::CounterError, /unable to find/i)
231
+ end
230
232
 
231
- def setup_counters(countable_count = 1)
232
- [1, 1, 2, 3, 5, 8, 13, 21, 34].each do |n|
233
- on_time = Time.zone.local(2014, 1, 1, 1, n)
234
- if countable_count == 1
235
- ball.rotations.make n, on_time: on_time
236
- else
237
- countable_count.times do
238
- Ball.create!.rotations.make n, on_time: on_time
233
+ def setup_counters(countable_count = 1)
234
+ [1, 1, 2, 3, 5, 8, 13, 21, 34].each do |n|
235
+ on_time = Time.zone.local(2014, 1, 1, 1, n)
236
+ if countable_count == 1
237
+ ball.rotations.make n, on_time: on_time
238
+ else
239
+ countable_count.times do
240
+ Ball.create!.rotations.make n, on_time: on_time
241
+ end
239
242
  end
240
243
  end
241
244
  end
242
- end
243
245
 
244
- context '#data_for' do
246
+ context '.data_for' do
245
247
 
246
- before { setup_counters }
248
+ subject { Ball }
249
+
250
+ before { setup_counters(3) }
251
+
252
+ it 'with a default interval' do
253
+ data = [
254
+ # [ minute, value ]
255
+ [ 0, 21 ],
256
+ [ 5, 39 ],
257
+ [ 10, 39 ],
258
+ [ 15, 0 ],
259
+ [ 20, 63 ],
260
+ [ 25, 0 ],
261
+ [ 30, 102 ]
262
+ ]
263
+ expect(subject.data_for(:rotations)).to eql_data(data)
264
+ end
265
+
266
+ it 'with a greater interval' do
267
+ data = [ [ 0, 60 ], [ 10, 39 ], [ 20, 63 ], [ 30, 102 ] ]
268
+
269
+ expect(subject.data_for(:rotations, interval: 10.minutes))
270
+ .to eql_data(data)
271
+ end
272
+
273
+ it 'with a greater interval within range' do
274
+ data = [ [ 10, 39 ], [ 20, 63 ] ]
275
+
276
+ range_start = Time.zone.local(2014, 1, 1, 1, 15)
277
+ range_end = Time.zone.local(2014, 1, 1, 1, 29)
278
+ range = range_start..range_end
279
+
280
+ expect(subject.data_for(:rotations, interval: 10.minutes, range: range))
281
+ .to eql_data(data)
282
+ end
283
+
284
+ it 'with a greater interval as symbol and a simple data' do
285
+ bmonth = Time.zone.local(2014, 1, 1).beginning_of_month
286
+ data = [ [ bmonth, 264 ] ]
287
+
288
+ expect(subject.data_for(:rotations, interval: :month))
289
+ .to match_array(data)
290
+ end
291
+
292
+ it 'with a greater interval as symbol and a simple data within range' do
293
+ bmonth = Time.zone.local(2014, 1, 1).beginning_of_month
294
+ data = [ [ bmonth, 264 ] ]
295
+
296
+ range_start = bmonth
297
+ range_end = bmonth.end_of_month
298
+ range = range_start..range_end
299
+
300
+ expect(subject.data_for(:rotations, interval: :month, range: range))
301
+ .to match_array(data)
302
+ end
303
+
304
+
305
+ it 'with a greater interval as symbol on large data set within range' do
306
+ EventCounter.all.each do |counter|
307
+ 11.times do |x|
308
+ created_at = counter.created_at - (x + 1).months
309
+ EventCounter.create!(counter.attributes.except('id')) do |c|
310
+ c.created_at = created_at
311
+ end
312
+ end
313
+ end
314
+
315
+ data = (6..12).map { |x| [ Time.zone.local(2013, x), 264 ] }
316
+ range_start = data[0][0].beginning_of_month
317
+ range_end = data[-1][0].end_of_month
318
+ range = range_start..range_end
319
+
320
+ expect(subject.data_for(:rotations, interval: :month, range: range))
321
+ .to match_array(data)
322
+ end
247
323
 
248
- it 'with default interval' do
249
- data = [
250
- # [ minute, value ]
251
- [ 0, 7 ],
252
- [ 5, 13 ],
253
- [ 10, 13 ],
254
- [ 15, 0 ],
255
- [ 20, 21 ],
256
- [ 25, 0 ],
257
- [ 30, 34 ]
258
- ]
259
- expect(ball.data_for(:rotations)).to eql_data(data)
260
324
  end
261
325
 
262
- it 'with a less interval' do
263
- expect { ball.data_for(:rotations, interval: 3.minutes) }
264
- .to raise_error(EventCounter::CounterError, /could not be less/i)
326
+ context '#data_for' do
327
+
328
+ before { setup_counters }
329
+
330
+ it 'with default interval' do
331
+ data = [
332
+ # [ minute, value ]
333
+ [ 0, 7 ],
334
+ [ 5, 13 ],
335
+ [ 10, 13 ],
336
+ [ 15, 0 ],
337
+ [ 20, 21 ],
338
+ [ 25, 0 ],
339
+ [ 30, 34 ]
340
+ ]
341
+ expect(ball.data_for(:rotations)).to eql_data(data)
342
+ end
265
343
 
266
- [:week, :month, :year].each do |interval|
267
- expect { ball.data_for(:rotations_by_two_year, interval: interval) }
344
+ it 'with a less interval' do
345
+ expect { ball.data_for(:rotations, interval: 3.minutes) }
268
346
  .to raise_error(EventCounter::CounterError, /could not be less/i)
347
+
348
+ [:week, :month, :year].each do |interval|
349
+ expect { ball.data_for(:rotations_by_two_year, interval: interval) }
350
+ .to raise_error(EventCounter::CounterError, /could not be less/i)
351
+ end
269
352
  end
270
- end
271
353
 
272
- it 'with a interval which is not a multiple of original interval' do
273
- expect { ball.data_for(:rotations, interval: 7.minutes) }
274
- .to raise_error(EventCounter::CounterError, /multiple of/i)
275
- end
354
+ it 'with a interval which is not a multiple of original interval' do
355
+ expect { ball.data_for(:rotations, interval: 7.minutes) }
356
+ .to raise_error(EventCounter::CounterError, /multiple of/i)
357
+ end
276
358
 
277
- it 'with a greater interval' do
278
- data = [ [ 0, 33 ], [ 20, 55 ] ]
359
+ it 'with a greater interval' do
360
+ data = [ [ 0, 33 ], [ 20, 55 ] ]
279
361
 
280
- expect(ball.data_for(:rotations, interval: 20.minutes))
281
- .to eql_data(data)
282
- end
362
+ expect(ball.data_for(:rotations, interval: 20.minutes))
363
+ .to eql_data(data)
364
+ end
283
365
 
284
- it 'with a greater interval and a time range' do
285
- range_start = Time.zone.local 2014, 1, 1, 1, 15
286
- range_end = Time.zone.local 2014, 1, 1, 1, 45
287
- range = range_start.in_time_zone..range_end.in_time_zone
366
+ it 'with a greater interval on random (min/max) time period' do
367
+ EventCounter.order("created_at").limit(4).to_a.map(&:destroy)
288
368
 
289
- data = [ [ 10, 13 ], [ 20, 21 ], [ 30, 34 ], [ 40, 0] ]
369
+ data = [ [ 0, 26 ], [ 20, 55 ] ]
290
370
 
291
- expect(ball.data_for(:rotations, interval: 10.minutes, range: range))
292
- .to eql_data(data)
293
- end
371
+ expect(ball.data_for(:rotations, interval: 20.minutes))
372
+ .to eql_data(data)
373
+ end
294
374
 
295
- it 'with a greater interval as symbol' do
296
- beginning_of_week = Time.zone.local(2014).beginning_of_week
375
+ it 'with a greater interval and a time range' do
376
+ range_start = Time.zone.local 2014, 1, 1, 1, 15
377
+ range_end = Time.zone.local 2014, 1, 1, 1, 45
378
+ range = range_start.in_time_zone..range_end.in_time_zone
297
379
 
298
- data = [ [ beginning_of_week, 88 ] ]
380
+ data = [ [ 10, 13 ], [ 20, 21 ], [ 30, 34 ], [ 40, 0] ]
381
+
382
+ expect(ball.data_for(:rotations, interval: 10.minutes, range: range))
383
+ .to eql_data(data)
384
+ end
385
+
386
+ it 'with a greater interval as symbol' do
387
+ beginning_of_week = Time.zone.local(2014).beginning_of_week
388
+
389
+ data = [ [ beginning_of_week, 88 ] ]
390
+
391
+ expect(ball.data_for(:rotations, interval: :week))
392
+ .to eql(data)
393
+ end
299
394
 
300
- expect(ball.data_for(:rotations, interval: :week))
301
- .to eql(data)
302
395
  end
303
396
 
304
397
  end
305
398
 
306
- context '.data_for' do
399
+ it_has 'default behavior'
307
400
 
308
- subject { Ball }
401
+ context "with AR.default_timezone set to :local" do
402
+ before { ActiveRecord::Base.default_timezone = :local }
403
+ after { ActiveRecord::Base.default_timezone = :utc }
309
404
 
310
- before { setup_counters(3) }
405
+ it_has 'default behavior'
406
+ end
311
407
 
312
- it 'with a default interval' do
313
- data = [
314
- # [ minute, value ]
315
- [ 0, 21 ],
316
- [ 5, 39 ],
317
- [ 10, 39 ],
318
- [ 15, 0 ],
319
- [ 20, 63 ],
320
- [ 25, 0 ],
321
- [ 30, 102 ]
322
- ]
323
- expect(subject.data_for(:rotations)).to eql_data(data)
324
- end
408
+ context "with different timezone" do
409
+ before { Time.zone = 'Pacific Time (US & Canada)' }
410
+ after { Time.zone = 'Moscow' }
325
411
 
326
- it 'with a greater interval' do
327
- data = [ [ 0, 60 ], [ 10, 39 ], [ 20, 63 ], [ 30, 102 ] ]
412
+ it_has 'default behavior'
413
+ end
328
414
 
329
- expect(subject.data_for(:rotations, interval: 10.minutes))
330
- .to eql_data(data)
415
+ context "with AR.default_timezone set to :local and different timezone" do
416
+ before do
417
+ ActiveRecord::Base.default_timezone = :local
418
+ Time.zone = 'Pacific Time (US & Canada)'
419
+ end
420
+ after do
421
+ Time.zone = 'Moscow'
422
+ ActiveRecord::Base.default_timezone = :utc
331
423
  end
332
424
 
333
- it 'with a greater interval within range' do
334
- data = [ [ 10, 39 ], [ 20, 63 ] ]
425
+ it_has 'default behavior'
426
+ end
335
427
 
336
- range_start = Time.zone.local(2014, 1, 1, 1, 15)
337
- range_end = Time.zone.local(2014, 1, 1, 1, 29)
338
- range = range_start..range_end
428
+ context 'in timezone with DST' do
339
429
 
340
- expect(subject.data_for(:rotations, interval: 10.minutes, range: range))
341
- .to eql_data(data)
342
- end
430
+ it 'starts PST -> PDT (+1 hour), offset after UTC-7h' do
431
+ Time.zone = 'UTC'
343
432
 
344
- it 'with a greater interval as symbol and a simple data' do
345
- bmonth = Time.zone.local(2014, 1, 1).beginning_of_month
346
- data = [ [ bmonth, 264 ] ]
433
+ # DST for US/Pacific in UTC
434
+ dst_before = Time.zone.local(2014, 3, 9, 9, 59)
435
+ dst_after = Time.zone.local(2014, 3, 9, 10, 1)
347
436
 
348
- expect(subject.data_for(:rotations, interval: :month))
349
- .to match_array(data)
350
- end
437
+ ball.rotations.make(1, on_time: dst_before)
438
+ ball.rotations.make(1, on_time: dst_after)
351
439
 
352
- it 'with a greater interval as symbol and a simple data within range' do
353
- bmonth = Time.zone.local(2014, 1, 1).beginning_of_month
354
- data = [ [ bmonth, 264 ] ]
440
+ Time.zone = 'Pacific Time (US & Canada)'
441
+ data = [
442
+ [ Time.zone.local(2014, 3, 9, 1), 1 ],
443
+ [ Time.zone.local(2014, 3, 9, 3), 1 ],
444
+ ]
355
445
 
356
- range_start = bmonth
357
- range_end = bmonth.end_of_month
358
- range = range_start..range_end
446
+ expect(ball.data_for(:rotations, interval: 1.hour)).to eql(data)
359
447
 
360
- expect(subject.data_for(:rotations, interval: :month, range: range))
361
- .to match_array(data)
448
+ range = dst_before..dst_after
449
+ expect(ball.data_for(:rotations, interval: 1.hour, range: range))
450
+ .to eql(data)
362
451
  end
363
452
 
453
+ it 'ends PDT -> PST (-1 hour), offset after UTC-8h' do
454
+ Time.zone = 'UTC'
364
455
 
365
- it 'with a greater interval as symbol on large data set within range' do
366
- EventCounter.all.each do |counter|
367
- 11.times do |x|
368
- created_at = counter.created_at - (x + 1).months
369
- EventCounter.create!(counter.attributes.except('id')) do |c|
370
- c.created_at = created_at
371
- end
372
- end
373
- end
456
+ # DST for US/Pacific in UTC
457
+ dst_before = Time.zone.local(2014, 11, 2, 8, 59)
458
+ dst_after = Time.zone.local(2014, 11, 2, 9, 1)
374
459
 
375
- data = (6..12).map { |x| [ Time.zone.local(2013, x), 264 ] }
376
- range_start = data[0][0].beginning_of_month
377
- range_end = data[-1][0].end_of_month
378
- range = range_start..range_end
460
+ ball.rotations.make(1, on_time: dst_before)
461
+ ball.rotations.make(1, on_time: dst_after)
379
462
 
380
- expect(subject.data_for(:rotations, interval: :month, range: range))
381
- .to match_array(data)
382
- end
463
+ Time.zone = 'Pacific Time (US & Canada)'
464
+ data = [
465
+ [ Time.zone.local(2014, 11, 2, 1), 1 ],
466
+ [ Time.utc(2014, 11, 2, 9).in_time_zone, 1 ],
467
+ ]
468
+
469
+ expect(ball.data_for(:rotations, interval: 1.hour)).to eql(data)
383
470
 
471
+ range = dst_before..dst_after
472
+ expect(ball.data_for(:rotations, interval: 1.hour, range: range))
473
+ .to eql(data)
474
+ end
384
475
  end
385
476
  end
@@ -28,7 +28,7 @@ describe Ball, slow: true do
28
28
  ball = Ball.create!
29
29
 
30
30
  (Time.zone.local(2012).to_i..Time.zone.local(2015).to_i).step(step) do |i|
31
- on_time = Time.at(i)
31
+ on_time = Time.zone.at(i)
32
32
  ball.rotations.make on_time: on_time
33
33
  end
34
34
 
@@ -36,7 +36,6 @@ describe Ball, slow: true do
36
36
 
37
37
  skip_count = 0
38
38
 
39
-
40
39
  export_sql = "COPY event_counters TO STDOUT (DELIMITER '|')"
41
40
  connection.copy_data(export_sql) do
42
41
  File.open(path, 'w') do |f|
data/spec/spec_helper.rb CHANGED
@@ -19,9 +19,6 @@ YAML.load(File.open(conf).read).values.each do |config|
19
19
  ActiveRecord::Base.establish_connection config
20
20
  end
21
21
 
22
- ActiveRecord::Base.default_timezone = :utc
23
- Time.zone = 'Moscow'
24
-
25
22
  ActiveRecord::Schema.define do
26
23
  self.verbose = false
27
24
 
@@ -35,18 +32,18 @@ ActiveRecord::Schema.define do
35
32
  t.datetime :created_at
36
33
  end
37
34
 
38
- add_index :event_counters, :created_at#, name: 'idx_created_at_desc'
35
+ add_index :event_counters, :created_at
39
36
  add_index :event_counters, [:countable_type, :name, :countable_id],
40
37
  name: 'idx_composite'
41
38
  end
42
39
 
43
40
  # :nodoc:
44
41
  class Ball < ActiveRecord::Base
45
- event_counter_for :rotations, 5.minutes
46
- event_counter_for :rotations_by_week, :week
47
- event_counter_for :rotations_by_month, :month
48
- event_counter_for :rotations_by_year, :year
49
- event_counter_for :rotations_by_two_year, 2.years
42
+ has_counter :rotations, 5.minutes
43
+ has_counter :rotations_by_week, :week
44
+ has_counter :rotations_by_month, :month
45
+ has_counter :rotations_by_year, :year
46
+ has_counter :rotations_by_two_year, 2.years
50
47
  end
51
48
 
52
49
  Dir[File.expand_path('../support/*.rb', __FILE__)].each do |file|
@@ -56,13 +53,24 @@ end
56
53
  RSpec.configure do |config|
57
54
 
58
55
  config.before(:suite) do
59
- DatabaseCleaner.strategy = :transaction
60
- DatabaseCleaner.clean_with(:truncation)
56
+ unless ENV['DEBUG']
57
+ DatabaseCleaner.strategy = :transaction
58
+ DatabaseCleaner.clean_with(:truncation)
59
+ end
60
+ ActiveRecord::Base.default_timezone = :utc
61
+ ActiveRecord::Base.connection.reconnect!
62
+ Time.zone = 'Moscow'
61
63
  end
62
64
 
63
65
  config.around(:each) do |example|
64
- DatabaseCleaner.cleaning { example.run }
66
+ if ENV['DEBUG']
67
+ example.run
68
+ else
69
+ DatabaseCleaner.cleaning { example.run }
70
+ end
65
71
  end
66
72
 
73
+ config.alias_it_should_behave_like_to :it_has, 'has:'
74
+
67
75
  config.filter_run_excluding slow: true unless ENV['RUN_ALL']
68
76
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event-counter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Orel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-22 00:00:00.000000000 Z
11
+ date: 2014-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord