event-counter 0.1.1 → 0.3.0

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