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