groupdate 4.0.0 → 4.0.1
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 +5 -5
- data/{ISSUE_TEMPLATE.md → .github/ISSUE_TEMPLATE.md} +0 -0
- data/.travis.yml +6 -8
- data/CHANGELOG.md +4 -0
- data/CONTRIBUTING.md +4 -1
- data/Gemfile +7 -1
- data/README.md +2 -2
- data/groupdate.gemspec +3 -1
- data/lib/groupdate.rb +2 -0
- data/lib/groupdate/magic.rb +50 -357
- data/lib/groupdate/relation_builder.rb +177 -0
- data/lib/groupdate/series_builder.rb +220 -0
- data/lib/groupdate/version.rb +1 -1
- data/test/gemfiles/activerecord42.gemfile +6 -0
- data/test/gemfiles/activerecord50.gemfile +6 -0
- data/test/gemfiles/activerecord51.gemfile +12 -0
- data/test/test_helper.rb +1 -1
- metadata +13 -11
- data/test/gemfiles/activerecord52.gemfile +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: edfe1e903c4ce720f5dfe74be0088937aa52b69f82dd88732802f49764205314
|
4
|
+
data.tar.gz: 85a0642b059de80a94ee621c028f117296e1baa09d93232ed6e97680b9d5fb25
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f2c7f1f9d5cd73b00b89005a17d62897494deb51164ccb75cbf5b6917ce389cd2847c410f271c75118c74b17677d6fabb1ef0c327063c5b7de527ad46604e66d
|
7
|
+
data.tar.gz: c0a4f216150c83fa1cca7e8f10775b619da2b654b4a85e6f148bead6fe8fe82b1fd45b1741f89b7873e7c9c1ca6494f528b6cfbf41b4dda723b37c88b9d22cc2
|
File without changes
|
data/.travis.yml
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
3
|
- 2.4.2
|
4
|
-
- jruby-9.1.
|
4
|
+
- jruby-9.1.9.0
|
5
5
|
gemfile:
|
6
6
|
- Gemfile
|
7
|
-
- test/gemfiles/
|
7
|
+
- test/gemfiles/activerecord51.gemfile
|
8
8
|
- test/gemfiles/activerecord50.gemfile
|
9
9
|
- test/gemfiles/activerecord42.gemfile
|
10
10
|
sudo: false
|
@@ -20,9 +20,7 @@ notifications:
|
|
20
20
|
on_failure: change
|
21
21
|
matrix:
|
22
22
|
allow_failures:
|
23
|
-
- rvm:
|
24
|
-
gemfile:
|
25
|
-
- rvm: jruby-9.1.
|
26
|
-
gemfile: test/gemfiles/
|
27
|
-
- rvm: jruby-9.1.5.0
|
28
|
-
gemfile: test/gemfiles/activerecord42.gemfile
|
23
|
+
- rvm: jruby-9.1.9.0
|
24
|
+
gemfile: Gemfile
|
25
|
+
- rvm: jruby-9.1.9.0
|
26
|
+
gemfile: test/gemfiles/activerecord51.gemfile
|
data/CHANGELOG.md
CHANGED
data/CONTRIBUTING.md
CHANGED
@@ -21,7 +21,10 @@ Think you’ve discovered an issue?
|
|
21
21
|
gem "groupdate", github: "ankane/groupdate"
|
22
22
|
```
|
23
23
|
|
24
|
-
If the above steps don’t help, create an issue. Include
|
24
|
+
If the above steps don’t help, create an issue. Include:
|
25
|
+
|
26
|
+
- Detailed steps to reproduce
|
27
|
+
- Complete backtraces for exceptions
|
25
28
|
|
26
29
|
## Pull Requests
|
27
30
|
|
data/Gemfile
CHANGED
@@ -3,4 +3,10 @@ source "https://rubygems.org"
|
|
3
3
|
# Specify your gem's dependencies in groupdate.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
gem "activerecord", "
|
6
|
+
gem "activerecord", "~> 5.2.0"
|
7
|
+
|
8
|
+
if defined?(JRUBY_VERSION)
|
9
|
+
gem "activerecord-jdbcpostgresql-adapter", git: "https://github.com/jruby/activerecord-jdbc-adapter.git"
|
10
|
+
gem "activerecord-jdbcmysql-adapter", git: "https://github.com/jruby/activerecord-jdbc-adapter.git"
|
11
|
+
gem "activerecord-jdbcsqlite3-adapter", git: "https://github.com/jruby/activerecord-jdbc-adapter.git"
|
12
|
+
end
|
data/README.md
CHANGED
@@ -218,7 +218,7 @@ gem 'groupdate'
|
|
218
218
|
|
219
219
|
#### For MySQL
|
220
220
|
|
221
|
-
[Time zone support](
|
221
|
+
[Time zone support](https://dev.mysql.com/doc/refman/5.7/en/time-zone-support.html) must be installed on the server.
|
222
222
|
|
223
223
|
```sh
|
224
224
|
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
|
@@ -278,7 +278,7 @@ Groupdate 2.0 brings a number of improvements. Here are two things to be aware
|
|
278
278
|
|
279
279
|
View the [changelog](https://github.com/ankane/groupdate/blob/master/CHANGELOG.md)
|
280
280
|
|
281
|
-
Groupdate follows [Semantic Versioning](
|
281
|
+
Groupdate follows [Semantic Versioning](https://semver.org/)
|
282
282
|
|
283
283
|
## Contributing
|
284
284
|
|
data/groupdate.gemspec
CHANGED
@@ -17,6 +17,8 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
+
spec.required_ruby_version = ">= 2.2.0"
|
21
|
+
|
20
22
|
spec.add_dependency "activesupport", ">= 4.2"
|
21
23
|
|
22
24
|
spec.add_development_dependency "bundler"
|
@@ -30,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
30
32
|
spec.add_development_dependency "activerecord-jdbcsqlite3-adapter"
|
31
33
|
else
|
32
34
|
spec.add_development_dependency "pg", "< 1"
|
33
|
-
spec.add_development_dependency "mysql2"
|
35
|
+
spec.add_development_dependency "mysql2", "< 0.5"
|
34
36
|
spec.add_development_dependency "sqlite3"
|
35
37
|
end
|
36
38
|
end
|
data/lib/groupdate.rb
CHANGED
data/lib/groupdate/magic.rb
CHANGED
@@ -2,7 +2,7 @@ require "i18n"
|
|
2
2
|
|
3
3
|
module Groupdate
|
4
4
|
class Magic
|
5
|
-
attr_accessor :period, :options
|
5
|
+
attr_accessor :period, :options, :group_index
|
6
6
|
|
7
7
|
def initialize(period:, **options)
|
8
8
|
@period = period
|
@@ -15,8 +15,6 @@ module Groupdate
|
|
15
15
|
raise Groupdate::Error, "Unrecognized :week_start option" if period == :week && !week_start
|
16
16
|
end
|
17
17
|
|
18
|
-
protected
|
19
|
-
|
20
18
|
def time_zone
|
21
19
|
@time_zone ||= begin
|
22
20
|
time_zone = "Etc/UTC" if options[:time_zone] == false
|
@@ -33,199 +31,25 @@ module Groupdate
|
|
33
31
|
@day_start ||= ((options[:day_start] || Groupdate.day_start).to_f * 3600).round
|
34
32
|
end
|
35
33
|
|
36
|
-
def
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
elsif !time_range && options[:last]
|
46
|
-
if period == :quarter
|
47
|
-
step = 3.months
|
48
|
-
elsif 1.respond_to?(period)
|
49
|
-
step = 1.send(period)
|
50
|
-
else
|
51
|
-
raise ArgumentError, "Cannot use last option with #{period}"
|
52
|
-
end
|
53
|
-
if step
|
54
|
-
now = Time.now
|
55
|
-
# loop instead of multiply to change start_at - see #151
|
56
|
-
start_at = now
|
57
|
-
(options[:last].to_i - 1).times do
|
58
|
-
start_at -= step
|
59
|
-
end
|
60
|
-
|
61
|
-
time_range =
|
62
|
-
if options[:current] == false
|
63
|
-
round_time(start_at - step)...round_time(now)
|
64
|
-
else
|
65
|
-
round_time(start_at)..now
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
time_range
|
70
|
-
end
|
34
|
+
def series_builder
|
35
|
+
@series_builder ||=
|
36
|
+
SeriesBuilder.new(
|
37
|
+
**options,
|
38
|
+
period: period,
|
39
|
+
time_zone: time_zone,
|
40
|
+
day_start: day_start,
|
41
|
+
week_start: week_start
|
42
|
+
)
|
71
43
|
end
|
72
44
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
series =
|
77
|
-
case period
|
78
|
-
when :day_of_week
|
79
|
-
0..6
|
80
|
-
when :hour_of_day
|
81
|
-
0..23
|
82
|
-
when :minute_of_hour
|
83
|
-
0..59
|
84
|
-
when :day_of_month
|
85
|
-
1..31
|
86
|
-
when :month_of_year
|
87
|
-
1..12
|
88
|
-
else
|
89
|
-
time_range = self.time_range
|
90
|
-
time_range =
|
91
|
-
if time_range.is_a?(Range)
|
92
|
-
time_range
|
93
|
-
else
|
94
|
-
# use first and last values
|
95
|
-
sorted_keys =
|
96
|
-
if multiple_groups
|
97
|
-
count.keys.map { |k| k[@group_index] }.sort
|
98
|
-
else
|
99
|
-
count.keys.sort
|
100
|
-
end
|
101
|
-
sorted_keys.first..sorted_keys.last
|
102
|
-
end
|
103
|
-
|
104
|
-
if time_range.first
|
105
|
-
series = [round_time(time_range.first)]
|
106
|
-
|
107
|
-
if period == :quarter
|
108
|
-
step = 3.months
|
109
|
-
else
|
110
|
-
step = 1.send(period)
|
111
|
-
end
|
112
|
-
|
113
|
-
last_step = series.last
|
114
|
-
while (next_step = round_time(last_step + step)) && time_range.cover?(next_step)
|
115
|
-
if next_step == last_step
|
116
|
-
last_step += step
|
117
|
-
next
|
118
|
-
end
|
119
|
-
series << next_step
|
120
|
-
last_step = next_step
|
121
|
-
end
|
122
|
-
|
123
|
-
series
|
124
|
-
else
|
125
|
-
[]
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
series =
|
130
|
-
if multiple_groups
|
131
|
-
keys = count.keys.map { |k| k[0...@group_index] + k[(@group_index + 1)..-1] }.uniq
|
132
|
-
series = series.to_a.reverse if reverse
|
133
|
-
keys.flat_map do |k|
|
134
|
-
series.map { |s| k[0...@group_index] + [s] + k[@group_index..-1] }
|
135
|
-
end
|
136
|
-
else
|
137
|
-
series
|
138
|
-
end
|
139
|
-
|
140
|
-
# reversed above if multiple groups
|
141
|
-
series = series.to_a.reverse if !multiple_groups && reverse
|
142
|
-
|
143
|
-
locale = options[:locale] || I18n.locale
|
144
|
-
use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
|
145
|
-
key_format =
|
146
|
-
if options[:format]
|
147
|
-
if options[:format].respond_to?(:call)
|
148
|
-
options[:format]
|
149
|
-
else
|
150
|
-
sunday = time_zone.parse("2014-03-02 00:00:00")
|
151
|
-
lambda do |key|
|
152
|
-
case period
|
153
|
-
when :hour_of_day
|
154
|
-
key = sunday + key.hours + day_start.seconds
|
155
|
-
when :minute_of_hour
|
156
|
-
key = sunday + key.minutes + day_start.seconds
|
157
|
-
when :day_of_week
|
158
|
-
key = sunday + key.days + (week_start + 1).days
|
159
|
-
when :day_of_month
|
160
|
-
key = Date.new(2014, 1, key).to_time
|
161
|
-
when :month_of_year
|
162
|
-
key = Date.new(2014, key, 1).to_time
|
163
|
-
end
|
164
|
-
I18n.localize(key, format: options[:format], locale: locale)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
elsif [:day, :week, :month, :quarter, :year].include?(period) && use_dates
|
168
|
-
lambda { |k| k.to_date }
|
169
|
-
else
|
170
|
-
lambda { |k| k }
|
171
|
-
end
|
172
|
-
|
173
|
-
use_series = options.key?(:series) ? options[:series] : series_default
|
174
|
-
if use_series == false
|
175
|
-
series = series.select { |k| count[k] }
|
176
|
-
end
|
177
|
-
|
178
|
-
value = 0
|
179
|
-
Hash[series.map do |k|
|
180
|
-
value = count[k] || (@options[:carry_forward] && value) || default_value
|
181
|
-
[multiple_groups ? k[0...@group_index] + [key_format.call(k[@group_index])] + k[(@group_index + 1)..-1] : key_format.call(k), value]
|
182
|
-
end]
|
183
|
-
end
|
184
|
-
|
185
|
-
def round_time(time)
|
186
|
-
time = time.to_time.in_time_zone(time_zone) - day_start.seconds
|
187
|
-
|
188
|
-
time =
|
189
|
-
case period
|
190
|
-
when :second
|
191
|
-
time.change(usec: 0)
|
192
|
-
when :minute
|
193
|
-
time.change(sec: 0)
|
194
|
-
when :hour
|
195
|
-
time.change(min: 0)
|
196
|
-
when :day
|
197
|
-
time.beginning_of_day
|
198
|
-
when :week
|
199
|
-
# same logic as MySQL group
|
200
|
-
weekday = (time.wday - 1) % 7
|
201
|
-
(time - ((7 - week_start + weekday) % 7).days).midnight
|
202
|
-
when :month
|
203
|
-
time.beginning_of_month
|
204
|
-
when :quarter
|
205
|
-
time.beginning_of_quarter
|
206
|
-
when :year
|
207
|
-
time.beginning_of_year
|
208
|
-
when :hour_of_day
|
209
|
-
time.hour
|
210
|
-
when :minute_of_hour
|
211
|
-
time.min
|
212
|
-
when :day_of_week
|
213
|
-
(time.wday - 1 - week_start) % 7
|
214
|
-
when :day_of_month
|
215
|
-
time.day
|
216
|
-
when :month_of_year
|
217
|
-
time.month
|
218
|
-
else
|
219
|
-
raise Groupdate::Error, "Invalid period"
|
220
|
-
end
|
221
|
-
|
222
|
-
time.is_a?(Time) ? time + day_start.seconds : time
|
45
|
+
def time_range
|
46
|
+
series_builder.time_range
|
223
47
|
end
|
224
48
|
|
225
49
|
class Enumerable < Magic
|
226
50
|
def group_by(enum, &_block)
|
227
|
-
group = enum.group_by { |v| v = yield(v); v ? round_time(v) : nil }
|
228
|
-
|
51
|
+
group = enum.group_by { |v| v = yield(v); v ? series_builder.round_time(v) : nil }
|
52
|
+
series_builder.generate(group, default_value: [], series_default: false)
|
229
53
|
end
|
230
54
|
|
231
55
|
def self.group_by(enum, period, options, &block)
|
@@ -239,191 +63,60 @@ module Groupdate
|
|
239
63
|
@options = options
|
240
64
|
end
|
241
65
|
|
242
|
-
def relation
|
243
|
-
|
244
|
-
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
245
|
-
end
|
246
|
-
|
247
|
-
time_zone = self.time_zone.tzinfo.name
|
248
|
-
|
249
|
-
adapter_name = relation.connection.adapter_name
|
250
|
-
query =
|
251
|
-
case adapter_name
|
252
|
-
when "MySQL", "Mysql2", "Mysql2Spatial", 'Mysql2Rgeo'
|
253
|
-
case period
|
254
|
-
when :day_of_week
|
255
|
-
["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1", time_zone]
|
256
|
-
when :hour_of_day
|
257
|
-
["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
|
258
|
-
when :minute_of_hour
|
259
|
-
["(EXTRACT(MINUTE from CONVERT_TZ(#{column}, '+00:00', ?)))", time_zone]
|
260
|
-
when :day_of_month
|
261
|
-
["DAYOFMONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
262
|
-
when :month_of_year
|
263
|
-
["MONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
264
|
-
when :week
|
265
|
-
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} second)) % 7) DAY) - INTERVAL #{day_start} second, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} second, ?, '+00:00')", time_zone, time_zone, time_zone]
|
266
|
-
when :quarter
|
267
|
-
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(EXTRACT(YEAR FROM CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)), '-', LPAD(1 + 3 * (QUARTER(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone, time_zone]
|
268
|
-
else
|
269
|
-
format =
|
270
|
-
case period
|
271
|
-
when :second
|
272
|
-
"%Y-%m-%d %H:%i:%S"
|
273
|
-
when :minute
|
274
|
-
"%Y-%m-%d %H:%i:00"
|
275
|
-
when :hour
|
276
|
-
"%Y-%m-%d %H:00:00"
|
277
|
-
when :day
|
278
|
-
"%Y-%m-%d 00:00:00"
|
279
|
-
when :month
|
280
|
-
"%Y-%m-01 00:00:00"
|
281
|
-
else # year
|
282
|
-
"%Y-01-01 00:00:00"
|
283
|
-
end
|
284
|
-
|
285
|
-
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone]
|
286
|
-
end
|
287
|
-
when "PostgreSQL", "PostGIS"
|
288
|
-
case period
|
289
|
-
when :day_of_week
|
290
|
-
["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
291
|
-
when :hour_of_day
|
292
|
-
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
293
|
-
when :minute_of_hour
|
294
|
-
["EXTRACT(MINUTE from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
295
|
-
when :day_of_month
|
296
|
-
["EXTRACT(DAY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
297
|
-
when :month_of_year
|
298
|
-
["EXTRACT(MONTH from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
299
|
-
when :week # start on Sunday, not PostgreSQL default Monday
|
300
|
-
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
301
|
-
else
|
302
|
-
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
303
|
-
end
|
304
|
-
when "SQLite"
|
305
|
-
raise Groupdate::Error, "Time zones not supported for SQLite" unless self.time_zone.utc_offset.zero?
|
306
|
-
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
307
|
-
raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
|
308
|
-
|
309
|
-
if period == :week
|
310
|
-
["strftime('%%Y-%%m-%%d 00:00:00 UTC', #{column}, '-6 days', 'weekday 0')"]
|
311
|
-
else
|
312
|
-
format =
|
313
|
-
case period
|
314
|
-
when :hour_of_day
|
315
|
-
"%H"
|
316
|
-
when :minute_of_hour
|
317
|
-
"%M"
|
318
|
-
when :day_of_week
|
319
|
-
"%w"
|
320
|
-
when :day_of_month
|
321
|
-
"%d"
|
322
|
-
when :month_of_year
|
323
|
-
"%m"
|
324
|
-
when :second
|
325
|
-
"%Y-%m-%d %H:%M:%S UTC"
|
326
|
-
when :minute
|
327
|
-
"%Y-%m-%d %H:%M:00 UTC"
|
328
|
-
when :hour
|
329
|
-
"%Y-%m-%d %H:00:00 UTC"
|
330
|
-
when :day
|
331
|
-
"%Y-%m-%d 00:00:00 UTC"
|
332
|
-
when :month
|
333
|
-
"%Y-%m-01 00:00:00 UTC"
|
334
|
-
when :quarter
|
335
|
-
raise Groupdate::Error, "Quarter not supported for SQLite"
|
336
|
-
else # year
|
337
|
-
"%Y-01-01 00:00:00 UTC"
|
338
|
-
end
|
339
|
-
|
340
|
-
["strftime('#{format.gsub(/%/, '%%')}', #{column})"]
|
341
|
-
end
|
342
|
-
when "Redshift"
|
343
|
-
case period
|
344
|
-
when :day_of_week
|
345
|
-
["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
346
|
-
when :hour_of_day
|
347
|
-
["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
348
|
-
when :minute_of_hour
|
349
|
-
["EXTRACT(MINUTE from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
350
|
-
when :day_of_month
|
351
|
-
["EXTRACT(DAY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
352
|
-
when :month_of_year
|
353
|
-
["EXTRACT(MONTH from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
354
|
-
when :week # start on Sunday, not Redshift default Monday
|
355
|
-
# Redshift does not return timezone information; it
|
356
|
-
# always says it is in UTC time, so we must convert
|
357
|
-
# back to UTC to play properly with the rest of Groupdate.
|
358
|
-
#
|
359
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
360
|
-
else
|
361
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
362
|
-
end
|
363
|
-
else
|
364
|
-
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
365
|
-
end
|
66
|
+
def perform(relation, result)
|
67
|
+
multiple_groups = relation.group_values.size > 1
|
366
68
|
|
367
|
-
|
368
|
-
|
369
|
-
end
|
69
|
+
check_time_zone_support(result, multiple_groups)
|
70
|
+
result = cast_result(result, multiple_groups)
|
370
71
|
|
371
|
-
|
72
|
+
series_builder.generate(
|
73
|
+
result,
|
74
|
+
default_value: options.key?(:default_value) ? options[:default_value] : 0,
|
75
|
+
multiple_groups: multiple_groups,
|
76
|
+
group_index: group_index
|
77
|
+
)
|
78
|
+
end
|
372
79
|
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
80
|
+
def cast_method
|
81
|
+
case period
|
82
|
+
when :day_of_week
|
83
|
+
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
84
|
+
when :hour_of_day, :day_of_month, :month_of_year, :minute_of_hour
|
85
|
+
lambda { |k| k.to_i }
|
86
|
+
else
|
87
|
+
utc = ActiveSupport::TimeZone["UTC"]
|
88
|
+
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
380
89
|
end
|
381
|
-
|
382
|
-
group = relation.group(group_str)
|
383
|
-
relation =
|
384
|
-
if time_range.is_a?(Range)
|
385
|
-
# doesn't matter whether we include the end of a ... range - it will be excluded later
|
386
|
-
group.where("#{column} >= ? AND #{column} <= ?", time_range.first, time_range.last)
|
387
|
-
else
|
388
|
-
group.where("#{column} IS NOT NULL")
|
389
|
-
end
|
390
|
-
|
391
|
-
# TODO do not change object state
|
392
|
-
@group_index = group.group_values.size - 1
|
393
|
-
|
394
|
-
relation
|
395
90
|
end
|
396
91
|
|
397
|
-
def
|
398
|
-
multiple_groups
|
399
|
-
|
400
|
-
cast_method =
|
401
|
-
case period
|
402
|
-
when :day_of_week
|
403
|
-
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
404
|
-
when :hour_of_day, :day_of_month, :month_of_year, :minute_of_hour
|
405
|
-
lambda { |k| k.to_i }
|
406
|
-
else
|
407
|
-
utc = ActiveSupport::TimeZone["UTC"]
|
408
|
-
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
409
|
-
end
|
92
|
+
def cast_result(result, multiple_groups)
|
93
|
+
Hash[result.map { |k, v| [multiple_groups ? k[0...group_index] + [cast_method.call(k[group_index])] + k[(group_index + 1)..-1] : cast_method.call(k), v] }]
|
94
|
+
end
|
410
95
|
|
411
|
-
|
96
|
+
def check_time_zone_support(result, multiple_groups)
|
97
|
+
missing_time_zone_support = multiple_groups ? (result.keys.first && result.keys.first[group_index].nil?) : result.key?(nil)
|
412
98
|
if missing_time_zone_support
|
413
99
|
raise Groupdate::Error, "Be sure to install time zone support - https://github.com/ankane/groupdate#for-mysql"
|
414
100
|
end
|
415
|
-
result = Hash[result.map { |k, v| [multiple_groups ? k[0...@group_index] + [cast_method.call(k[@group_index])] + k[(@group_index + 1)..-1] : cast_method.call(k), v] }]
|
416
|
-
|
417
|
-
series(result, (options.key?(:default_value) ? options[:default_value] : 0), multiple_groups, @reverse)
|
418
101
|
end
|
419
102
|
|
420
103
|
def self.generate_relation(relation, field:, **options)
|
421
104
|
magic = Groupdate::Magic::Relation.new(**options)
|
422
105
|
|
423
106
|
# generate ActiveRecord relation
|
424
|
-
relation =
|
107
|
+
relation =
|
108
|
+
RelationBuilder.new(
|
109
|
+
relation,
|
110
|
+
column: field,
|
111
|
+
period: magic.period,
|
112
|
+
time_zone: magic.time_zone,
|
113
|
+
time_range: magic.time_range,
|
114
|
+
week_start: magic.week_start,
|
115
|
+
day_start: magic.day_start
|
116
|
+
).generate
|
425
117
|
|
426
118
|
# add Groupdate info
|
119
|
+
magic.group_index = relation.group_values.size - 1
|
427
120
|
(relation.groupdate_values ||= []) << magic
|
428
121
|
|
429
122
|
relation
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Groupdate
|
2
|
+
class RelationBuilder
|
3
|
+
attr_reader :period, :column, :day_start, :week_start
|
4
|
+
|
5
|
+
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:)
|
6
|
+
@relation = relation
|
7
|
+
@column = column
|
8
|
+
@period = period
|
9
|
+
@time_zone = time_zone
|
10
|
+
@time_range = time_range
|
11
|
+
@week_start = week_start
|
12
|
+
@day_start = day_start
|
13
|
+
|
14
|
+
if relation.default_timezone == :local
|
15
|
+
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate
|
20
|
+
@relation.group(group_clause).where(*where_clause)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def group_clause
|
26
|
+
time_zone = @time_zone.tzinfo.name
|
27
|
+
adapter_name = @relation.connection.adapter_name
|
28
|
+
query =
|
29
|
+
case adapter_name
|
30
|
+
when "MySQL", "Mysql2", "Mysql2Spatial", 'Mysql2Rgeo'
|
31
|
+
case period
|
32
|
+
when :day_of_week
|
33
|
+
["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1", time_zone]
|
34
|
+
when :hour_of_day
|
35
|
+
["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
|
36
|
+
when :minute_of_hour
|
37
|
+
["(EXTRACT(MINUTE from CONVERT_TZ(#{column}, '+00:00', ?)))", time_zone]
|
38
|
+
when :day_of_month
|
39
|
+
["DAYOFMONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
40
|
+
when :month_of_year
|
41
|
+
["MONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
42
|
+
when :week
|
43
|
+
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} second)) % 7) DAY) - INTERVAL #{day_start} second, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} second, ?, '+00:00')", time_zone, time_zone, time_zone]
|
44
|
+
when :quarter
|
45
|
+
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(EXTRACT(YEAR FROM CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)), '-', LPAD(1 + 3 * (QUARTER(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone, time_zone]
|
46
|
+
else
|
47
|
+
format =
|
48
|
+
case period
|
49
|
+
when :second
|
50
|
+
"%Y-%m-%d %H:%i:%S"
|
51
|
+
when :minute
|
52
|
+
"%Y-%m-%d %H:%i:00"
|
53
|
+
when :hour
|
54
|
+
"%Y-%m-%d %H:00:00"
|
55
|
+
when :day
|
56
|
+
"%Y-%m-%d 00:00:00"
|
57
|
+
when :month
|
58
|
+
"%Y-%m-01 00:00:00"
|
59
|
+
else # year
|
60
|
+
"%Y-01-01 00:00:00"
|
61
|
+
end
|
62
|
+
|
63
|
+
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone]
|
64
|
+
end
|
65
|
+
when "PostgreSQL", "PostGIS"
|
66
|
+
case period
|
67
|
+
when :day_of_week
|
68
|
+
["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
69
|
+
when :hour_of_day
|
70
|
+
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
71
|
+
when :minute_of_hour
|
72
|
+
["EXTRACT(MINUTE from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
73
|
+
when :day_of_month
|
74
|
+
["EXTRACT(DAY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
75
|
+
when :month_of_year
|
76
|
+
["EXTRACT(MONTH from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
77
|
+
when :week # start on Sunday, not PostgreSQL default Monday
|
78
|
+
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
79
|
+
else
|
80
|
+
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
81
|
+
end
|
82
|
+
when "SQLite"
|
83
|
+
raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
|
84
|
+
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
85
|
+
raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
|
86
|
+
|
87
|
+
if period == :week
|
88
|
+
["strftime('%%Y-%%m-%%d 00:00:00 UTC', #{column}, '-6 days', 'weekday 0')"]
|
89
|
+
else
|
90
|
+
format =
|
91
|
+
case period
|
92
|
+
when :hour_of_day
|
93
|
+
"%H"
|
94
|
+
when :minute_of_hour
|
95
|
+
"%M"
|
96
|
+
when :day_of_week
|
97
|
+
"%w"
|
98
|
+
when :day_of_month
|
99
|
+
"%d"
|
100
|
+
when :month_of_year
|
101
|
+
"%m"
|
102
|
+
when :second
|
103
|
+
"%Y-%m-%d %H:%M:%S UTC"
|
104
|
+
when :minute
|
105
|
+
"%Y-%m-%d %H:%M:00 UTC"
|
106
|
+
when :hour
|
107
|
+
"%Y-%m-%d %H:00:00 UTC"
|
108
|
+
when :day
|
109
|
+
"%Y-%m-%d 00:00:00 UTC"
|
110
|
+
when :month
|
111
|
+
"%Y-%m-01 00:00:00 UTC"
|
112
|
+
when :quarter
|
113
|
+
raise Groupdate::Error, "Quarter not supported for SQLite"
|
114
|
+
else # year
|
115
|
+
"%Y-01-01 00:00:00 UTC"
|
116
|
+
end
|
117
|
+
|
118
|
+
["strftime('#{format.gsub(/%/, '%%')}', #{column})"]
|
119
|
+
end
|
120
|
+
when "Redshift"
|
121
|
+
case period
|
122
|
+
when :day_of_week
|
123
|
+
["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
124
|
+
when :hour_of_day
|
125
|
+
["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
126
|
+
when :minute_of_hour
|
127
|
+
["EXTRACT(MINUTE from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
128
|
+
when :day_of_month
|
129
|
+
["EXTRACT(DAY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
130
|
+
when :month_of_year
|
131
|
+
["EXTRACT(MONTH from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
132
|
+
when :week # start on Sunday, not Redshift default Monday
|
133
|
+
# Redshift does not return timezone information; it
|
134
|
+
# always says it is in UTC time, so we must convert
|
135
|
+
# back to UTC to play properly with the rest of Groupdate.
|
136
|
+
#
|
137
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
138
|
+
else
|
139
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
140
|
+
end
|
141
|
+
else
|
142
|
+
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
143
|
+
end
|
144
|
+
|
145
|
+
if adapter_name == "MySQL" && period == :week
|
146
|
+
query[0] = "CAST(#{query[0]} AS DATETIME)"
|
147
|
+
end
|
148
|
+
|
149
|
+
clause = @relation.send(:sanitize_sql_array, query)
|
150
|
+
|
151
|
+
# cleaner queries in logs
|
152
|
+
clause = clean_group_clause_postgresql(clause)
|
153
|
+
clean_group_clause_mysql(clause)
|
154
|
+
end
|
155
|
+
|
156
|
+
def clean_group_clause_postgresql(clause)
|
157
|
+
clause.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
|
158
|
+
end
|
159
|
+
|
160
|
+
def clean_group_clause_mysql(clause)
|
161
|
+
clause = clause.gsub("DATE_SUB(#{column}, INTERVAL 0 second)", "#{column}")
|
162
|
+
if clause.start_with?("DATE_ADD(") && clause.end_with?(", INTERVAL 0 second)")
|
163
|
+
clause = clause[9..-21]
|
164
|
+
end
|
165
|
+
clause
|
166
|
+
end
|
167
|
+
|
168
|
+
def where_clause
|
169
|
+
if @time_range.is_a?(Range)
|
170
|
+
# doesn't matter whether we include the end of a ... range - it will be excluded later
|
171
|
+
["#{column} >= ? AND #{column} <= ?", @time_range.first, @time_range.last]
|
172
|
+
else
|
173
|
+
["#{column} IS NOT NULL"]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Groupdate
|
2
|
+
class SeriesBuilder
|
3
|
+
attr_reader :period, :time_zone, :day_start, :week_start, :options
|
4
|
+
|
5
|
+
def initialize(period:, time_zone:, day_start:, week_start:, **options)
|
6
|
+
@period = period
|
7
|
+
@time_zone = time_zone
|
8
|
+
@week_start = week_start
|
9
|
+
@day_start = day_start
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate(data, default_value:, series_default: true, multiple_groups: false, group_index: nil)
|
14
|
+
series = generate_series(data, multiple_groups, group_index)
|
15
|
+
series = handle_multiple(data, series, multiple_groups, group_index)
|
16
|
+
|
17
|
+
unless entire_series?(series_default)
|
18
|
+
series = series.select { |k| data[k] }
|
19
|
+
end
|
20
|
+
|
21
|
+
value = 0
|
22
|
+
Hash[series.map do |k|
|
23
|
+
value = data[k] || (@options[:carry_forward] && value) || default_value
|
24
|
+
key =
|
25
|
+
if multiple_groups
|
26
|
+
k[0...group_index] + [key_format.call(k[group_index])] + k[(group_index + 1)..-1]
|
27
|
+
else
|
28
|
+
key_format.call(k)
|
29
|
+
end
|
30
|
+
|
31
|
+
[key, value]
|
32
|
+
end]
|
33
|
+
end
|
34
|
+
|
35
|
+
def round_time(time)
|
36
|
+
time = time.to_time.in_time_zone(time_zone) - day_start.seconds
|
37
|
+
|
38
|
+
time =
|
39
|
+
case period
|
40
|
+
when :second
|
41
|
+
time.change(usec: 0)
|
42
|
+
when :minute
|
43
|
+
time.change(sec: 0)
|
44
|
+
when :hour
|
45
|
+
time.change(min: 0)
|
46
|
+
when :day
|
47
|
+
time.beginning_of_day
|
48
|
+
when :week
|
49
|
+
# same logic as MySQL group
|
50
|
+
weekday = (time.wday - 1) % 7
|
51
|
+
(time - ((7 - week_start + weekday) % 7).days).midnight
|
52
|
+
when :month
|
53
|
+
time.beginning_of_month
|
54
|
+
when :quarter
|
55
|
+
time.beginning_of_quarter
|
56
|
+
when :year
|
57
|
+
time.beginning_of_year
|
58
|
+
when :hour_of_day
|
59
|
+
time.hour
|
60
|
+
when :minute_of_hour
|
61
|
+
time.min
|
62
|
+
when :day_of_week
|
63
|
+
(time.wday - 1 - week_start) % 7
|
64
|
+
when :day_of_month
|
65
|
+
time.day
|
66
|
+
when :month_of_year
|
67
|
+
time.month
|
68
|
+
else
|
69
|
+
raise Groupdate::Error, "Invalid period"
|
70
|
+
end
|
71
|
+
|
72
|
+
time.is_a?(Time) ? time + day_start.seconds : time
|
73
|
+
end
|
74
|
+
|
75
|
+
def time_range
|
76
|
+
@time_range ||= begin
|
77
|
+
time_range = options[:range]
|
78
|
+
if time_range.is_a?(Range) && time_range.first.is_a?(Date)
|
79
|
+
# convert range of dates to range of times
|
80
|
+
# use parsing instead of in_time_zone due to Rails < 4
|
81
|
+
last = time_zone.parse(time_range.last.to_s)
|
82
|
+
last += 1.day unless time_range.exclude_end?
|
83
|
+
time_range = Range.new(time_zone.parse(time_range.first.to_s), last, true)
|
84
|
+
elsif !time_range && options[:last]
|
85
|
+
if period == :quarter
|
86
|
+
step = 3.months
|
87
|
+
elsif 1.respond_to?(period)
|
88
|
+
step = 1.send(period)
|
89
|
+
else
|
90
|
+
raise ArgumentError, "Cannot use last option with #{period}"
|
91
|
+
end
|
92
|
+
if step
|
93
|
+
now = time_zone.now
|
94
|
+
# loop instead of multiply to change start_at - see #151
|
95
|
+
start_at = now
|
96
|
+
(options[:last].to_i - 1).times do
|
97
|
+
start_at -= step
|
98
|
+
end
|
99
|
+
|
100
|
+
time_range =
|
101
|
+
if options[:current] == false
|
102
|
+
round_time(start_at - step)...round_time(now)
|
103
|
+
else
|
104
|
+
round_time(start_at)..now
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
time_range
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def generate_series(data, multiple_groups, group_index)
|
115
|
+
case period
|
116
|
+
when :day_of_week
|
117
|
+
0..6
|
118
|
+
when :hour_of_day
|
119
|
+
0..23
|
120
|
+
when :minute_of_hour
|
121
|
+
0..59
|
122
|
+
when :day_of_month
|
123
|
+
1..31
|
124
|
+
when :month_of_year
|
125
|
+
1..12
|
126
|
+
else
|
127
|
+
time_range = self.time_range
|
128
|
+
time_range =
|
129
|
+
if time_range.is_a?(Range)
|
130
|
+
time_range
|
131
|
+
else
|
132
|
+
# use first and last values
|
133
|
+
sorted_keys =
|
134
|
+
if multiple_groups
|
135
|
+
data.keys.map { |k| k[group_index] }.sort
|
136
|
+
else
|
137
|
+
data.keys.sort
|
138
|
+
end
|
139
|
+
sorted_keys.first..sorted_keys.last
|
140
|
+
end
|
141
|
+
|
142
|
+
if time_range.first
|
143
|
+
series = [round_time(time_range.first)]
|
144
|
+
|
145
|
+
if period == :quarter
|
146
|
+
step = 3.months
|
147
|
+
else
|
148
|
+
step = 1.send(period)
|
149
|
+
end
|
150
|
+
|
151
|
+
last_step = series.last
|
152
|
+
while (next_step = round_time(last_step + step)) && time_range.cover?(next_step)
|
153
|
+
if next_step == last_step
|
154
|
+
last_step += step
|
155
|
+
next
|
156
|
+
end
|
157
|
+
series << next_step
|
158
|
+
last_step = next_step
|
159
|
+
end
|
160
|
+
|
161
|
+
series
|
162
|
+
else
|
163
|
+
[]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def key_format
|
169
|
+
locale = options[:locale] || I18n.locale
|
170
|
+
use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
|
171
|
+
|
172
|
+
if options[:format]
|
173
|
+
if options[:format].respond_to?(:call)
|
174
|
+
options[:format]
|
175
|
+
else
|
176
|
+
sunday = time_zone.parse("2014-03-02 00:00:00")
|
177
|
+
lambda do |key|
|
178
|
+
case period
|
179
|
+
when :hour_of_day
|
180
|
+
key = sunday + key.hours + day_start.seconds
|
181
|
+
when :minute_of_hour
|
182
|
+
key = sunday + key.minutes + day_start.seconds
|
183
|
+
when :day_of_week
|
184
|
+
key = sunday + key.days + (week_start + 1).days
|
185
|
+
when :day_of_month
|
186
|
+
key = Date.new(2014, 1, key).to_time
|
187
|
+
when :month_of_year
|
188
|
+
key = Date.new(2014, key, 1).to_time
|
189
|
+
end
|
190
|
+
I18n.localize(key, format: options[:format], locale: locale)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
elsif [:day, :week, :month, :quarter, :year].include?(period) && use_dates
|
194
|
+
lambda { |k| k.to_date }
|
195
|
+
else
|
196
|
+
lambda { |k| k }
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def handle_multiple(data, series, multiple_groups, group_index)
|
201
|
+
reverse = options[:reverse]
|
202
|
+
|
203
|
+
if multiple_groups
|
204
|
+
keys = data.keys.map { |k| k[0...group_index] + k[(group_index + 1)..-1] }.uniq
|
205
|
+
series = series.to_a.reverse if reverse
|
206
|
+
keys.flat_map do |k|
|
207
|
+
series.map { |s| k[0...group_index] + [s] + k[group_index..-1] }
|
208
|
+
end
|
209
|
+
elsif reverse
|
210
|
+
series.to_a.reverse
|
211
|
+
else
|
212
|
+
series
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def entire_series?(series_default)
|
217
|
+
options.key?(:series) ? options[:series] : series_default
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/lib/groupdate/version.rb
CHANGED
@@ -4,3 +4,9 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec path: "../../"
|
5
5
|
|
6
6
|
gem "activerecord", "~> 4.2.0"
|
7
|
+
|
8
|
+
if defined?(JRUBY_VERSION)
|
9
|
+
gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.24"
|
10
|
+
gem "activerecord-jdbcmysql-adapter", "~> 1.3.24"
|
11
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.24"
|
12
|
+
end
|
@@ -4,3 +4,9 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec path: "../../"
|
5
5
|
|
6
6
|
gem "activerecord", "~> 5.0.0"
|
7
|
+
|
8
|
+
if defined?(JRUBY_VERSION)
|
9
|
+
gem "activerecord-jdbcpostgresql-adapter", "~> 50.0"
|
10
|
+
gem "activerecord-jdbcmysql-adapter", "~> 50.0"
|
11
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 50.0"
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in groupdate.gemspec
|
4
|
+
gemspec path: "../../"
|
5
|
+
|
6
|
+
gem "activerecord", "~> 5.1.0"
|
7
|
+
|
8
|
+
if defined?(JRUBY_VERSION)
|
9
|
+
gem "activerecord-jdbcpostgresql-adapter" # no 51.0 yet
|
10
|
+
gem "activerecord-jdbcmysql-adapter", "~> 51.0"
|
11
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 51.0"
|
12
|
+
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: groupdate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.0.
|
4
|
+
version: 4.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -98,16 +98,16 @@ dependencies:
|
|
98
98
|
name: mysql2
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- - "
|
101
|
+
- - "<"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
103
|
+
version: '0.5'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- - "
|
108
|
+
- - "<"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
110
|
+
version: '0.5'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: sqlite3
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -129,12 +129,12 @@ executables: []
|
|
129
129
|
extensions: []
|
130
130
|
extra_rdoc_files: []
|
131
131
|
files:
|
132
|
+
- ".github/ISSUE_TEMPLATE.md"
|
132
133
|
- ".gitignore"
|
133
134
|
- ".travis.yml"
|
134
135
|
- CHANGELOG.md
|
135
136
|
- CONTRIBUTING.md
|
136
137
|
- Gemfile
|
137
|
-
- ISSUE_TEMPLATE.md
|
138
138
|
- LICENSE.txt
|
139
139
|
- README.md
|
140
140
|
- Rakefile
|
@@ -145,11 +145,13 @@ files:
|
|
145
145
|
- lib/groupdate/magic.rb
|
146
146
|
- lib/groupdate/query_methods.rb
|
147
147
|
- lib/groupdate/relation.rb
|
148
|
+
- lib/groupdate/relation_builder.rb
|
149
|
+
- lib/groupdate/series_builder.rb
|
148
150
|
- lib/groupdate/version.rb
|
149
151
|
- test/enumerable_test.rb
|
150
152
|
- test/gemfiles/activerecord42.gemfile
|
151
153
|
- test/gemfiles/activerecord50.gemfile
|
152
|
-
- test/gemfiles/
|
154
|
+
- test/gemfiles/activerecord51.gemfile
|
153
155
|
- test/gemfiles/redshift.gemfile
|
154
156
|
- test/mysql_test.rb
|
155
157
|
- test/postgresql_test.rb
|
@@ -168,7 +170,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
168
170
|
requirements:
|
169
171
|
- - ">="
|
170
172
|
- !ruby/object:Gem::Version
|
171
|
-
version:
|
173
|
+
version: 2.2.0
|
172
174
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
175
|
requirements:
|
174
176
|
- - ">="
|
@@ -176,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
178
|
version: '0'
|
177
179
|
requirements: []
|
178
180
|
rubyforge_project:
|
179
|
-
rubygems_version: 2.6
|
181
|
+
rubygems_version: 2.7.6
|
180
182
|
signing_key:
|
181
183
|
specification_version: 4
|
182
184
|
summary: The simplest way to group temporal data
|
@@ -184,7 +186,7 @@ test_files:
|
|
184
186
|
- test/enumerable_test.rb
|
185
187
|
- test/gemfiles/activerecord42.gemfile
|
186
188
|
- test/gemfiles/activerecord50.gemfile
|
187
|
-
- test/gemfiles/
|
189
|
+
- test/gemfiles/activerecord51.gemfile
|
188
190
|
- test/gemfiles/redshift.gemfile
|
189
191
|
- test/mysql_test.rb
|
190
192
|
- test/postgresql_test.rb
|