groupdate 5.2.4 → 6.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 +17 -0
- data/LICENSE.txt +1 -1
- data/README.md +15 -20
- data/lib/groupdate/adapters/base_adapter.rb +1 -26
- data/lib/groupdate/adapters/mysql_adapter.rb +15 -9
- data/lib/groupdate/adapters/postgresql_adapter.rb +9 -7
- data/lib/groupdate/adapters/sqlite_adapter.rb +4 -4
- data/lib/groupdate/magic.rb +40 -2
- data/lib/groupdate/relation.rb +3 -0
- data/lib/groupdate/series_builder.rb +38 -81
- data/lib/groupdate/version.rb +1 -1
- data/lib/groupdate.rb +2 -5
- metadata +6 -7
- data/lib/groupdate/adapters/redshift_adapter.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95d89de45bf1e6828b313f8181a5b68ba1ba7c32b173bc0a9c15290e7aec9cd6
|
4
|
+
data.tar.gz: e39b910219e23681dac05095ae0a6a81de70e846f7bc089bb45d0bd3a4849211
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74a5a30dc46ad1f8087889dcd5ef19f995e60b691adaadf8a01576b14086085ee9dece80bc45e8b14bac0dd001f51ca6015e4b503c339cc2ae20c9f8b7dc10f9
|
7
|
+
data.tar.gz: 1067a26e0e5b99c36a93e501eda2f152056e46b8c96ae6cfd1468464cebf0113455fdcbad288730d31d9b527b05411a6c20fb71de6575501b183d07e3aed3d36
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## 6.1.0 (2022-04-05)
|
2
|
+
|
3
|
+
- Added `expand_range` option
|
4
|
+
|
5
|
+
## 6.0.1 (2022-01-16)
|
6
|
+
|
7
|
+
- Fixed incorrect results (error before 6.0) with `includes` with Active Record 6.1+
|
8
|
+
|
9
|
+
## 6.0.0 (2022-01-15)
|
10
|
+
|
11
|
+
- Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments
|
12
|
+
- Raise `ArgumentError` for ranges with string bounds
|
13
|
+
- Added `n` option for Redshift
|
14
|
+
- Changed SQL to return dates instead of times for day, week, month, quarter, and year
|
15
|
+
- Removed `dates` option
|
16
|
+
- Dropped support for Ruby < 2.6 and Rails < 5.2
|
17
|
+
|
1
18
|
## 5.2.4 (2021-12-15)
|
2
19
|
|
3
20
|
- Simplified queries for Active Record 7 and MySQL
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -22,7 +22,7 @@ Supports PostgreSQL, MySQL, and Redshift, plus arrays and hashes (and limited su
|
|
22
22
|
Add this line to your application’s Gemfile:
|
23
23
|
|
24
24
|
```ruby
|
25
|
-
gem
|
25
|
+
gem "groupdate"
|
26
26
|
```
|
27
27
|
|
28
28
|
For MySQL and SQLite, also follow [these instructions](#additional-instructions).
|
@@ -119,6 +119,12 @@ To get a specific time range, use:
|
|
119
119
|
User.group_by_day(:created_at, range: 2.weeks.ago.midnight..Time.now).count
|
120
120
|
```
|
121
121
|
|
122
|
+
To expand the range to the start and end of the time period, use:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
User.group_by_day(:created_at, range: 2.weeks.ago..Time.now, expand_range: true).count
|
126
|
+
```
|
127
|
+
|
122
128
|
To get the most recent time periods, use:
|
123
129
|
|
124
130
|
```ruby
|
@@ -210,19 +216,6 @@ If grouping on date columns which don’t need time zone conversion, use:
|
|
210
216
|
User.group_by_week(:created_on, time_zone: false).count
|
211
217
|
```
|
212
218
|
|
213
|
-
### User Input
|
214
|
-
|
215
|
-
If passing user input as the column, be sure to sanitize it first [like you must](https://rails-sqli.org/) with `group`.
|
216
|
-
|
217
|
-
```ruby
|
218
|
-
column = params[:column]
|
219
|
-
|
220
|
-
# check against permitted columns
|
221
|
-
raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
|
222
|
-
|
223
|
-
User.group_by_day(column).count
|
224
|
-
```
|
225
|
-
|
226
219
|
### Default Scopes
|
227
220
|
|
228
221
|
If you use Postgres and have a default scope that uses `order`, you may get a `column must appear in the GROUP BY clause` error (just like with Active Record’s `group` method). Remove the `order` scope with:
|
@@ -252,7 +245,7 @@ users.group_by_day(series: true) { |u| u.created_at }
|
|
252
245
|
Count
|
253
246
|
|
254
247
|
```ruby
|
255
|
-
users.group_by_day { |u| u.created_at }.
|
248
|
+
users.group_by_day { |u| u.created_at }.to_h { |k, v| [k, v.count] }
|
256
249
|
```
|
257
250
|
|
258
251
|
## Additional Instructions
|
@@ -289,13 +282,15 @@ Groupdate.time_zone = false
|
|
289
282
|
|
290
283
|
## Upgrading
|
291
284
|
|
292
|
-
###
|
285
|
+
### 6.0
|
293
286
|
|
294
|
-
Groupdate
|
287
|
+
Groupdate 6.0 protects against unsafe input by default. For non-attribute arguments, use:
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
User.group_by_day(Arel.sql(known_safe_value)).count
|
291
|
+
```
|
295
292
|
|
296
|
-
|
297
|
-
- The `day_start` option is now consistent between Active Record and enumerable
|
298
|
-
- Deprecated positional arguments for time zone and range have been removed
|
293
|
+
Also, the `dates` option has been removed.
|
299
294
|
|
300
295
|
## History
|
301
296
|
|
@@ -4,11 +4,8 @@ module Groupdate
|
|
4
4
|
attr_reader :period, :column, :day_start, :week_start, :n_seconds
|
5
5
|
|
6
6
|
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:, n_seconds:)
|
7
|
-
# very important
|
8
|
-
column = validate_column(column)
|
9
|
-
|
10
7
|
@relation = relation
|
11
|
-
@column =
|
8
|
+
@column = column
|
12
9
|
@period = period
|
13
10
|
@time_zone = time_zone
|
14
11
|
@time_range = time_range
|
@@ -49,28 +46,6 @@ module Groupdate
|
|
49
46
|
["#{column} IS NOT NULL"]
|
50
47
|
end
|
51
48
|
end
|
52
|
-
|
53
|
-
# basic version of Active Record disallow_raw_sql!
|
54
|
-
# symbol = column (safe), Arel node = SQL (safe), other = untrusted
|
55
|
-
# matches table.column and column
|
56
|
-
def validate_column(column)
|
57
|
-
unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
|
58
|
-
column = column.to_s
|
59
|
-
unless /\A\w+(\.\w+)?\z/i.match(column)
|
60
|
-
warn "[groupdate] Non-attribute argument: #{column}. Use Arel.sql() for known-safe values. This will raise an error in Groupdate 6"
|
61
|
-
end
|
62
|
-
end
|
63
|
-
column
|
64
|
-
end
|
65
|
-
|
66
|
-
# resolves eagerly
|
67
|
-
# need to convert both where_clause (easy)
|
68
|
-
# and group_clause (not easy) if want to avoid this
|
69
|
-
def resolve_column(relation, column)
|
70
|
-
node = relation.send(:relation).send(:arel_columns, [column]).first
|
71
|
-
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
72
|
-
relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
|
73
|
-
end
|
74
49
|
end
|
75
50
|
end
|
76
51
|
end
|
@@ -20,9 +20,21 @@ module Groupdate
|
|
20
20
|
when :month_of_year
|
21
21
|
["MONTH(#{day_start_column})", time_zone, day_start]
|
22
22
|
when :week
|
23
|
-
["
|
23
|
+
["CAST(DATE_FORMAT(#{day_start_column} - INTERVAL ((? + DAYOFWEEK(#{day_start_column})) % 7) DAY, '%Y-%m-%d') AS DATE)", time_zone, day_start, 12 - week_start, time_zone, day_start]
|
24
24
|
when :quarter
|
25
|
-
["
|
25
|
+
["CAST(CONCAT(YEAR(#{day_start_column}), '-', LPAD(1 + 3 * (QUARTER(#{day_start_column}) - 1), 2, '00'), '-01') AS DATE)", time_zone, day_start, time_zone, day_start]
|
26
|
+
when :day, :month, :year
|
27
|
+
format =
|
28
|
+
case period
|
29
|
+
when :day
|
30
|
+
"%Y-%m-%d"
|
31
|
+
when :month
|
32
|
+
"%Y-%m-01"
|
33
|
+
else # year
|
34
|
+
"%Y-01-01"
|
35
|
+
end
|
36
|
+
|
37
|
+
["CAST(DATE_FORMAT(#{day_start_column}, ?) AS DATE)", time_zone, day_start, format]
|
26
38
|
when :custom
|
27
39
|
["FROM_UNIXTIME((UNIX_TIMESTAMP(#{column}) DIV ?) * ?)", n_seconds, n_seconds]
|
28
40
|
else
|
@@ -32,14 +44,8 @@ module Groupdate
|
|
32
44
|
"%Y-%m-%d %H:%i:%S"
|
33
45
|
when :minute
|
34
46
|
"%Y-%m-%d %H:%i:00"
|
35
|
-
|
47
|
+
else # hour
|
36
48
|
"%Y-%m-%d %H:00:00"
|
37
|
-
when :day
|
38
|
-
"%Y-%m-%d 00:00:00"
|
39
|
-
when :month
|
40
|
-
"%Y-%m-01 00:00:00"
|
41
|
-
else # year
|
42
|
-
"%Y-01-01 00:00:00"
|
43
49
|
end
|
44
50
|
|
45
51
|
["CONVERT_TZ(DATE_FORMAT(#{day_start_column}, ?) + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, format, day_start, time_zone]
|
@@ -21,16 +21,18 @@ module Groupdate
|
|
21
21
|
when :month_of_year
|
22
22
|
["EXTRACT(MONTH FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
23
23
|
when :week
|
24
|
-
["(DATE_TRUNC('day', #{day_start_column} - INTERVAL '1 day' * ((? + EXTRACT(DOW FROM #{day_start_column})::integer) % 7)) + INTERVAL ?)
|
24
|
+
["(DATE_TRUNC('day', #{day_start_column} - INTERVAL '1 day' * ((? + EXTRACT(DOW FROM #{day_start_column})::integer) % 7)) + INTERVAL ?)::date", time_zone, day_start_interval, 13 - week_start, time_zone, day_start_interval, day_start_interval]
|
25
25
|
when :custom
|
26
|
-
|
27
|
-
|
28
|
-
if day_start == 0
|
29
|
-
# prettier
|
30
|
-
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
26
|
+
if @relation.connection.adapter_name == "Redshift"
|
27
|
+
["TIMESTAMP 'epoch' + (FLOOR(EXTRACT(EPOCH FROM #{column}::timestamp) / ?) * ?) * INTERVAL '1 second'", n_seconds, n_seconds]
|
31
28
|
else
|
32
|
-
["(
|
29
|
+
["TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM #{column}::timestamptz) / ?) * ?)", n_seconds, n_seconds]
|
33
30
|
end
|
31
|
+
when :day, :month, :quarter, :year
|
32
|
+
["DATE_TRUNC(?, #{day_start_column})::date", period, time_zone, day_start_interval]
|
33
|
+
else
|
34
|
+
# day start is always 0 for seconds, minute, hour
|
35
|
+
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
34
36
|
end
|
35
37
|
|
36
38
|
clean_group_clause(@relation.send(:sanitize_sql_array, query))
|
@@ -7,7 +7,7 @@ module Groupdate
|
|
7
7
|
|
8
8
|
query =
|
9
9
|
if period == :week
|
10
|
-
["strftime('%Y-%m-%d
|
10
|
+
["strftime('%Y-%m-%d', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
|
11
11
|
elsif period == :custom
|
12
12
|
["datetime((strftime('%s', #{column}) / ?) * ?, 'unixepoch')", n_seconds, n_seconds]
|
13
13
|
else
|
@@ -32,13 +32,13 @@ module Groupdate
|
|
32
32
|
when :hour
|
33
33
|
"%Y-%m-%d %H:00:00 UTC"
|
34
34
|
when :day
|
35
|
-
"%Y-%m-%d
|
35
|
+
"%Y-%m-%d"
|
36
36
|
when :month
|
37
|
-
"%Y-%m-01
|
37
|
+
"%Y-%m-01"
|
38
38
|
when :quarter
|
39
39
|
raise Groupdate::Error, "Quarter not supported for SQLite"
|
40
40
|
else # year
|
41
|
-
"%Y-01-01
|
41
|
+
"%Y-01-01"
|
42
42
|
end
|
43
43
|
|
44
44
|
["strftime(?, #{column})", format]
|
data/lib/groupdate/magic.rb
CHANGED
@@ -22,7 +22,7 @@ module Groupdate
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def validate_keywords
|
25
|
-
known_keywords = [:time_zone, :
|
25
|
+
known_keywords = [:time_zone, :series, :format, :locale, :range, :expand_range, :reverse]
|
26
26
|
|
27
27
|
if %i[week day_of_week].include?(period)
|
28
28
|
known_keywords << :week_start
|
@@ -145,6 +145,16 @@ module Groupdate
|
|
145
145
|
lambda { |k| k.to_i }
|
146
146
|
when :day_of_week
|
147
147
|
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
148
|
+
when :day, :week, :month, :quarter, :year
|
149
|
+
# TODO keep as date
|
150
|
+
if day_start != 0
|
151
|
+
day_start_hour = day_start / 3600
|
152
|
+
day_start_min = (day_start % 3600) / 60
|
153
|
+
day_start_sec = (day_start % 3600) % 60
|
154
|
+
lambda { |k| k.in_time_zone(time_zone).change(hour: day_start_hour, min: day_start_min, sec: day_start_sec) }
|
155
|
+
else
|
156
|
+
lambda { |k| k.in_time_zone(time_zone) }
|
157
|
+
end
|
148
158
|
else
|
149
159
|
utc = ActiveSupport::TimeZone["UTC"]
|
150
160
|
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
@@ -193,11 +203,15 @@ module Groupdate
|
|
193
203
|
adapter = Groupdate.adapters[adapter_name]
|
194
204
|
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}" unless adapter
|
195
205
|
|
206
|
+
# very important
|
207
|
+
column = validate_column(field)
|
208
|
+
column = resolve_column(relation, column)
|
209
|
+
|
196
210
|
# generate ActiveRecord relation
|
197
211
|
relation =
|
198
212
|
adapter.new(
|
199
213
|
relation,
|
200
|
-
column:
|
214
|
+
column: column,
|
201
215
|
period: magic.period,
|
202
216
|
time_zone: magic.time_zone,
|
203
217
|
time_range: magic.time_range,
|
@@ -213,6 +227,30 @@ module Groupdate
|
|
213
227
|
relation
|
214
228
|
end
|
215
229
|
|
230
|
+
class << self
|
231
|
+
# basic version of Active Record disallow_raw_sql!
|
232
|
+
# symbol = column (safe), Arel node = SQL (safe), other = untrusted
|
233
|
+
# matches table.column and column
|
234
|
+
def validate_column(column)
|
235
|
+
unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
|
236
|
+
column = column.to_s
|
237
|
+
unless /\A\w+(\.\w+)?\z/i.match(column)
|
238
|
+
raise ActiveRecord::UnknownAttributeReference, "Query method called with non-attribute argument(s): #{column.inspect}. Use Arel.sql() for known-safe values."
|
239
|
+
end
|
240
|
+
end
|
241
|
+
column
|
242
|
+
end
|
243
|
+
|
244
|
+
# resolves eagerly
|
245
|
+
# need to convert both where_clause (easy)
|
246
|
+
# and group_clause (not easy) if want to avoid this
|
247
|
+
def resolve_column(relation, column)
|
248
|
+
node = relation.send(:relation).send(:arel_columns, [column]).first
|
249
|
+
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
250
|
+
relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
216
254
|
# allow any options to keep flexible for future
|
217
255
|
def self.process_result(relation, result, **options)
|
218
256
|
relation.groupdate_values.reverse.each do |gv|
|
data/lib/groupdate/relation.rb
CHANGED
@@ -9,6 +9,9 @@ module Groupdate
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def calculate(*args, &block)
|
12
|
+
# prevent calculate from being called twice
|
13
|
+
return super if ActiveRecord::VERSION::STRING.to_f >= 6.1 && has_include?(args[1])
|
14
|
+
|
12
15
|
default_value = [:count, :sum].include?(args[0]) ? 0 : nil
|
13
16
|
Groupdate.process_result(self, super, default_value: default_value)
|
14
17
|
end
|
@@ -2,8 +2,6 @@ module Groupdate
|
|
2
2
|
class SeriesBuilder
|
3
3
|
attr_reader :period, :time_zone, :day_start, :week_start, :n_seconds, :options
|
4
4
|
|
5
|
-
CHECK_PERIODS = [:day, :week, :month, :quarter, :year]
|
6
|
-
|
7
5
|
def initialize(period:, time_zone:, day_start:, week_start:, n_seconds:, **options)
|
8
6
|
@period = period
|
9
7
|
@time_zone = time_zone
|
@@ -23,41 +21,12 @@ module Groupdate
|
|
23
21
|
verified_data[k] = data.delete(k)
|
24
22
|
end
|
25
23
|
|
26
|
-
# this is a fun one
|
27
|
-
# PostgreSQL and Ruby both return the 2nd hour when converting/parsing a backward DST change
|
28
|
-
# Other databases and Active Support return the 1st hour (as expected)
|
29
|
-
# Active Support good: ActiveSupport::TimeZone["America/Los_Angeles"].parse("2013-11-03 01:00:00")
|
30
|
-
# MySQL good: SELECT CONVERT_TZ('2013-11-03 01:00:00', 'America/Los_Angeles', 'Etc/UTC');
|
31
|
-
# Ruby not good: Time.parse("2013-11-03 01:00:00")
|
32
|
-
# PostgreSQL not good: SELECT '2013-11-03 01:00:00'::timestamp AT TIME ZONE 'America/Los_Angeles';
|
33
|
-
# we need to account for this here
|
34
|
-
if series_default && CHECK_PERIODS.include?(period)
|
35
|
-
data.each do |k, v|
|
36
|
-
key = multiple_groups ? k[group_index] : k
|
37
|
-
# TODO only do this for PostgreSQL
|
38
|
-
# this may mask some inconsistent time zone errors
|
39
|
-
# but not sure there's a better approach
|
40
|
-
if key.hour == (key - 1.hour).hour && series.include?(key - 1.hour)
|
41
|
-
key -= 1.hour
|
42
|
-
if multiple_groups
|
43
|
-
k[group_index] = key
|
44
|
-
else
|
45
|
-
k = key
|
46
|
-
end
|
47
|
-
verified_data[k] = v
|
48
|
-
elsif key != round_time(key)
|
49
|
-
# only need to show what database returned since it will cast in Ruby time zone
|
50
|
-
raise Groupdate::Error, "Database and Ruby have inconsistent time zone info. Database returned #{key}"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
24
|
unless entire_series?(series_default)
|
56
25
|
series = series.select { |k| verified_data[k] }
|
57
26
|
end
|
58
27
|
|
59
28
|
value = 0
|
60
|
-
result =
|
29
|
+
result = series.to_h do |k|
|
61
30
|
value = verified_data[k] || (@options[:carry_forward] && value) || default_value
|
62
31
|
key =
|
63
32
|
if multiple_groups
|
@@ -67,7 +36,7 @@ module Groupdate
|
|
67
36
|
end
|
68
37
|
|
69
38
|
[key, value]
|
70
|
-
end
|
39
|
+
end
|
71
40
|
|
72
41
|
result
|
73
42
|
end
|
@@ -81,7 +50,7 @@ module Groupdate
|
|
81
50
|
|
82
51
|
if day_start != 0
|
83
52
|
# apply day_start to a time object that's not affected by DST
|
84
|
-
time =
|
53
|
+
time = time.change(zone: utc)
|
85
54
|
time -= day_start.seconds
|
86
55
|
end
|
87
56
|
|
@@ -121,23 +90,12 @@ module Groupdate
|
|
121
90
|
|
122
91
|
if day_start != 0 && time.is_a?(Time)
|
123
92
|
time += day_start.seconds
|
124
|
-
time =
|
93
|
+
time = time.change(zone: time_zone)
|
125
94
|
end
|
126
95
|
|
127
96
|
time
|
128
97
|
end
|
129
98
|
|
130
|
-
def change_zone
|
131
|
-
@change_zone ||= begin
|
132
|
-
if ActiveSupport::VERSION::STRING >= "5.2"
|
133
|
-
->(time, zone) { time.change(zone: zone) }
|
134
|
-
else
|
135
|
-
# TODO make more efficient
|
136
|
-
->(time, zone) { zone.parse(time.strftime("%Y-%m-%d %H:%M:%S")) }
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
99
|
def time_range
|
142
100
|
@time_range ||= begin
|
143
101
|
time_range = options[:range]
|
@@ -148,10 +106,6 @@ module Groupdate
|
|
148
106
|
case v
|
149
107
|
when nil, Date, Time
|
150
108
|
# good
|
151
|
-
when String
|
152
|
-
# TODO raise error in Groupdate 6
|
153
|
-
warn "[groupdate] Range bounds should be Date or Time, not #{v.class.name}. This will raise an error in Groupdate 6"
|
154
|
-
break
|
155
109
|
else
|
156
110
|
raise ArgumentError, "Range bounds should be Date or Time, not #{v.class.name}"
|
157
111
|
end
|
@@ -169,32 +123,32 @@ module Groupdate
|
|
169
123
|
exclude_end = true
|
170
124
|
end
|
171
125
|
|
126
|
+
if options[:expand_range]
|
127
|
+
start = round_time(start) if start
|
128
|
+
if finish && !(finish == round_time(finish) && exclude_end)
|
129
|
+
finish = round_time(finish) + step
|
130
|
+
exclude_end = true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
172
134
|
time_range = Range.new(start, finish, exclude_end)
|
173
135
|
elsif !time_range && options[:last]
|
174
|
-
|
175
|
-
|
176
|
-
elsif period == :custom
|
177
|
-
step = n_seconds
|
178
|
-
elsif 1.respond_to?(period)
|
179
|
-
step = 1.send(period)
|
180
|
-
else
|
181
|
-
raise ArgumentError, "Cannot use last option with #{period}"
|
182
|
-
end
|
183
|
-
if step
|
184
|
-
# loop instead of multiply to change start_at - see #151
|
185
|
-
start_at = now
|
186
|
-
(options[:last].to_i - 1).times do
|
187
|
-
start_at -= step
|
188
|
-
end
|
136
|
+
step = step()
|
137
|
+
raise ArgumentError, "Cannot use last option with #{period}" unless step
|
189
138
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
# extend to end of current period
|
195
|
-
round_time(start_at)...(round_time(now) + step)
|
196
|
-
end
|
139
|
+
# loop instead of multiply to change start_at - see #151
|
140
|
+
start_at = now
|
141
|
+
(options[:last].to_i - 1).times do
|
142
|
+
start_at -= step
|
197
143
|
end
|
144
|
+
|
145
|
+
time_range =
|
146
|
+
if options[:current] == false
|
147
|
+
round_time(start_at - step)...round_time(now)
|
148
|
+
else
|
149
|
+
# extend to end of current period
|
150
|
+
round_time(start_at)...(round_time(now) + step)
|
151
|
+
end
|
198
152
|
end
|
199
153
|
time_range
|
200
154
|
end
|
@@ -256,13 +210,7 @@ module Groupdate
|
|
256
210
|
if time_range.begin
|
257
211
|
series = [round_time(time_range.begin)]
|
258
212
|
|
259
|
-
|
260
|
-
step = 3.months
|
261
|
-
elsif period == :custom
|
262
|
-
step = n_seconds
|
263
|
-
else
|
264
|
-
step = 1.send(period)
|
265
|
-
end
|
213
|
+
step = step()
|
266
214
|
|
267
215
|
last_step = series.last
|
268
216
|
day_start_hour = day_start / 3600
|
@@ -289,7 +237,6 @@ module Groupdate
|
|
289
237
|
def key_format
|
290
238
|
@key_format ||= begin
|
291
239
|
locale = options[:locale] || I18n.locale
|
292
|
-
use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
|
293
240
|
|
294
241
|
if options[:format]
|
295
242
|
if options[:format].respond_to?(:call)
|
@@ -312,7 +259,7 @@ module Groupdate
|
|
312
259
|
I18n.localize(key, format: options[:format], locale: locale)
|
313
260
|
end
|
314
261
|
end
|
315
|
-
elsif [:day, :week, :month, :quarter, :year].include?(period)
|
262
|
+
elsif [:day, :week, :month, :quarter, :year].include?(period)
|
316
263
|
lambda { |k| k.to_date }
|
317
264
|
else
|
318
265
|
lambda { |k| k }
|
@@ -320,6 +267,16 @@ module Groupdate
|
|
320
267
|
end
|
321
268
|
end
|
322
269
|
|
270
|
+
def step
|
271
|
+
if period == :quarter
|
272
|
+
3.months
|
273
|
+
elsif period == :custom
|
274
|
+
n_seconds
|
275
|
+
elsif 1.respond_to?(period)
|
276
|
+
1.send(period)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
323
280
|
def handle_multiple(data, series, multiple_groups, group_index)
|
324
281
|
reverse = options[:reverse]
|
325
282
|
|
data/lib/groupdate/version.rb
CHANGED
data/lib/groupdate.rb
CHANGED
@@ -12,7 +12,6 @@ require "groupdate/version"
|
|
12
12
|
require "groupdate/adapters/base_adapter"
|
13
13
|
require "groupdate/adapters/mysql_adapter"
|
14
14
|
require "groupdate/adapters/postgresql_adapter"
|
15
|
-
require "groupdate/adapters/redshift_adapter"
|
16
15
|
require "groupdate/adapters/sqlite_adapter"
|
17
16
|
|
18
17
|
module Groupdate
|
@@ -21,10 +20,9 @@ module Groupdate
|
|
21
20
|
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]
|
22
21
|
METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
|
23
22
|
|
24
|
-
mattr_accessor :week_start, :day_start, :time_zone
|
23
|
+
mattr_accessor :week_start, :day_start, :time_zone
|
25
24
|
self.week_start = :sunday
|
26
25
|
self.day_start = 0
|
27
|
-
self.dates = true
|
28
26
|
|
29
27
|
# api for gems like ActiveMedian
|
30
28
|
def self.process_result(relation, result, **options)
|
@@ -46,8 +44,7 @@ module Groupdate
|
|
46
44
|
end
|
47
45
|
|
48
46
|
Groupdate.register_adapter ["Mysql2", "Mysql2Spatial", "Mysql2Rgeo"], Groupdate::Adapters::MySQLAdapter
|
49
|
-
Groupdate.register_adapter ["PostgreSQL", "PostGIS"], Groupdate::Adapters::PostgreSQLAdapter
|
50
|
-
Groupdate.register_adapter "Redshift", Groupdate::Adapters::RedshiftAdapter
|
47
|
+
Groupdate.register_adapter ["PostgreSQL", "PostGIS", "Redshift"], Groupdate::Adapters::PostgreSQLAdapter
|
51
48
|
Groupdate.register_adapter "SQLite", Groupdate::Adapters::SQLiteAdapter
|
52
49
|
|
53
50
|
require "groupdate/enumerable"
|
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: 6.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: 2022-04-05 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: '5'
|
19
|
+
version: '5.2'
|
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: '5'
|
26
|
+
version: '5.2'
|
27
27
|
description:
|
28
28
|
email: andrew@ankane.org
|
29
29
|
executables: []
|
@@ -39,7 +39,6 @@ files:
|
|
39
39
|
- lib/groupdate/adapters/base_adapter.rb
|
40
40
|
- lib/groupdate/adapters/mysql_adapter.rb
|
41
41
|
- lib/groupdate/adapters/postgresql_adapter.rb
|
42
|
-
- lib/groupdate/adapters/redshift_adapter.rb
|
43
42
|
- lib/groupdate/adapters/sqlite_adapter.rb
|
44
43
|
- lib/groupdate/enumerable.rb
|
45
44
|
- lib/groupdate/magic.rb
|
@@ -59,14 +58,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
58
|
requirements:
|
60
59
|
- - ">="
|
61
60
|
- !ruby/object:Gem::Version
|
62
|
-
version: '2.
|
61
|
+
version: '2.6'
|
63
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
63
|
requirements:
|
65
64
|
- - ">="
|
66
65
|
- !ruby/object:Gem::Version
|
67
66
|
version: '0'
|
68
67
|
requirements: []
|
69
|
-
rubygems_version: 3.
|
68
|
+
rubygems_version: 3.3.7
|
70
69
|
signing_key:
|
71
70
|
specification_version: 4
|
72
71
|
summary: The simplest way to group temporal data
|
@@ -1,39 +0,0 @@
|
|
1
|
-
module Groupdate
|
2
|
-
module Adapters
|
3
|
-
class RedshiftAdapter < BaseAdapter
|
4
|
-
def group_clause
|
5
|
-
time_zone = @time_zone.tzinfo.name
|
6
|
-
day_start_column = "CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL ?"
|
7
|
-
day_start_interval = "#{day_start} second"
|
8
|
-
|
9
|
-
query =
|
10
|
-
case period
|
11
|
-
when :minute_of_hour
|
12
|
-
["EXTRACT(MINUTE from #{day_start_column})::integer", time_zone, day_start_interval]
|
13
|
-
when :hour_of_day
|
14
|
-
["EXTRACT(HOUR from #{day_start_column})::integer", time_zone, day_start_interval]
|
15
|
-
when :day_of_week
|
16
|
-
["EXTRACT(DOW from #{day_start_column})::integer", time_zone, day_start_interval]
|
17
|
-
when :day_of_month
|
18
|
-
["EXTRACT(DAY from #{day_start_column})::integer", time_zone, day_start_interval]
|
19
|
-
when :day_of_year
|
20
|
-
["EXTRACT(DOY from #{day_start_column})::integer", time_zone, day_start_interval]
|
21
|
-
when :month_of_year
|
22
|
-
["EXTRACT(MONTH from #{day_start_column})::integer", time_zone, day_start_interval]
|
23
|
-
when :week # start on Sunday, not Redshift default Monday
|
24
|
-
# Redshift does not return timezone information; it
|
25
|
-
# always says it is in UTC time, so we must convert
|
26
|
-
# back to UTC to play properly with the rest of Groupdate.
|
27
|
-
week_start_interval = "#{week_start} day"
|
28
|
-
["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]
|
29
|
-
when :custom
|
30
|
-
raise Groupdate::Error, "Not implemented yet"
|
31
|
-
else
|
32
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?)::timestamp", time_zone, period, time_zone, day_start_interval, day_start_interval]
|
33
|
-
end
|
34
|
-
|
35
|
-
@relation.send(:sanitize_sql_array, query)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|