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 +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +1 -1
- data/gemfiles/pg_ar_30.gemfile.lock +1 -1
- data/gemfiles/pg_ar_31.gemfile.lock +1 -1
- data/gemfiles/pg_ar_32.gemfile.lock +1 -1
- data/gemfiles/pg_ar_40.gemfile.lock +1 -1
- data/gemfiles/pg_ar_41.gemfile.lock +1 -1
- data/lib/event_counter.rb +5 -5
- data/lib/event_counter/active_record_extension.rb +88 -50
- data/lib/event_counter/version.rb +1 -1
- data/spec/lib/event_counter_spec.rb +315 -224
- data/spec/lib/performance_spec.rb +1 -2
- data/spec/spec_helper.rb +20 -12
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5e4803060e3164a0eaf04624b6d5002c0789dd4
|
4
|
+
data.tar.gz: 58e249fde274f8d405e6d9d872fbdef6ca799a87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9002dad388634f8afd70f9c8f57591e32eb1e8ed0ac859daee6b15609e2699c3cc90ee1103e5c15f2a3aa2430a02df6a00bea150f11f21b34e52022e71266120
|
7
|
+
data.tar.gz: e321c6a69d69f52b1b1b7b53c65fa14865723ed46e037a8ad3e3b4b4ebbf0017577c0d627f1c69d82d403753886dd0a254454ca31c94740196a7789265f6d81d
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
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 =
|
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
|
-
|
38
|
+
scoped_relation.create!(attrs)
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
42
|
def self.current_interval
|
43
|
-
|
43
|
+
scoped_relation.proxy_association.owner.event_counters[counter_name]
|
44
44
|
end
|
45
45
|
|
46
46
|
def self.counter_name
|
47
|
-
|
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.
|
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
|
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
|
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
|
77
|
-
|
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,
|
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
|
-
|
88
|
+
q = <<-SQL.squish!
|
89
89
|
SELECT created_at, value
|
90
|
-
FROM (#{series(interval,
|
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(
|
95
|
+
result = connection.execute(q).to_a
|
96
96
|
|
97
|
-
raw ? result : normalize_counters_data(result
|
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
|
-
|
107
|
+
dtrunc(interval, 'created_at', tz)
|
108
108
|
else
|
109
|
-
time =
|
110
|
-
|
111
|
-
#{
|
112
|
-
|
113
|
-
|
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
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
131
|
-
|
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
|
-
|
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
|
-
|
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
|
148
|
-
|
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
|
-
|
152
|
-
|
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
|
-
|
158
|
-
|
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,
|
165
|
-
"date_trunc(#{sanitize(interval)}, #{
|
202
|
+
def dtrunc(interval, str, tz)
|
203
|
+
"date_trunc(#{sanitize(interval)}, #{at_tz("#{str}::timestamptz", tz)})"
|
166
204
|
end
|
167
205
|
|
168
|
-
def
|
169
|
-
"#{str}
|
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
|
213
|
-
|
214
|
-
|
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
|
|
@@ -75,311 +75,402 @@ end
|
|
75
75
|
describe Ball do
|
76
76
|
let(:ball) { Ball.create! }
|
77
77
|
|
78
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
115
|
-
|
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
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
125
|
-
|
126
|
+
it 'decrements existent counter with default value' do
|
127
|
+
counter = ball.rotations.make(- 1)
|
126
128
|
|
127
|
-
expect {
|
128
129
|
expect {
|
129
|
-
expect
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
136
|
+
it 'increments existent counter by a specified value' do
|
137
|
+
counter = ball.rotations.make
|
136
138
|
|
137
|
-
expect {
|
138
139
|
expect {
|
139
|
-
expect
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
145
|
-
|
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
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
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
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
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
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
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
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
199
|
-
|
200
|
+
it 'forces existent counter with new value' do
|
201
|
+
counter = ball.rotations.make
|
200
202
|
|
201
|
-
expect {
|
202
203
|
expect {
|
203
|
-
expect
|
204
|
-
.
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
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
|
216
|
-
.
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
246
|
+
context '.data_for' do
|
245
247
|
|
246
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
267
|
-
expect { ball.data_for(:
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
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
|
-
|
278
|
-
|
359
|
+
it 'with a greater interval' do
|
360
|
+
data = [ [ 0, 33 ], [ 20, 55 ] ]
|
279
361
|
|
280
|
-
|
281
|
-
|
282
|
-
|
362
|
+
expect(ball.data_for(:rotations, interval: 20.minutes))
|
363
|
+
.to eql_data(data)
|
364
|
+
end
|
283
365
|
|
284
|
-
|
285
|
-
|
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
|
-
|
369
|
+
data = [ [ 0, 26 ], [ 20, 55 ] ]
|
290
370
|
|
291
|
-
|
292
|
-
|
293
|
-
|
371
|
+
expect(ball.data_for(:rotations, interval: 20.minutes))
|
372
|
+
.to eql_data(data)
|
373
|
+
end
|
294
374
|
|
295
|
-
|
296
|
-
|
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
|
-
|
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
|
-
|
399
|
+
it_has 'default behavior'
|
307
400
|
|
308
|
-
|
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
|
-
|
405
|
+
it_has 'default behavior'
|
406
|
+
end
|
311
407
|
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
327
|
-
|
412
|
+
it_has 'default behavior'
|
413
|
+
end
|
328
414
|
|
329
|
-
|
330
|
-
|
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
|
-
|
334
|
-
|
425
|
+
it_has 'default behavior'
|
426
|
+
end
|
335
427
|
|
336
|
-
|
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
|
-
|
341
|
-
|
342
|
-
end
|
430
|
+
it 'starts PST -> PDT (+1 hour), offset after UTC-7h' do
|
431
|
+
Time.zone = 'UTC'
|
343
432
|
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
349
|
-
|
350
|
-
end
|
437
|
+
ball.rotations.make(1, on_time: dst_before)
|
438
|
+
ball.rotations.make(1, on_time: dst_after)
|
351
439
|
|
352
|
-
|
353
|
-
|
354
|
-
|
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
|
-
|
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
|
-
|
361
|
-
|
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
|
-
|
366
|
-
|
367
|
-
|
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
|
-
|
376
|
-
|
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
|
-
|
381
|
-
|
382
|
-
|
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
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
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.
|
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-
|
11
|
+
date: 2014-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|