groupdate 4.1.1 → 5.1.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/CHANGELOG.md +67 -33
- data/CONTRIBUTING.md +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +49 -45
- data/lib/groupdate.rb +7 -4
- data/lib/groupdate/enumerable.rb +9 -15
- data/lib/groupdate/magic.rb +57 -10
- data/lib/groupdate/query_methods.rb +3 -10
- data/lib/groupdate/relation_builder.rb +68 -47
- data/lib/groupdate/series_builder.rb +113 -65
- data/lib/groupdate/version.rb +1 -1
- metadata +14 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 163a969c3eb174890b78b4512c6a6631e02722a43f1fbcc30926f3e48cdba461
|
4
|
+
data.tar.gz: 256ee01a6e991c7513390e8cdf6d27b3e4458018e0980d63e9506bccfc6923d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37093986846259dc59a17187af24097a2ed9d881de6a48ff9a394883e5a936a569fd758daf820cc9444e69212d29548ab21a816f7fac0cba1b7e502ac2d99198
|
7
|
+
data.tar.gz: d1639375454ed0d1d33600226287a484ae7001c3759446b2c196dd24f1e003f3fce7f0c8f2253d90819bc280260bedfed2fa6f622f24ad46c590c05739177df2
|
data/CHANGELOG.md
CHANGED
@@ -1,25 +1,59 @@
|
|
1
|
-
##
|
1
|
+
## 5.1.0 (2020-07-30)
|
2
|
+
|
3
|
+
- Added `n` option to minute and second for custom durations
|
4
|
+
|
5
|
+
## 5.0.0 (2020-02-18)
|
6
|
+
|
7
|
+
- Added support for `week_start` for SQLite
|
8
|
+
- Added support for full weekday names
|
9
|
+
- Made `day_start` behavior consistent between Active Record and enumerable
|
10
|
+
- Made `last` option extend to end of current period
|
11
|
+
- Raise error when `day_start` and `week_start` passed to unsupported methods
|
12
|
+
- The `day_start` option no longer applies to shorter periods
|
13
|
+
- Fixed `inconsistent time zone info` errors around DST with MySQL and PostgreSQL
|
14
|
+
- Improved performance of `format` option
|
15
|
+
- Removed deprecated positional arguments for time zone and range
|
16
|
+
- Dropped support for `mysql` gem (last release was 2013)
|
17
|
+
|
18
|
+
## 4.3.0 (2019-12-26)
|
19
|
+
|
20
|
+
- Fixed error with empty results in Ruby 2.7
|
21
|
+
- Fixed deprecation warnings in Ruby 2.7
|
22
|
+
- Deprecated positional arguments for time zone and range
|
23
|
+
|
24
|
+
## 4.2.0 (2019-10-28)
|
25
|
+
|
26
|
+
- Added `day_of_year`
|
27
|
+
- Dropped support for Rails 4.2
|
28
|
+
|
29
|
+
## 4.1.2 (2019-05-26)
|
30
|
+
|
31
|
+
- Fixed error with empty data and `current: false`
|
32
|
+
- Fixed error in time zone check for Rails < 5.2
|
33
|
+
- Prevent infinite loop with endless ranges
|
34
|
+
|
35
|
+
## 4.1.1 (2018-12-11)
|
2
36
|
|
3
37
|
- Made column resolution consistent with `group`
|
4
38
|
- Added support for `alias_attribute`
|
5
39
|
|
6
|
-
## 4.1.0
|
40
|
+
## 4.1.0 (2018-11-04)
|
7
41
|
|
8
42
|
- Many performance improvements
|
9
43
|
- Added check for consistent time zone info
|
10
44
|
- Fixed error message for invalid queries with MySQL and SQLite
|
11
45
|
- Fixed issue with enumerable methods ignoring nils
|
12
46
|
|
13
|
-
## 4.0.2
|
47
|
+
## 4.0.2 (2018-10-15)
|
14
48
|
|
15
49
|
- Make `current` option work without `last`
|
16
50
|
- Fixed default value for `maximum`, `minimum`, and `average` (periods with no results now return `nil` instead of `0`, pass `default_value: 0` for previous behavior)
|
17
51
|
|
18
|
-
## 4.0.1
|
52
|
+
## 4.0.1 (2018-05-03)
|
19
53
|
|
20
54
|
- Fixed incorrect range with `last` option near time change
|
21
55
|
|
22
|
-
## 4.0.0
|
56
|
+
## 4.0.0 (2018-02-21)
|
23
57
|
|
24
58
|
- Custom calculation methods are supported by default - `groupdate_calculation_methods` is no longer needed
|
25
59
|
|
@@ -31,37 +65,37 @@ Breaking changes
|
|
31
65
|
- `week_start` now affects `day_of_week`
|
32
66
|
- Removed support for `reverse_order` (was never supported in Rails 5)
|
33
67
|
|
34
|
-
## 3.2.1
|
68
|
+
## 3.2.1 (2018-02-21)
|
35
69
|
|
36
70
|
- Added `minute_of_hour`
|
37
71
|
- Added support for `unscoped`
|
38
72
|
|
39
|
-
## 3.2.0
|
73
|
+
## 3.2.0 (2017-01-30)
|
40
74
|
|
41
75
|
- Added limited support for SQLite
|
42
76
|
|
43
|
-
## 3.1.1
|
77
|
+
## 3.1.1 (2016-10-25)
|
44
78
|
|
45
79
|
- Fixed `current: false`
|
46
80
|
- Fixed `last` with `group_by_quarter`
|
47
81
|
- Raise `ArgumentError` when `last` option is not supported
|
48
82
|
|
49
|
-
## 3.1.0
|
83
|
+
## 3.1.0 (2016-10-22)
|
50
84
|
|
51
85
|
- Better support for date columns with `time_zone: false`
|
52
86
|
- Better date range handling for `range` option
|
53
87
|
|
54
|
-
## 3.0.2
|
88
|
+
## 3.0.2 (2016-08-09)
|
55
89
|
|
56
90
|
- Fixed `group_by_period` with associations
|
57
91
|
- Fixed `week_start` option for enumerables
|
58
92
|
|
59
|
-
## 3.0.1
|
93
|
+
## 3.0.1 (2016-07-13)
|
60
94
|
|
61
95
|
- Added support for Redshift
|
62
96
|
- Fix for infinite loop in certain cases for Rails 5
|
63
97
|
|
64
|
-
## 3.0.0
|
98
|
+
## 3.0.0 (2016-05-30)
|
65
99
|
|
66
100
|
Breaking changes
|
67
101
|
|
@@ -69,16 +103,16 @@ Breaking changes
|
|
69
103
|
- Array and hash methods no longer return the entire series by default. Use `series: true` for the previous behavior.
|
70
104
|
- The `series: false` option now returns the correct types and order, and plays nicely with other options.
|
71
105
|
|
72
|
-
## 2.5.3
|
106
|
+
## 2.5.3 (2016-04-28)
|
73
107
|
|
74
108
|
- All tests green with `mysql` gem
|
75
109
|
- Added support for decimal day start
|
76
110
|
|
77
|
-
## 2.5.2
|
111
|
+
## 2.5.2 (2016-02-16)
|
78
112
|
|
79
113
|
- Added `dates` option to return dates for day, week, month, quarter, and year
|
80
114
|
|
81
|
-
## 2.5.1
|
115
|
+
## 2.5.1 (2016-02-03)
|
82
116
|
|
83
117
|
- Added `group_by_quarter`
|
84
118
|
- Added `default_value` option
|
@@ -86,13 +120,13 @@ Breaking changes
|
|
86
120
|
- Raise `ArgumentError` if no field specified
|
87
121
|
- Added support for ActiveRecord 5 beta
|
88
122
|
|
89
|
-
## 2.5.0
|
123
|
+
## 2.5.0 (2015-09-29)
|
90
124
|
|
91
125
|
- Added `group_by_period` method
|
92
126
|
- Added `current` option
|
93
127
|
- Raise `ArgumentError` if no block given to enumerable
|
94
128
|
|
95
|
-
## 2.4.0
|
129
|
+
## 2.4.0 (2014-12-28)
|
96
130
|
|
97
131
|
- Added localization
|
98
132
|
- Added `carry_forward` option
|
@@ -100,77 +134,77 @@ Breaking changes
|
|
100
134
|
- Fixed issue w/ Brasilia Summer Time
|
101
135
|
- Fixed issues w/ ActiveRecord 4.2
|
102
136
|
|
103
|
-
## 2.3.0
|
137
|
+
## 2.3.0 (2014-08-31)
|
104
138
|
|
105
139
|
- Raise error when ActiveRecord::Base.default_timezone is not `:utc`
|
106
140
|
- Added `day_of_month`
|
107
141
|
- Added `month_of_year`
|
108
142
|
- Do not quote column name
|
109
143
|
|
110
|
-
## 2.2.1
|
144
|
+
## 2.2.1 (2014-06-23)
|
111
145
|
|
112
146
|
- Fixed ActiveRecord 3 associations
|
113
147
|
|
114
|
-
## 2.2.0
|
148
|
+
## 2.2.0 (2014-06-22)
|
115
149
|
|
116
150
|
- Added support for arrays and hashes
|
117
151
|
|
118
|
-
## 2.1.1
|
152
|
+
## 2.1.1 (2014-05-17)
|
119
153
|
|
120
154
|
- Fixed format option with multiple groups
|
121
155
|
- Better error message if time zone support is missing for MySQL
|
122
156
|
|
123
|
-
## 2.1.0
|
157
|
+
## 2.1.0 (2014-03-16)
|
124
158
|
|
125
159
|
- Added last option
|
126
160
|
- Added format option
|
127
161
|
|
128
|
-
## 2.0.4
|
162
|
+
## 2.0.4 (2014-03-12)
|
129
163
|
|
130
164
|
- Added multiple groups
|
131
165
|
- Added order
|
132
166
|
- Subsequent methods no longer modify relation
|
133
167
|
|
134
|
-
## 2.0.3
|
168
|
+
## 2.0.3 (2014-03-11)
|
135
169
|
|
136
170
|
- Implemented respond_to?
|
137
171
|
|
138
|
-
## 2.0.2
|
172
|
+
## 2.0.2 (2014-03-11)
|
139
173
|
|
140
174
|
- where, joins, and includes no longer need to be before the group_by method
|
141
175
|
|
142
|
-
## 2.0.1
|
176
|
+
## 2.0.1 (2014-03-07)
|
143
177
|
|
144
178
|
- Use time zone instead of UTC for results
|
145
179
|
|
146
|
-
## 2.0.0
|
180
|
+
## 2.0.0 (2014-03-07)
|
147
181
|
|
148
182
|
- Returns entire series by default
|
149
183
|
- Added day_start option
|
150
184
|
- Better interface
|
151
185
|
|
152
|
-
## 1.0.5
|
186
|
+
## 1.0.5 (2014-03-06)
|
153
187
|
|
154
188
|
- Added global time_zone option
|
155
189
|
|
156
|
-
## 1.0.4
|
190
|
+
## 1.0.4 (2013-07-20)
|
157
191
|
|
158
192
|
- Added global week_start option
|
159
193
|
- Fixed bug with NULL values and series
|
160
194
|
|
161
|
-
## 1.0.3
|
195
|
+
## 1.0.3 (2013-07-05)
|
162
196
|
|
163
197
|
- Fixed deprecation warning when used with will_paginate
|
164
198
|
- Fixed bug with DateTime series
|
165
199
|
|
166
|
-
## 1.0.2
|
200
|
+
## 1.0.2 (2013-06-10)
|
167
201
|
|
168
202
|
- Added :start option for custom week start for group_by_week
|
169
203
|
|
170
|
-
## 1.0.1
|
204
|
+
## 1.0.1 (2013-06-03)
|
171
205
|
|
172
206
|
- Fixed series for Rails < 3.2 and MySQL
|
173
207
|
|
174
|
-
## 1.0.0
|
208
|
+
## 1.0.0 (2013-05-15)
|
175
209
|
|
176
210
|
- First major release
|
data/CONTRIBUTING.md
CHANGED
@@ -57,7 +57,7 @@ brew services start mysql
|
|
57
57
|
|
58
58
|
# create databases
|
59
59
|
createdb groupdate_test
|
60
|
-
|
60
|
+
mysqladmin create groupdate_test
|
61
61
|
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
|
62
62
|
|
63
63
|
# clone the repo and run the tests
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -17,14 +17,24 @@ Supports PostgreSQL, MySQL, and Redshift, plus arrays and hashes (and limited su
|
|
17
17
|
|
18
18
|
[](https://travis-ci.org/ankane/groupdate)
|
19
19
|
|
20
|
-
##
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application’s Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'groupdate'
|
26
|
+
```
|
27
|
+
|
28
|
+
For MySQL and SQLite, also follow [these instructions](#additional-instructions).
|
29
|
+
|
30
|
+
## Getting Started
|
21
31
|
|
22
32
|
```ruby
|
23
33
|
User.group_by_day(:created_at).count
|
24
34
|
# {
|
25
|
-
# Sat,
|
26
|
-
# Sun,
|
27
|
-
# Mon,
|
35
|
+
# Sat, 24 May 2020 => 50,
|
36
|
+
# Sun, 25 May 2020 => 100,
|
37
|
+
# Mon, 26 May 2020 => 34
|
28
38
|
# }
|
29
39
|
```
|
30
40
|
|
@@ -43,12 +53,14 @@ You can group by:
|
|
43
53
|
|
44
54
|
and
|
45
55
|
|
56
|
+
- minute_of_hour
|
46
57
|
- hour_of_day
|
47
58
|
- day_of_week (Sunday = 0, Monday = 1, etc)
|
48
59
|
- day_of_month
|
60
|
+
- day_of_year
|
49
61
|
- month_of_year
|
50
62
|
|
51
|
-
Use it anywhere you can use `group`. Works with `count`, `sum`, `minimum`, `maximum`, and `average`. For `median`, check out [ActiveMedian](https://github.com/ankane/active_median).
|
63
|
+
Use it anywhere you can use `group`. Works with `count`, `sum`, `minimum`, `maximum`, and `average`. For `median`, check out [ActiveMedian](https://github.com/ankane/active_median).
|
52
64
|
|
53
65
|
### Time Zones
|
54
66
|
|
@@ -63,9 +75,9 @@ or
|
|
63
75
|
```ruby
|
64
76
|
User.group_by_week(:created_at, time_zone: "Pacific Time (US & Canada)").count
|
65
77
|
# {
|
66
|
-
# Sun,
|
67
|
-
# Sun,
|
68
|
-
# Sun,
|
78
|
+
# Sun, 08 Mar 2020 => 70,
|
79
|
+
# Sun, 15 Mar 2020 => 54,
|
80
|
+
# Sun, 22 Mar 2020 => 80
|
69
81
|
# }
|
70
82
|
```
|
71
83
|
|
@@ -76,13 +88,13 @@ Time zone objects also work. To see a list of available time zones in Rails, run
|
|
76
88
|
Weeks start on Sunday by default. Change this with:
|
77
89
|
|
78
90
|
```ruby
|
79
|
-
Groupdate.week_start = :
|
91
|
+
Groupdate.week_start = :monday
|
80
92
|
```
|
81
93
|
|
82
94
|
or
|
83
95
|
|
84
96
|
```ruby
|
85
|
-
User.group_by_week(:created_at, week_start: :
|
97
|
+
User.group_by_week(:created_at, week_start: :monday).count
|
86
98
|
```
|
87
99
|
|
88
100
|
### Day Start
|
@@ -136,8 +148,8 @@ To get keys in a different format, use:
|
|
136
148
|
```ruby
|
137
149
|
User.group_by_month(:created_at, format: "%b %Y").count
|
138
150
|
# {
|
139
|
-
# "Jan
|
140
|
-
# "Feb
|
151
|
+
# "Jan 2020" => 10
|
152
|
+
# "Feb 2020" => 12
|
141
153
|
# }
|
142
154
|
```
|
143
155
|
|
@@ -182,6 +194,14 @@ User.group_by_period(params[:period], :created_at, permit: ["day", "week"]).coun
|
|
182
194
|
|
183
195
|
Raises an `ArgumentError` for unpermitted periods.
|
184
196
|
|
197
|
+
### Custom Duration
|
198
|
+
|
199
|
+
To group by a specific number of minutes or seconds, use:
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
User.group_by_minute(:created_at, n: 10).count # 10 minutes
|
203
|
+
```
|
204
|
+
|
185
205
|
### Date Columns
|
186
206
|
|
187
207
|
If grouping on date columns which don’t need time zone conversion, use:
|
@@ -215,23 +235,23 @@ Supports the same options as above
|
|
215
235
|
users.group_by_day(time_zone: time_zone) { |u| u.created_at }
|
216
236
|
```
|
217
237
|
|
218
|
-
|
238
|
+
Get the entire series with:
|
219
239
|
|
220
240
|
```ruby
|
221
|
-
|
241
|
+
users.group_by_day(series: true) { |u| u.created_at }
|
222
242
|
```
|
223
243
|
|
224
|
-
|
225
|
-
|
226
|
-
Add this line to your application’s Gemfile:
|
244
|
+
Count
|
227
245
|
|
228
246
|
```ruby
|
229
|
-
|
247
|
+
users.group_by_day { |u| u.created_at }.map { |k, v| [k, v.count] }.to_h
|
230
248
|
```
|
231
249
|
|
232
|
-
|
250
|
+
## Additional Instructions
|
251
|
+
|
252
|
+
### For MySQL
|
233
253
|
|
234
|
-
[Time zone support](https://dev.mysql.com/doc/refman/
|
254
|
+
[Time zone support](https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html) must be installed on the server.
|
235
255
|
|
236
256
|
```sh
|
237
257
|
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
|
@@ -247,12 +267,12 @@ SELECT CONVERT_TZ(NOW(), '+00:00', 'Pacific/Honolulu');
|
|
247
267
|
|
248
268
|
It should return the time instead of `NULL`.
|
249
269
|
|
250
|
-
|
270
|
+
### For SQLite
|
251
271
|
|
252
272
|
Groupdate has limited support for SQLite.
|
253
273
|
|
254
274
|
- No time zone support
|
255
|
-
- No `day_start`
|
275
|
+
- No `day_start` option
|
256
276
|
- No `group_by_quarter` method
|
257
277
|
|
258
278
|
If your application’s time zone is set to something other than `Etc/UTC` (the default), create an initializer with:
|
@@ -263,36 +283,18 @@ Groupdate.time_zone = false
|
|
263
283
|
|
264
284
|
## Upgrading
|
265
285
|
|
266
|
-
###
|
267
|
-
|
268
|
-
Groupdate 4.0 brings a number of improvements. Here are a few to be aware of:
|
286
|
+
### 5.0
|
269
287
|
|
270
|
-
|
271
|
-
- Invalid options now throw an `ArgumentError`
|
272
|
-
- `week_start` now affects `day_of_week`
|
273
|
-
- Custom calculation methods are supported by default
|
288
|
+
Groupdate 5.0 brings a number of improvements. Here are a few to be aware of:
|
274
289
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
- `Date` objects are now returned for day, week, month, quarter, and year by default. Use `dates: false` for the previous behavior, or change this globally with `Groupdate.dates = false`.
|
280
|
-
- Array and hash methods no longer return the entire series by default. Use `series: true` for the previous behavior.
|
281
|
-
- The `series: false` option now returns the correct type and order, and plays nicely with other options.
|
282
|
-
|
283
|
-
### 2.0
|
284
|
-
|
285
|
-
Groupdate 2.0 brings a number of improvements. Here are two things to be aware of:
|
286
|
-
|
287
|
-
- the entire series is returned by default
|
288
|
-
- `ActiveSupport::TimeWithZone` keys are now returned for every database adapter - adapters previously returned `Time` or `String` keys
|
290
|
+
- The `week_start` option is now supported for SQLite
|
291
|
+
- The `day_start` option is now consistent between Active Record and enumerable
|
292
|
+
- Deprecated positional arguments for time zone and range have been removed
|
289
293
|
|
290
294
|
## History
|
291
295
|
|
292
296
|
View the [changelog](https://github.com/ankane/groupdate/blob/master/CHANGELOG.md)
|
293
297
|
|
294
|
-
Groupdate follows [Semantic Versioning](https://semver.org/)
|
295
|
-
|
296
298
|
## Contributing
|
297
299
|
|
298
300
|
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
@@ -301,3 +303,5 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
|
|
301
303
|
- Fix bugs and [submit pull requests](https://github.com/ankane/groupdate/pulls)
|
302
304
|
- Write, clarify, or fix documentation
|
303
305
|
- Suggest or add new features
|
306
|
+
|
307
|
+
To get started with development and testing, check out the [Contributing Guide](CONTRIBUTING.md).
|
data/lib/groupdate.rb
CHANGED
@@ -1,18 +1,21 @@
|
|
1
|
+
# dependencies
|
1
2
|
require "active_support/core_ext/module/attribute_accessors"
|
2
3
|
require "active_support/time"
|
3
|
-
|
4
|
+
|
5
|
+
# modules
|
6
|
+
require "groupdate/magic"
|
4
7
|
require "groupdate/relation_builder"
|
5
8
|
require "groupdate/series_builder"
|
6
|
-
require "groupdate/
|
9
|
+
require "groupdate/version"
|
7
10
|
|
8
11
|
module Groupdate
|
9
12
|
class Error < RuntimeError; end
|
10
13
|
|
11
|
-
PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :minute_of_hour, :day_of_month, :month_of_year]
|
14
|
+
PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :minute_of_hour, :day_of_month, :day_of_year, :month_of_year]
|
12
15
|
METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
|
13
16
|
|
14
17
|
mattr_accessor :week_start, :day_start, :time_zone, :dates
|
15
|
-
self.week_start = :
|
18
|
+
self.week_start = :sunday
|
16
19
|
self.day_start = 0
|
17
20
|
self.dates = true
|
18
21
|
|
data/lib/groupdate/enumerable.rb
CHANGED
@@ -1,31 +1,25 @@
|
|
1
1
|
module Enumerable
|
2
2
|
Groupdate::PERIODS.each do |period|
|
3
|
-
define_method :"group_by_#{period}" do |*args, &block|
|
3
|
+
define_method :"group_by_#{period}" do |*args, **options, &block|
|
4
4
|
if block
|
5
|
-
|
5
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" if args.any?
|
6
|
+
Groupdate::Magic::Enumerable.group_by(self, period, options, &block)
|
6
7
|
elsif respond_to?(:scoping)
|
7
|
-
scoping { @klass.
|
8
|
+
scoping { @klass.group_by_period(period, *args, **options, &block) }
|
8
9
|
else
|
9
10
|
raise ArgumentError, "no block given"
|
10
11
|
end
|
11
12
|
end
|
12
13
|
end
|
13
14
|
|
14
|
-
def group_by_period(*args, &block)
|
15
|
+
def group_by_period(period, *args, **options, &block)
|
15
16
|
if block || !respond_to?(:scoping)
|
16
|
-
|
17
|
-
options = args[1] || {}
|
17
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 1)" if args.any?
|
18
18
|
|
19
|
-
|
20
|
-
#
|
21
|
-
permitted_periods = ((options.delete(:permit) || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
|
22
|
-
if permitted_periods.include?(period.to_s)
|
23
|
-
send("group_by_#{period}", options, &block)
|
24
|
-
else
|
25
|
-
raise ArgumentError, "Unpermitted period"
|
26
|
-
end
|
19
|
+
Groupdate::Magic.validate_period(period, options.delete(:permit))
|
20
|
+
send("group_by_#{period}", **options, &block)
|
27
21
|
else
|
28
|
-
scoping { @klass.
|
22
|
+
scoping { @klass.group_by_period(period, *args, **options, &block) }
|
29
23
|
end
|
30
24
|
end
|
31
25
|
end
|
data/lib/groupdate/magic.rb
CHANGED
@@ -2,17 +2,53 @@ require "i18n"
|
|
2
2
|
|
3
3
|
module Groupdate
|
4
4
|
class Magic
|
5
|
-
|
5
|
+
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday]
|
6
|
+
|
7
|
+
attr_accessor :period, :options, :group_index, :n_seconds
|
6
8
|
|
7
9
|
def initialize(period:, **options)
|
8
10
|
@period = period
|
9
11
|
@options = options
|
10
12
|
|
11
|
-
|
13
|
+
validate_keywords
|
14
|
+
validate_arguments
|
15
|
+
|
16
|
+
if options[:n]
|
17
|
+
raise ArgumentError, "n must be a positive integer" if !options[:n].is_a?(Integer) || options[:n] < 1
|
18
|
+
@period = :custom
|
19
|
+
@n_seconds = options[:n].to_i
|
20
|
+
@n_seconds *= 60 if period == :minute
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_keywords
|
25
|
+
known_keywords = [:time_zone, :dates, :series, :format, :locale, :range, :reverse]
|
26
|
+
|
27
|
+
if %i[week day_of_week].include?(period)
|
28
|
+
known_keywords << :week_start
|
29
|
+
end
|
30
|
+
|
31
|
+
if %i[day week month quarter year day_of_week hour_of_day day_of_month day_of_year month_of_year].include?(period)
|
32
|
+
known_keywords << :day_start
|
33
|
+
else
|
34
|
+
# prevent Groupdate.day_start from applying
|
35
|
+
@day_start = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
if %i[second minute].include?(period)
|
39
|
+
known_keywords << :n
|
40
|
+
end
|
41
|
+
|
42
|
+
unknown_keywords = options.keys - known_keywords
|
12
43
|
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
44
|
+
end
|
13
45
|
|
14
|
-
|
15
|
-
|
46
|
+
def validate_arguments
|
47
|
+
# TODO better messages
|
48
|
+
raise ArgumentError, "Unrecognized time zone" unless time_zone
|
49
|
+
raise ArgumentError, "Unrecognized :week_start option" unless week_start
|
50
|
+
raise ArgumentError, "Cannot use endless range for :range option" if options[:range].is_a?(Range) && !options[:range].end
|
51
|
+
raise ArgumentError, ":day_start must be between 0 and 24" if (day_start / 3600) < 0 || (day_start / 3600) >= 24
|
16
52
|
end
|
17
53
|
|
18
54
|
def time_zone
|
@@ -24,7 +60,10 @@ module Groupdate
|
|
24
60
|
end
|
25
61
|
|
26
62
|
def week_start
|
27
|
-
@week_start ||=
|
63
|
+
@week_start ||= begin
|
64
|
+
v = (options[:week_start] || Groupdate.week_start).to_sym
|
65
|
+
DAYS.index(v) || [:mon, :tue, :wed, :thu, :fri, :sat, :sun].index(v)
|
66
|
+
end
|
28
67
|
end
|
29
68
|
|
30
69
|
def day_start
|
@@ -38,7 +77,8 @@ module Groupdate
|
|
38
77
|
period: period,
|
39
78
|
time_zone: time_zone,
|
40
79
|
day_start: day_start,
|
41
|
-
week_start: week_start
|
80
|
+
week_start: week_start,
|
81
|
+
n_seconds: n_seconds
|
42
82
|
)
|
43
83
|
end
|
44
84
|
|
@@ -46,6 +86,11 @@ module Groupdate
|
|
46
86
|
series_builder.time_range
|
47
87
|
end
|
48
88
|
|
89
|
+
def self.validate_period(period, permit)
|
90
|
+
permitted_periods = ((permit || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
|
91
|
+
raise ArgumentError, "Unpermitted period" unless permitted_periods.include?(period.to_s)
|
92
|
+
end
|
93
|
+
|
49
94
|
class Enumerable < Magic
|
50
95
|
def group_by(enum, &_block)
|
51
96
|
group = enum.group_by do |v|
|
@@ -84,10 +129,10 @@ module Groupdate
|
|
84
129
|
def cast_method
|
85
130
|
@cast_method ||= begin
|
86
131
|
case period
|
132
|
+
when :minute_of_hour, :hour_of_day, :day_of_month, :day_of_year, :month_of_year
|
133
|
+
lambda { |k| k.to_i }
|
87
134
|
when :day_of_week
|
88
135
|
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
89
|
-
when :hour_of_day, :day_of_month, :month_of_year, :minute_of_hour
|
90
|
-
lambda { |k| k.to_i }
|
91
136
|
else
|
92
137
|
utc = ActiveSupport::TimeZone["UTC"]
|
93
138
|
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
@@ -110,7 +155,8 @@ module Groupdate
|
|
110
155
|
|
111
156
|
def time_zone_support?(relation)
|
112
157
|
if relation.connection.adapter_name =~ /mysql/i
|
113
|
-
|
158
|
+
# need to call klass for Rails < 5.2
|
159
|
+
sql = relation.klass.send(:sanitize_sql_array, ["SELECT CONVERT_TZ(NOW(), '+00:00', ?)", time_zone.tzinfo.name])
|
114
160
|
!relation.connection.select_all(sql).first.values.first.nil?
|
115
161
|
else
|
116
162
|
true
|
@@ -140,7 +186,8 @@ module Groupdate
|
|
140
186
|
time_zone: magic.time_zone,
|
141
187
|
time_range: magic.time_range,
|
142
188
|
week_start: magic.week_start,
|
143
|
-
day_start: magic.day_start
|
189
|
+
day_start: magic.day_start,
|
190
|
+
n_seconds: magic.n_seconds
|
144
191
|
).generate
|
145
192
|
|
146
193
|
# add Groupdate info
|
@@ -1,25 +1,18 @@
|
|
1
1
|
module Groupdate
|
2
2
|
module QueryMethods
|
3
3
|
Groupdate::PERIODS.each do |period|
|
4
|
-
define_method :"group_by_#{period}" do |field,
|
4
|
+
define_method :"group_by_#{period}" do |field, **options|
|
5
5
|
Groupdate::Magic::Relation.generate_relation(self,
|
6
6
|
period: period,
|
7
7
|
field: field,
|
8
|
-
time_zone: time_zone,
|
9
|
-
range: range,
|
10
8
|
**options
|
11
9
|
)
|
12
10
|
end
|
13
11
|
end
|
14
12
|
|
15
13
|
def group_by_period(period, field, permit: nil, **options)
|
16
|
-
|
17
|
-
|
18
|
-
if permitted_periods.include?(period.to_s)
|
19
|
-
send("group_by_#{period}", field, **options)
|
20
|
-
else
|
21
|
-
raise ArgumentError, "Unpermitted period"
|
22
|
-
end
|
14
|
+
Groupdate::Magic.validate_period(period, permit)
|
15
|
+
send("group_by_#{period}", field, **options)
|
23
16
|
end
|
24
17
|
end
|
25
18
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Groupdate
|
2
2
|
class RelationBuilder
|
3
|
-
attr_reader :period, :column, :day_start, :week_start
|
3
|
+
attr_reader :period, :column, :day_start, :week_start, :n_seconds
|
4
4
|
|
5
|
-
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:)
|
5
|
+
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:, n_seconds:)
|
6
6
|
@relation = relation
|
7
7
|
@column = resolve_column(relation, column)
|
8
8
|
@period = period
|
@@ -10,6 +10,7 @@ module Groupdate
|
|
10
10
|
@time_range = time_range
|
11
11
|
@week_start = week_start
|
12
12
|
@day_start = day_start
|
13
|
+
@n_seconds = n_seconds
|
13
14
|
|
14
15
|
if relation.default_timezone == :local
|
15
16
|
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
@@ -27,22 +28,28 @@ module Groupdate
|
|
27
28
|
adapter_name = @relation.connection.adapter_name
|
28
29
|
query =
|
29
30
|
case adapter_name
|
30
|
-
when "
|
31
|
+
when "Mysql2", "Mysql2Spatial", "Mysql2Rgeo"
|
32
|
+
day_start_column = "CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL ? second"
|
33
|
+
|
31
34
|
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
35
|
when :minute_of_hour
|
37
|
-
["
|
36
|
+
["MINUTE(#{day_start_column})", time_zone, day_start]
|
37
|
+
when :hour_of_day
|
38
|
+
["HOUR(#{day_start_column})", time_zone, day_start]
|
39
|
+
when :day_of_week
|
40
|
+
["DAYOFWEEK(#{day_start_column}) - 1", time_zone, day_start]
|
38
41
|
when :day_of_month
|
39
|
-
["DAYOFMONTH(
|
42
|
+
["DAYOFMONTH(#{day_start_column})", time_zone, day_start]
|
43
|
+
when :day_of_year
|
44
|
+
["DAYOFYEAR(#{day_start_column})", time_zone, day_start]
|
40
45
|
when :month_of_year
|
41
|
-
["MONTH(
|
46
|
+
["MONTH(#{day_start_column})", time_zone, day_start]
|
42
47
|
when :week
|
43
|
-
["CONVERT_TZ(DATE_FORMAT(
|
48
|
+
["CONVERT_TZ(DATE_FORMAT(#{day_start_column} - INTERVAL ((? + DAYOFWEEK(#{day_start_column})) % 7) DAY, '%Y-%m-%d 00:00:00') + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, 12 - week_start, time_zone, day_start, day_start, time_zone]
|
44
49
|
when :quarter
|
45
|
-
["
|
50
|
+
["CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(YEAR(#{day_start_column}), '-', LPAD(1 + 3 * (QUARTER(#{day_start_column}) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S') + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, time_zone, day_start, day_start, time_zone]
|
51
|
+
when :custom
|
52
|
+
["FROM_UNIXTIME((UNIX_TIMESTAMP(#{column}) DIV ?) * ?)", n_seconds, n_seconds]
|
46
53
|
else
|
47
54
|
format =
|
48
55
|
case period
|
@@ -60,43 +67,58 @@ module Groupdate
|
|
60
67
|
"%Y-01-01 00:00:00"
|
61
68
|
end
|
62
69
|
|
63
|
-
["
|
70
|
+
["CONVERT_TZ(DATE_FORMAT(#{day_start_column}, ?) + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, format, day_start, time_zone]
|
64
71
|
end
|
65
72
|
when "PostgreSQL", "PostGIS"
|
73
|
+
day_start_column = "#{column}::timestamptz AT TIME ZONE ? - INTERVAL ?"
|
74
|
+
day_start_interval = "#{day_start} second"
|
75
|
+
|
66
76
|
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
77
|
when :minute_of_hour
|
72
|
-
["EXTRACT(MINUTE
|
78
|
+
["EXTRACT(MINUTE FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
79
|
+
when :hour_of_day
|
80
|
+
["EXTRACT(HOUR FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
81
|
+
when :day_of_week
|
82
|
+
["EXTRACT(DOW FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
73
83
|
when :day_of_month
|
74
|
-
["EXTRACT(DAY
|
84
|
+
["EXTRACT(DAY FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
85
|
+
when :day_of_year
|
86
|
+
["EXTRACT(DOY FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
75
87
|
when :month_of_year
|
76
|
-
["EXTRACT(MONTH
|
77
|
-
when :week
|
78
|
-
["(DATE_TRUNC('
|
88
|
+
["EXTRACT(MONTH FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
89
|
+
when :week
|
90
|
+
["(DATE_TRUNC('day', #{day_start_column} - INTERVAL '1 day' * ((? + EXTRACT(DOW FROM #{day_start_column})::integer) % 7)) + INTERVAL ?) AT TIME ZONE ?", time_zone, day_start_interval, 13 - week_start, time_zone, day_start_interval, day_start_interval, time_zone]
|
91
|
+
when :custom
|
92
|
+
["TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM #{column}::timestamptz) / ?) * ?)", n_seconds, n_seconds]
|
79
93
|
else
|
80
|
-
|
94
|
+
if day_start == 0
|
95
|
+
# prettier
|
96
|
+
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
97
|
+
else
|
98
|
+
["(DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?) AT TIME ZONE ?", period, time_zone, day_start_interval, day_start_interval, time_zone]
|
99
|
+
end
|
81
100
|
end
|
82
101
|
when "SQLite"
|
83
102
|
raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
|
84
103
|
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
104
|
|
87
105
|
if period == :week
|
88
|
-
["strftime('
|
106
|
+
["strftime('%Y-%m-%d 00:00:00 UTC', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
|
107
|
+
elsif period == :custom
|
108
|
+
["datetime((strftime('%s', #{column}) / ?) * ?, 'unixepoch')", n_seconds, n_seconds]
|
89
109
|
else
|
90
110
|
format =
|
91
111
|
case period
|
92
|
-
when :hour_of_day
|
93
|
-
"%H"
|
94
112
|
when :minute_of_hour
|
95
113
|
"%M"
|
114
|
+
when :hour_of_day
|
115
|
+
"%H"
|
96
116
|
when :day_of_week
|
97
117
|
"%w"
|
98
118
|
when :day_of_month
|
99
119
|
"%d"
|
120
|
+
when :day_of_year
|
121
|
+
"%j"
|
100
122
|
when :month_of_year
|
101
123
|
"%m"
|
102
124
|
when :second
|
@@ -115,37 +137,40 @@ module Groupdate
|
|
115
137
|
"%Y-01-01 00:00:00 UTC"
|
116
138
|
end
|
117
139
|
|
118
|
-
["strftime(
|
140
|
+
["strftime(?, #{column})", format]
|
119
141
|
end
|
120
142
|
when "Redshift"
|
143
|
+
day_start_column = "CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL ?"
|
144
|
+
day_start_interval = "#{day_start} second"
|
145
|
+
|
121
146
|
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
147
|
when :minute_of_hour
|
127
|
-
["EXTRACT(MINUTE from
|
148
|
+
["EXTRACT(MINUTE from #{day_start_column})::integer", time_zone, day_start_interval]
|
149
|
+
when :hour_of_day
|
150
|
+
["EXTRACT(HOUR from #{day_start_column})::integer", time_zone, day_start_interval]
|
151
|
+
when :day_of_week
|
152
|
+
["EXTRACT(DOW from #{day_start_column})::integer", time_zone, day_start_interval]
|
128
153
|
when :day_of_month
|
129
|
-
["EXTRACT(DAY from
|
154
|
+
["EXTRACT(DAY from #{day_start_column})::integer", time_zone, day_start_interval]
|
155
|
+
when :day_of_year
|
156
|
+
["EXTRACT(DOY from #{day_start_column})::integer", time_zone, day_start_interval]
|
130
157
|
when :month_of_year
|
131
|
-
["EXTRACT(MONTH from
|
158
|
+
["EXTRACT(MONTH from #{day_start_column})::integer", time_zone, day_start_interval]
|
132
159
|
when :week # start on Sunday, not Redshift default Monday
|
133
160
|
# Redshift does not return timezone information; it
|
134
161
|
# always says it is in UTC time, so we must convert
|
135
162
|
# back to UTC to play properly with the rest of Groupdate.
|
136
|
-
#
|
137
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(
|
163
|
+
week_start_interval = "#{week_start} day"
|
164
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC('week', #{day_start_column} - INTERVAL ?) + INTERVAL ? + INTERVAL ?)::timestamp", time_zone, time_zone, day_start_interval, week_start_interval, week_start_interval, day_start_interval]
|
165
|
+
when :custom
|
166
|
+
raise Groupdate::Error, "Not implemented yet"
|
138
167
|
else
|
139
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?,
|
168
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?)::timestamp", time_zone, period, time_zone, day_start_interval, day_start_interval]
|
140
169
|
end
|
141
170
|
else
|
142
171
|
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
143
172
|
end
|
144
173
|
|
145
|
-
if adapter_name == "MySQL" && period == :week
|
146
|
-
query[0] = "CAST(#{query[0]} AS DATETIME)"
|
147
|
-
end
|
148
|
-
|
149
174
|
clause = @relation.send(:sanitize_sql_array, query)
|
150
175
|
|
151
176
|
# cleaner queries in logs
|
@@ -158,11 +183,7 @@ module Groupdate
|
|
158
183
|
end
|
159
184
|
|
160
185
|
def clean_group_clause_mysql(clause)
|
161
|
-
clause
|
162
|
-
if clause.start_with?("DATE_ADD(") && clause.end_with?(", INTERVAL 0 second)")
|
163
|
-
clause = clause[9..-21]
|
164
|
-
end
|
165
|
-
clause
|
186
|
+
clause.gsub(/ (\-|\+) INTERVAL 0 second/, "")
|
166
187
|
end
|
167
188
|
|
168
189
|
def where_clause
|
@@ -1,29 +1,65 @@
|
|
1
1
|
module Groupdate
|
2
2
|
class SeriesBuilder
|
3
|
-
attr_reader :period, :time_zone, :day_start, :week_start, :options
|
3
|
+
attr_reader :period, :time_zone, :day_start, :week_start, :n_seconds, :options
|
4
4
|
|
5
5
|
CHECK_PERIODS = [:day, :week, :month, :quarter, :year]
|
6
6
|
|
7
|
-
def initialize(period:, time_zone:, day_start:, week_start:, **options)
|
7
|
+
def initialize(period:, time_zone:, day_start:, week_start:, n_seconds:, **options)
|
8
8
|
@period = period
|
9
9
|
@time_zone = time_zone
|
10
10
|
@week_start = week_start
|
11
11
|
@day_start = day_start
|
12
|
+
@n_seconds = n_seconds
|
12
13
|
@options = options
|
13
14
|
@round_time = {}
|
15
|
+
@week_start_key = Groupdate::Magic::DAYS[@week_start] if @week_start
|
14
16
|
end
|
15
17
|
|
16
18
|
def generate(data, default_value:, series_default: true, multiple_groups: false, group_index: nil)
|
17
19
|
series = generate_series(data, multiple_groups, group_index)
|
18
20
|
series = handle_multiple(data, series, multiple_groups, group_index)
|
19
21
|
|
22
|
+
verified_data = {}
|
23
|
+
series.each do |k|
|
24
|
+
verified_data[k] = data.delete(k)
|
25
|
+
end
|
26
|
+
|
27
|
+
# this is a fun one
|
28
|
+
# PostgreSQL and Ruby both return the 2nd hour when converting/parsing a backward DST change
|
29
|
+
# Other databases and Active Support return the 1st hour (as expected)
|
30
|
+
# Active Support good: ActiveSupport::TimeZone["America/Los_Angeles"].parse("2013-11-03 01:00:00")
|
31
|
+
# MySQL good: SELECT CONVERT_TZ('2013-11-03 01:00:00', 'America/Los_Angeles', 'Etc/UTC');
|
32
|
+
# Ruby not good: Time.parse("2013-11-03 01:00:00")
|
33
|
+
# PostgreSQL not good: SELECT '2013-11-03 01:00:00'::timestamp AT TIME ZONE 'America/Los_Angeles';
|
34
|
+
# we need to account for this here
|
35
|
+
if series_default && CHECK_PERIODS.include?(period)
|
36
|
+
data.each do |k, v|
|
37
|
+
key = multiple_groups ? k[group_index] : k
|
38
|
+
# TODO only do this for PostgreSQL
|
39
|
+
# this may mask some inconsistent time zone errors
|
40
|
+
# but not sure there's a better approach
|
41
|
+
if key.hour == (key - 1.hour).hour && series.include?(key - 1.hour)
|
42
|
+
key -= 1.hour
|
43
|
+
if multiple_groups
|
44
|
+
k[group_index] = key
|
45
|
+
else
|
46
|
+
k = key
|
47
|
+
end
|
48
|
+
verified_data[k] = v
|
49
|
+
elsif key != round_time(key)
|
50
|
+
# only need to show what database returned since it will cast in Ruby time zone
|
51
|
+
raise Groupdate::Error, "Database and Ruby have inconsistent time zone info. Database returned #{key}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
20
56
|
unless entire_series?(series_default)
|
21
|
-
series = series.select { |k|
|
57
|
+
series = series.select { |k| verified_data[k] }
|
22
58
|
end
|
23
59
|
|
24
60
|
value = 0
|
25
61
|
result = Hash[series.map do |k|
|
26
|
-
value =
|
62
|
+
value = verified_data[k] || (@options[:carry_forward] && value) || default_value
|
27
63
|
key =
|
28
64
|
if multiple_groups
|
29
65
|
k[0...group_index] + [key_format.call(k[group_index])] + k[(group_index + 1)..-1]
|
@@ -34,20 +70,21 @@ module Groupdate
|
|
34
70
|
[key, value]
|
35
71
|
end]
|
36
72
|
|
37
|
-
# only check for database
|
38
|
-
# only checks remaining keys to avoid expensive calls to round_time
|
39
|
-
if series_default && CHECK_PERIODS.include?(period)
|
40
|
-
check_consistent_time_zone_info(data, multiple_groups, group_index)
|
41
|
-
end
|
42
|
-
|
43
73
|
result
|
44
74
|
end
|
45
75
|
|
46
76
|
def round_time(time)
|
77
|
+
if period == :custom
|
78
|
+
return time_zone.at((time.to_time.to_i / n_seconds) * n_seconds)
|
79
|
+
end
|
80
|
+
|
47
81
|
time = time.to_time.in_time_zone(time_zone)
|
48
82
|
|
49
|
-
|
50
|
-
|
83
|
+
if day_start != 0
|
84
|
+
# apply day_start to a time object that's not affected by DST
|
85
|
+
time = change_zone.call(time, utc)
|
86
|
+
time -= day_start.seconds
|
87
|
+
end
|
51
88
|
|
52
89
|
time =
|
53
90
|
case period
|
@@ -60,9 +97,7 @@ module Groupdate
|
|
60
97
|
when :day
|
61
98
|
time.beginning_of_day
|
62
99
|
when :week
|
63
|
-
|
64
|
-
weekday = (time.wday - 1) % 7
|
65
|
-
(time - ((7 - week_start + weekday) % 7).days).midnight
|
100
|
+
time.beginning_of_week(@week_start_key)
|
66
101
|
when :month
|
67
102
|
time.beginning_of_month
|
68
103
|
when :quarter
|
@@ -74,33 +109,49 @@ module Groupdate
|
|
74
109
|
when :minute_of_hour
|
75
110
|
time.min
|
76
111
|
when :day_of_week
|
77
|
-
|
112
|
+
time.days_to_week_start(@week_start_key)
|
78
113
|
when :day_of_month
|
79
114
|
time.day
|
80
115
|
when :month_of_year
|
81
116
|
time.month
|
117
|
+
when :day_of_year
|
118
|
+
time.yday
|
82
119
|
else
|
83
120
|
raise Groupdate::Error, "Invalid period"
|
84
121
|
end
|
85
122
|
|
86
|
-
|
87
|
-
|
123
|
+
if day_start != 0 && time.is_a?(Time)
|
124
|
+
time += day_start.seconds
|
125
|
+
time = change_zone.call(time, time_zone)
|
126
|
+
end
|
88
127
|
|
89
128
|
time
|
90
129
|
end
|
91
130
|
|
131
|
+
def change_zone
|
132
|
+
@change_zone ||= begin
|
133
|
+
if ActiveSupport::VERSION::STRING >= "5.2"
|
134
|
+
->(time, zone) { time.change(zone: zone) }
|
135
|
+
else
|
136
|
+
# TODO make more efficient
|
137
|
+
->(time, zone) { zone.parse(time.strftime("%Y-%m-%d %H:%M:%S")) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
92
142
|
def time_range
|
93
143
|
@time_range ||= begin
|
94
144
|
time_range = options[:range]
|
95
145
|
if time_range.is_a?(Range) && time_range.first.is_a?(Date)
|
96
146
|
# convert range of dates to range of times
|
97
|
-
|
98
|
-
last = time_zone.parse(time_range.last.to_s)
|
147
|
+
last = time_range.last.in_time_zone(time_zone)
|
99
148
|
last += 1.day unless time_range.exclude_end?
|
100
|
-
time_range = Range.new(
|
149
|
+
time_range = Range.new(time_range.first.in_time_zone(time_zone), last, true)
|
101
150
|
elsif !time_range && options[:last]
|
102
151
|
if period == :quarter
|
103
152
|
step = 3.months
|
153
|
+
elsif period == :custom
|
154
|
+
step = n_seconds
|
104
155
|
elsif 1.respond_to?(period)
|
105
156
|
step = 1.send(period)
|
106
157
|
else
|
@@ -117,7 +168,8 @@ module Groupdate
|
|
117
168
|
if options[:current] == false
|
118
169
|
round_time(start_at - step)...round_time(now)
|
119
170
|
else
|
120
|
-
|
171
|
+
# extend to end of current period
|
172
|
+
round_time(start_at)...(round_time(now) + step)
|
121
173
|
end
|
122
174
|
end
|
123
175
|
end
|
@@ -141,6 +193,8 @@ module Groupdate
|
|
141
193
|
0..59
|
142
194
|
when :day_of_month
|
143
195
|
1..31
|
196
|
+
when :day_of_year
|
197
|
+
1..366
|
144
198
|
when :month_of_year
|
145
199
|
1..12
|
146
200
|
else
|
@@ -158,25 +212,28 @@ module Groupdate
|
|
158
212
|
end
|
159
213
|
|
160
214
|
tr = sorted_keys.first..sorted_keys.last
|
161
|
-
if options[:current] == false && round_time(now) >= tr.last
|
215
|
+
if options[:current] == false && sorted_keys.any? && round_time(now) >= tr.last
|
162
216
|
tr = tr.first...round_time(now)
|
163
217
|
end
|
164
218
|
tr
|
165
219
|
end
|
166
220
|
|
167
|
-
if time_range.
|
168
|
-
series = [round_time(time_range.
|
221
|
+
if time_range.begin
|
222
|
+
series = [round_time(time_range.begin)]
|
169
223
|
|
170
224
|
if period == :quarter
|
171
225
|
step = 3.months
|
226
|
+
elsif period == :custom
|
227
|
+
step = n_seconds
|
172
228
|
else
|
173
229
|
step = 1.send(period)
|
174
230
|
end
|
175
231
|
|
176
232
|
last_step = series.last
|
233
|
+
day_start_hour = day_start / 3600
|
177
234
|
loop do
|
178
235
|
next_step = last_step + step
|
179
|
-
next_step = round_time(next_step) if next_step.hour !=
|
236
|
+
next_step = round_time(next_step) if next_step.hour != day_start_hour # add condition to speed up
|
180
237
|
break unless time_range.cover?(next_step)
|
181
238
|
|
182
239
|
if next_step == last_step
|
@@ -195,34 +252,36 @@ module Groupdate
|
|
195
252
|
end
|
196
253
|
|
197
254
|
def key_format
|
198
|
-
|
199
|
-
|
255
|
+
@key_format ||= begin
|
256
|
+
locale = options[:locale] || I18n.locale
|
257
|
+
use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
|
200
258
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
259
|
+
if options[:format]
|
260
|
+
if options[:format].respond_to?(:call)
|
261
|
+
options[:format]
|
262
|
+
else
|
263
|
+
sunday = time_zone.parse("2014-03-02 00:00:00")
|
264
|
+
lambda do |key|
|
265
|
+
case period
|
266
|
+
when :hour_of_day
|
267
|
+
key = sunday + key.hours + day_start.seconds
|
268
|
+
when :minute_of_hour
|
269
|
+
key = sunday + key.minutes + day_start.seconds
|
270
|
+
when :day_of_week
|
271
|
+
key = sunday + key.days + (week_start + 1).days
|
272
|
+
when :day_of_month
|
273
|
+
key = Date.new(2014, 1, key).to_time
|
274
|
+
when :month_of_year
|
275
|
+
key = Date.new(2014, key, 1).to_time
|
276
|
+
end
|
277
|
+
I18n.localize(key, format: options[:format], locale: locale)
|
218
278
|
end
|
219
|
-
I18n.localize(key, format: options[:format], locale: locale)
|
220
279
|
end
|
280
|
+
elsif [:day, :week, :month, :quarter, :year].include?(period) && use_dates
|
281
|
+
lambda { |k| k.to_date }
|
282
|
+
else
|
283
|
+
lambda { |k| k }
|
221
284
|
end
|
222
|
-
elsif [:day, :week, :month, :quarter, :year].include?(period) && use_dates
|
223
|
-
lambda { |k| k.to_date }
|
224
|
-
else
|
225
|
-
lambda { |k| k }
|
226
285
|
end
|
227
286
|
end
|
228
287
|
|
@@ -242,23 +301,12 @@ module Groupdate
|
|
242
301
|
end
|
243
302
|
end
|
244
303
|
|
245
|
-
def check_consistent_time_zone_info(data, multiple_groups, group_index)
|
246
|
-
keys = data.keys
|
247
|
-
if multiple_groups
|
248
|
-
keys.map! { |k| k[group_index] }
|
249
|
-
keys.uniq!
|
250
|
-
end
|
251
|
-
|
252
|
-
keys.each do |key|
|
253
|
-
if key != round_time(key)
|
254
|
-
# only need to show what database returned since it will cast in Ruby time zone
|
255
|
-
raise Groupdate::Error, "Database and Ruby have inconsistent time zone info. Database returned #{key}"
|
256
|
-
end
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
304
|
def entire_series?(series_default)
|
261
305
|
options.key?(:series) ? options[:series] : series_default
|
262
306
|
end
|
307
|
+
|
308
|
+
def utc
|
309
|
+
@utc ||= ActiveSupport::TimeZone["Etc/UTC"]
|
310
|
+
end
|
263
311
|
end
|
264
312
|
end
|
data/lib/groupdate/version.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
|
+
version: 5.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '5'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -84,30 +84,30 @@ dependencies:
|
|
84
84
|
name: pg
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
96
|
+
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
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'
|
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'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: sqlite3
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -153,15 +153,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
153
153
|
requirements:
|
154
154
|
- - ">="
|
155
155
|
- !ruby/object:Gem::Version
|
156
|
-
version: '2.
|
156
|
+
version: '2.4'
|
157
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
158
|
requirements:
|
159
159
|
- - ">="
|
160
160
|
- !ruby/object:Gem::Version
|
161
161
|
version: '0'
|
162
162
|
requirements: []
|
163
|
-
|
164
|
-
rubygems_version: 2.7.6
|
163
|
+
rubygems_version: 3.1.2
|
165
164
|
signing_key:
|
166
165
|
specification_version: 4
|
167
166
|
summary: The simplest way to group temporal data
|