groupdate 4.3.0 → 5.0.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 +13 -0
- data/LICENSE.txt +1 -1
- data/README.md +17 -32
- data/lib/groupdate.rb +6 -3
- data/lib/groupdate/enumerable.rb +8 -15
- data/lib/groupdate/magic.rb +39 -7
- data/lib/groupdate/query_methods.rb +3 -12
- data/lib/groupdate/relation_builder.rb +56 -52
- data/lib/groupdate/series_builder.rb +93 -59
- data/lib/groupdate/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fee4526fc30babd6bcffe4121102670b945ac37e5017c47eff03e406a83bf029
|
4
|
+
data.tar.gz: ce960c7d5c3166f212f93c358ffae736b4306bdedfef6a6db4fe137aea628ade
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fbb0de4e049d1765067cc4c0a175be1e1808b2c56016e54566d00f9b87234cc90fa7ac61d3fbf7683597369d67e2be38dcd08dad9ce20d734224f102d00456be
|
7
|
+
data.tar.gz: 465694dd5bfb028c1f7f3cd3c9d5fc9ff3dab28de64f394bf765c5f71fdfed67a797ebaf9f24a7a8d3a808e684b296872f58cd71bae088676e3e767e089ab121
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## 5.0.0 (2020-02-18)
|
2
|
+
|
3
|
+
- Added support for `week_start` for SQLite
|
4
|
+
- Added support for full weekday names
|
5
|
+
- Made `day_start` behavior consistent between Active Record and enumerable
|
6
|
+
- Made `last` option extend to end of current period
|
7
|
+
- Raise error when `day_start` and `week_start` passed to unsupported methods
|
8
|
+
- The `day_start` option no longer applies to shorter periods
|
9
|
+
- Fixed `inconsistent time zone info` errors around DST with MySQL and PostgreSQL
|
10
|
+
- Improved performance of `format` option
|
11
|
+
- Removed deprecated positional arguments for time zone and range
|
12
|
+
- Dropped support for `mysql` gem (last release was 2013)
|
13
|
+
|
1
14
|
## 4.3.0 (2019-12-26)
|
2
15
|
|
3
16
|
- Fixed error with empty results in Ruby 2.7
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -32,9 +32,9 @@ For MySQL and SQLite, also follow [these instructions](#additional-instructions)
|
|
32
32
|
```ruby
|
33
33
|
User.group_by_day(:created_at).count
|
34
34
|
# {
|
35
|
-
# Sat,
|
36
|
-
# Sun,
|
37
|
-
# Mon,
|
35
|
+
# Sat, 24 May 2020 => 50,
|
36
|
+
# Sun, 25 May 2020 => 100,
|
37
|
+
# Mon, 26 May 2020 => 34
|
38
38
|
# }
|
39
39
|
```
|
40
40
|
|
@@ -53,6 +53,7 @@ You can group by:
|
|
53
53
|
|
54
54
|
and
|
55
55
|
|
56
|
+
- minute_of_hour
|
56
57
|
- hour_of_day
|
57
58
|
- day_of_week (Sunday = 0, Monday = 1, etc)
|
58
59
|
- day_of_month
|
@@ -74,9 +75,9 @@ or
|
|
74
75
|
```ruby
|
75
76
|
User.group_by_week(:created_at, time_zone: "Pacific Time (US & Canada)").count
|
76
77
|
# {
|
77
|
-
# Sun,
|
78
|
-
# Sun,
|
79
|
-
# Sun,
|
78
|
+
# Sun, 08 Mar 2020 => 70,
|
79
|
+
# Sun, 15 Mar 2020 => 54,
|
80
|
+
# Sun, 22 Mar 2020 => 80
|
80
81
|
# }
|
81
82
|
```
|
82
83
|
|
@@ -87,13 +88,13 @@ Time zone objects also work. To see a list of available time zones in Rails, run
|
|
87
88
|
Weeks start on Sunday by default. Change this with:
|
88
89
|
|
89
90
|
```ruby
|
90
|
-
Groupdate.week_start = :
|
91
|
+
Groupdate.week_start = :monday
|
91
92
|
```
|
92
93
|
|
93
94
|
or
|
94
95
|
|
95
96
|
```ruby
|
96
|
-
User.group_by_week(:created_at, week_start: :
|
97
|
+
User.group_by_week(:created_at, week_start: :monday).count
|
97
98
|
```
|
98
99
|
|
99
100
|
### Day Start
|
@@ -147,8 +148,8 @@ To get keys in a different format, use:
|
|
147
148
|
```ruby
|
148
149
|
User.group_by_month(:created_at, format: "%b %Y").count
|
149
150
|
# {
|
150
|
-
# "Jan
|
151
|
-
# "Feb
|
151
|
+
# "Jan 2020" => 10
|
152
|
+
# "Feb 2020" => 12
|
152
153
|
# }
|
153
154
|
```
|
154
155
|
|
@@ -263,7 +264,7 @@ It should return the time instead of `NULL`.
|
|
263
264
|
Groupdate has limited support for SQLite.
|
264
265
|
|
265
266
|
- No time zone support
|
266
|
-
- No `day_start`
|
267
|
+
- No `day_start` option
|
267
268
|
- No `group_by_quarter` method
|
268
269
|
|
269
270
|
If your application’s time zone is set to something other than `Etc/UTC` (the default), create an initializer with:
|
@@ -274,29 +275,13 @@ Groupdate.time_zone = false
|
|
274
275
|
|
275
276
|
## Upgrading
|
276
277
|
|
277
|
-
###
|
278
|
+
### 5.0
|
278
279
|
|
279
|
-
Groupdate
|
280
|
+
Groupdate 5.0 brings a number of improvements. Here are a few to be aware of:
|
280
281
|
|
281
|
-
- `
|
282
|
-
-
|
283
|
-
-
|
284
|
-
- Custom calculation methods are supported by default
|
285
|
-
|
286
|
-
### 3.0
|
287
|
-
|
288
|
-
Groupdate 3.0 brings a number of improvements. Here are a few to be aware of:
|
289
|
-
|
290
|
-
- `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`.
|
291
|
-
- Array and hash methods no longer return the entire series by default. Use `series: true` for the previous behavior.
|
292
|
-
- The `series: false` option now returns the correct type and order, and plays nicely with other options.
|
293
|
-
|
294
|
-
### 2.0
|
295
|
-
|
296
|
-
Groupdate 2.0 brings a number of improvements. Here are two things to be aware of:
|
297
|
-
|
298
|
-
- the entire series is returned by default
|
299
|
-
- `ActiveSupport::TimeWithZone` keys are now returned for every database adapter - adapters previously returned `Time` or `String` keys
|
282
|
+
- The `week_start` option is now supported for SQLite
|
283
|
+
- The `day_start` option is now consistent between Active Record and enumerable
|
284
|
+
- Deprecated positional arguments for time zone and range have been removed
|
300
285
|
|
301
286
|
## History
|
302
287
|
|
data/lib/groupdate.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
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
|
@@ -12,7 +15,7 @@ module Groupdate
|
|
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
@@ -2,11 +2,10 @@ module Enumerable
|
|
2
2
|
Groupdate::PERIODS.each do |period|
|
3
3
|
define_method :"group_by_#{period}" do |*args, **options, &block|
|
4
4
|
if block
|
5
|
-
|
6
|
-
|
7
|
-
Groupdate::Magic::Enumerable.group_by(self, period, (args[0] || {}).merge(options), &block)
|
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)
|
8
7
|
elsif respond_to?(:scoping)
|
9
|
-
scoping { @klass.
|
8
|
+
scoping { @klass.group_by_period(period, *args, **options, &block) }
|
10
9
|
else
|
11
10
|
raise ArgumentError, "no block given"
|
12
11
|
end
|
@@ -15,18 +14,12 @@ module Enumerable
|
|
15
14
|
|
16
15
|
def group_by_period(period, *args, **options, &block)
|
17
16
|
if block || !respond_to?(:scoping)
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
#
|
22
|
-
permitted_periods = ((options.delete(:permit) || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
|
23
|
-
if permitted_periods.include?(period.to_s)
|
24
|
-
send("group_by_#{period}", **options, &block)
|
25
|
-
else
|
26
|
-
raise ArgumentError, "Unpermitted period"
|
27
|
-
end
|
17
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 1)" if args.any?
|
18
|
+
|
19
|
+
Groupdate::Magic.validate_period(period, options.delete(:permit))
|
20
|
+
send("group_by_#{period}", **options, &block)
|
28
21
|
else
|
29
|
-
scoping { @klass.
|
22
|
+
scoping { @klass.group_by_period(period, *args, **options, &block) }
|
30
23
|
end
|
31
24
|
end
|
32
25
|
end
|
data/lib/groupdate/magic.rb
CHANGED
@@ -2,18 +2,42 @@ require "i18n"
|
|
2
2
|
|
3
3
|
module Groupdate
|
4
4
|
class Magic
|
5
|
+
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday]
|
6
|
+
|
5
7
|
attr_accessor :period, :options, :group_index
|
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
|
+
end
|
16
|
+
|
17
|
+
def validate_keywords
|
18
|
+
known_keywords = [:time_zone, :dates, :series, :format, :locale, :range, :reverse]
|
19
|
+
|
20
|
+
if %i[week day_of_week].include?(period)
|
21
|
+
known_keywords << :week_start
|
22
|
+
end
|
23
|
+
|
24
|
+
if %i[day week month quarter year day_of_week hour_of_day day_of_month day_of_year month_of_year].include?(period)
|
25
|
+
known_keywords << :day_start
|
26
|
+
else
|
27
|
+
# prevent Groupdate.day_start from applying
|
28
|
+
@day_start = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
unknown_keywords = options.keys - known_keywords
|
12
32
|
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
33
|
+
end
|
13
34
|
|
14
|
-
|
15
|
-
|
16
|
-
raise
|
35
|
+
def validate_arguments
|
36
|
+
# TODO better messages
|
37
|
+
raise ArgumentError, "Unrecognized time zone" unless time_zone
|
38
|
+
raise ArgumentError, "Unrecognized :week_start option" unless week_start
|
39
|
+
raise ArgumentError, "Cannot use endless range for :range option" if options[:range].is_a?(Range) && !options[:range].end
|
40
|
+
raise ArgumentError, ":day_start must be between 0 and 24" if (day_start / 3600) < 0 || (day_start / 3600) >= 24
|
17
41
|
end
|
18
42
|
|
19
43
|
def time_zone
|
@@ -25,7 +49,10 @@ module Groupdate
|
|
25
49
|
end
|
26
50
|
|
27
51
|
def week_start
|
28
|
-
@week_start ||=
|
52
|
+
@week_start ||= begin
|
53
|
+
v = (options[:week_start] || Groupdate.week_start).to_sym
|
54
|
+
DAYS.index(v) || [:mon, :tue, :wed, :thu, :fri, :sat, :sun].index(v)
|
55
|
+
end
|
29
56
|
end
|
30
57
|
|
31
58
|
def day_start
|
@@ -47,6 +74,11 @@ module Groupdate
|
|
47
74
|
series_builder.time_range
|
48
75
|
end
|
49
76
|
|
77
|
+
def self.validate_period(period, permit)
|
78
|
+
permitted_periods = ((permit || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
|
79
|
+
raise ArgumentError, "Unpermitted period" unless permitted_periods.include?(period.to_s)
|
80
|
+
end
|
81
|
+
|
50
82
|
class Enumerable < Magic
|
51
83
|
def group_by(enum, &_block)
|
52
84
|
group = enum.group_by do |v|
|
@@ -85,10 +117,10 @@ module Groupdate
|
|
85
117
|
def cast_method
|
86
118
|
@cast_method ||= begin
|
87
119
|
case period
|
120
|
+
when :minute_of_hour, :hour_of_day, :day_of_month, :day_of_year, :month_of_year
|
121
|
+
lambda { |k| k.to_i }
|
88
122
|
when :day_of_week
|
89
123
|
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
90
|
-
when :hour_of_day, :day_of_month, :day_of_year, :month_of_year, :minute_of_hour
|
91
|
-
lambda { |k| k.to_i }
|
92
124
|
else
|
93
125
|
utc = ActiveSupport::TimeZone["UTC"]
|
94
126
|
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
@@ -1,27 +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,
|
5
|
-
warn "[groupdate] positional arguments for time zone and range are deprecated" if time_zone || range
|
6
|
-
|
4
|
+
define_method :"group_by_#{period}" do |field, **options|
|
7
5
|
Groupdate::Magic::Relation.generate_relation(self,
|
8
6
|
period: period,
|
9
7
|
field: field,
|
10
|
-
time_zone: time_zone,
|
11
|
-
range: range,
|
12
8
|
**options
|
13
9
|
)
|
14
10
|
end
|
15
11
|
end
|
16
12
|
|
17
13
|
def group_by_period(period, field, permit: nil, **options)
|
18
|
-
|
19
|
-
|
20
|
-
if permitted_periods.include?(period.to_s)
|
21
|
-
send("group_by_#{period}", field, **options)
|
22
|
-
else
|
23
|
-
raise ArgumentError, "Unpermitted period"
|
24
|
-
end
|
14
|
+
Groupdate::Magic.validate_period(period, permit)
|
15
|
+
send("group_by_#{period}", field, **options)
|
25
16
|
end
|
26
17
|
end
|
27
18
|
end
|
@@ -27,24 +27,26 @@ module Groupdate
|
|
27
27
|
adapter_name = @relation.connection.adapter_name
|
28
28
|
query =
|
29
29
|
case adapter_name
|
30
|
-
when "
|
30
|
+
when "Mysql2", "Mysql2Spatial", "Mysql2Rgeo"
|
31
|
+
day_start_column = "CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL ? second"
|
32
|
+
|
31
33
|
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 :day_of_year
|
35
|
-
["DAYOFYEAR(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
36
|
-
when :hour_of_day
|
37
|
-
["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
|
38
34
|
when :minute_of_hour
|
39
|
-
["
|
35
|
+
["MINUTE(#{day_start_column})", time_zone, day_start]
|
36
|
+
when :hour_of_day
|
37
|
+
["HOUR(#{day_start_column})", time_zone, day_start]
|
38
|
+
when :day_of_week
|
39
|
+
["DAYOFWEEK(#{day_start_column}) - 1", time_zone, day_start]
|
40
40
|
when :day_of_month
|
41
|
-
["DAYOFMONTH(
|
41
|
+
["DAYOFMONTH(#{day_start_column})", time_zone, day_start]
|
42
|
+
when :day_of_year
|
43
|
+
["DAYOFYEAR(#{day_start_column})", time_zone, day_start]
|
42
44
|
when :month_of_year
|
43
|
-
["MONTH(
|
45
|
+
["MONTH(#{day_start_column})", time_zone, day_start]
|
44
46
|
when :week
|
45
|
-
["CONVERT_TZ(DATE_FORMAT(
|
47
|
+
["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]
|
46
48
|
when :quarter
|
47
|
-
["
|
49
|
+
["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]
|
48
50
|
else
|
49
51
|
format =
|
50
52
|
case period
|
@@ -62,49 +64,56 @@ module Groupdate
|
|
62
64
|
"%Y-01-01 00:00:00"
|
63
65
|
end
|
64
66
|
|
65
|
-
["
|
67
|
+
["CONVERT_TZ(DATE_FORMAT(#{day_start_column}, ?) + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, format, day_start, time_zone]
|
66
68
|
end
|
67
69
|
when "PostgreSQL", "PostGIS"
|
70
|
+
day_start_column = "#{column}::timestamptz AT TIME ZONE ? - INTERVAL ?"
|
71
|
+
day_start_interval = "#{day_start} second"
|
72
|
+
|
68
73
|
case period
|
69
|
-
when :day_of_week
|
70
|
-
["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
71
|
-
when :day_of_year
|
72
|
-
["EXTRACT(DOY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
73
|
-
when :hour_of_day
|
74
|
-
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
75
74
|
when :minute_of_hour
|
76
|
-
["EXTRACT(MINUTE
|
75
|
+
["EXTRACT(MINUTE FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
76
|
+
when :hour_of_day
|
77
|
+
["EXTRACT(HOUR FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
78
|
+
when :day_of_week
|
79
|
+
["EXTRACT(DOW FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
77
80
|
when :day_of_month
|
78
|
-
["EXTRACT(DAY
|
81
|
+
["EXTRACT(DAY FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
82
|
+
when :day_of_year
|
83
|
+
["EXTRACT(DOY FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
79
84
|
when :month_of_year
|
80
|
-
["EXTRACT(MONTH
|
81
|
-
when :week
|
82
|
-
["(DATE_TRUNC('
|
85
|
+
["EXTRACT(MONTH FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
86
|
+
when :week
|
87
|
+
["(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]
|
83
88
|
else
|
84
|
-
|
89
|
+
if day_start == 0
|
90
|
+
# prettier
|
91
|
+
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
92
|
+
else
|
93
|
+
["(DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?) AT TIME ZONE ?", period, time_zone, day_start_interval, day_start_interval, time_zone]
|
94
|
+
end
|
85
95
|
end
|
86
96
|
when "SQLite"
|
87
97
|
raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
|
88
98
|
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
89
|
-
raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
|
90
99
|
|
91
100
|
if period == :week
|
92
|
-
["strftime('
|
101
|
+
["strftime('%Y-%m-%d 00:00:00 UTC', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
|
93
102
|
else
|
94
103
|
format =
|
95
104
|
case period
|
96
|
-
when :hour_of_day
|
97
|
-
"%H"
|
98
105
|
when :minute_of_hour
|
99
106
|
"%M"
|
107
|
+
when :hour_of_day
|
108
|
+
"%H"
|
100
109
|
when :day_of_week
|
101
110
|
"%w"
|
102
111
|
when :day_of_month
|
103
112
|
"%d"
|
104
|
-
when :month_of_year
|
105
|
-
"%m"
|
106
113
|
when :day_of_year
|
107
114
|
"%j"
|
115
|
+
when :month_of_year
|
116
|
+
"%m"
|
108
117
|
when :second
|
109
118
|
"%Y-%m-%d %H:%M:%S UTC"
|
110
119
|
when :minute
|
@@ -121,39 +130,38 @@ module Groupdate
|
|
121
130
|
"%Y-01-01 00:00:00 UTC"
|
122
131
|
end
|
123
132
|
|
124
|
-
["strftime(
|
133
|
+
["strftime(?, #{column})", format]
|
125
134
|
end
|
126
135
|
when "Redshift"
|
136
|
+
day_start_column = "CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL ?"
|
137
|
+
day_start_interval = "#{day_start} second"
|
138
|
+
|
127
139
|
case period
|
128
|
-
when :day_of_week
|
129
|
-
["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
130
|
-
when :hour_of_day
|
131
|
-
["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
132
140
|
when :minute_of_hour
|
133
|
-
["EXTRACT(MINUTE from
|
141
|
+
["EXTRACT(MINUTE from #{day_start_column})::integer", time_zone, day_start_interval]
|
142
|
+
when :hour_of_day
|
143
|
+
["EXTRACT(HOUR from #{day_start_column})::integer", time_zone, day_start_interval]
|
144
|
+
when :day_of_week
|
145
|
+
["EXTRACT(DOW from #{day_start_column})::integer", time_zone, day_start_interval]
|
134
146
|
when :day_of_month
|
135
|
-
["EXTRACT(DAY from
|
147
|
+
["EXTRACT(DAY from #{day_start_column})::integer", time_zone, day_start_interval]
|
136
148
|
when :day_of_year
|
137
|
-
["EXTRACT(DOY from
|
149
|
+
["EXTRACT(DOY from #{day_start_column})::integer", time_zone, day_start_interval]
|
138
150
|
when :month_of_year
|
139
|
-
["EXTRACT(MONTH from
|
151
|
+
["EXTRACT(MONTH from #{day_start_column})::integer", time_zone, day_start_interval]
|
140
152
|
when :week # start on Sunday, not Redshift default Monday
|
141
153
|
# Redshift does not return timezone information; it
|
142
154
|
# always says it is in UTC time, so we must convert
|
143
155
|
# back to UTC to play properly with the rest of Groupdate.
|
144
|
-
#
|
145
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(
|
156
|
+
week_start_interval = "#{week_start} day"
|
157
|
+
["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]
|
146
158
|
else
|
147
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?,
|
159
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?)::timestamp", time_zone, period, time_zone, day_start_interval, day_start_interval]
|
148
160
|
end
|
149
161
|
else
|
150
162
|
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
151
163
|
end
|
152
164
|
|
153
|
-
if adapter_name == "MySQL" && period == :week
|
154
|
-
query[0] = "CAST(#{query[0]} AS DATETIME)"
|
155
|
-
end
|
156
|
-
|
157
165
|
clause = @relation.send(:sanitize_sql_array, query)
|
158
166
|
|
159
167
|
# cleaner queries in logs
|
@@ -166,11 +174,7 @@ module Groupdate
|
|
166
174
|
end
|
167
175
|
|
168
176
|
def clean_group_clause_mysql(clause)
|
169
|
-
clause
|
170
|
-
if clause.start_with?("DATE_ADD(") && clause.end_with?(", INTERVAL 0 second)")
|
171
|
-
clause = clause[9..-21]
|
172
|
-
end
|
173
|
-
clause
|
177
|
+
clause.gsub(/ (\-|\+) INTERVAL 0 second/, "")
|
174
178
|
end
|
175
179
|
|
176
180
|
def where_clause
|
@@ -11,19 +11,54 @@ module Groupdate
|
|
11
11
|
@day_start = day_start
|
12
12
|
@options = options
|
13
13
|
@round_time = {}
|
14
|
+
@week_start_key = Groupdate::Magic::DAYS[@week_start] if @week_start
|
14
15
|
end
|
15
16
|
|
16
17
|
def generate(data, default_value:, series_default: true, multiple_groups: false, group_index: nil)
|
17
18
|
series = generate_series(data, multiple_groups, group_index)
|
18
19
|
series = handle_multiple(data, series, multiple_groups, group_index)
|
19
20
|
|
21
|
+
verified_data = {}
|
22
|
+
series.each do |k|
|
23
|
+
verified_data[k] = data.delete(k)
|
24
|
+
end
|
25
|
+
|
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
|
+
|
20
55
|
unless entire_series?(series_default)
|
21
|
-
series = series.select { |k|
|
56
|
+
series = series.select { |k| verified_data[k] }
|
22
57
|
end
|
23
58
|
|
24
59
|
value = 0
|
25
60
|
result = Hash[series.map do |k|
|
26
|
-
value =
|
61
|
+
value = verified_data[k] || (@options[:carry_forward] && value) || default_value
|
27
62
|
key =
|
28
63
|
if multiple_groups
|
29
64
|
k[0...group_index] + [key_format.call(k[group_index])] + k[(group_index + 1)..-1]
|
@@ -34,20 +69,17 @@ module Groupdate
|
|
34
69
|
[key, value]
|
35
70
|
end]
|
36
71
|
|
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
72
|
result
|
44
73
|
end
|
45
74
|
|
46
75
|
def round_time(time)
|
47
76
|
time = time.to_time.in_time_zone(time_zone)
|
48
77
|
|
49
|
-
|
50
|
-
|
78
|
+
if day_start != 0
|
79
|
+
# apply day_start to a time object that's not affected by DST
|
80
|
+
time = change_zone.call(time, utc)
|
81
|
+
time -= day_start.seconds
|
82
|
+
end
|
51
83
|
|
52
84
|
time =
|
53
85
|
case period
|
@@ -60,9 +92,7 @@ module Groupdate
|
|
60
92
|
when :day
|
61
93
|
time.beginning_of_day
|
62
94
|
when :week
|
63
|
-
|
64
|
-
weekday = (time.wday - 1) % 7
|
65
|
-
(time - ((7 - week_start + weekday) % 7).days).midnight
|
95
|
+
time.beginning_of_week(@week_start_key)
|
66
96
|
when :month
|
67
97
|
time.beginning_of_month
|
68
98
|
when :quarter
|
@@ -74,7 +104,7 @@ module Groupdate
|
|
74
104
|
when :minute_of_hour
|
75
105
|
time.min
|
76
106
|
when :day_of_week
|
77
|
-
|
107
|
+
time.days_to_week_start(@week_start_key)
|
78
108
|
when :day_of_month
|
79
109
|
time.day
|
80
110
|
when :month_of_year
|
@@ -85,21 +115,33 @@ module Groupdate
|
|
85
115
|
raise Groupdate::Error, "Invalid period"
|
86
116
|
end
|
87
117
|
|
88
|
-
|
89
|
-
|
118
|
+
if day_start != 0 && time.is_a?(Time)
|
119
|
+
time += day_start.seconds
|
120
|
+
time = change_zone.call(time, time_zone)
|
121
|
+
end
|
90
122
|
|
91
123
|
time
|
92
124
|
end
|
93
125
|
|
126
|
+
def change_zone
|
127
|
+
@change_zone ||= begin
|
128
|
+
if ActiveSupport::VERSION::STRING >= "5.2"
|
129
|
+
->(time, zone) { time.change(zone: zone) }
|
130
|
+
else
|
131
|
+
# TODO make more efficient
|
132
|
+
->(time, zone) { zone.parse(time.strftime("%Y-%m-%d %H:%M:%S")) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
94
137
|
def time_range
|
95
138
|
@time_range ||= begin
|
96
139
|
time_range = options[:range]
|
97
140
|
if time_range.is_a?(Range) && time_range.first.is_a?(Date)
|
98
141
|
# convert range of dates to range of times
|
99
|
-
|
100
|
-
last = time_zone.parse(time_range.last.to_s)
|
142
|
+
last = time_range.last.in_time_zone(time_zone)
|
101
143
|
last += 1.day unless time_range.exclude_end?
|
102
|
-
time_range = Range.new(
|
144
|
+
time_range = Range.new(time_range.first.in_time_zone(time_zone), last, true)
|
103
145
|
elsif !time_range && options[:last]
|
104
146
|
if period == :quarter
|
105
147
|
step = 3.months
|
@@ -119,7 +161,8 @@ module Groupdate
|
|
119
161
|
if options[:current] == false
|
120
162
|
round_time(start_at - step)...round_time(now)
|
121
163
|
else
|
122
|
-
|
164
|
+
# extend to end of current period
|
165
|
+
round_time(start_at)...(round_time(now) + step)
|
123
166
|
end
|
124
167
|
end
|
125
168
|
end
|
@@ -199,34 +242,36 @@ module Groupdate
|
|
199
242
|
end
|
200
243
|
|
201
244
|
def key_format
|
202
|
-
|
203
|
-
|
245
|
+
@key_format ||= begin
|
246
|
+
locale = options[:locale] || I18n.locale
|
247
|
+
use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
|
204
248
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
249
|
+
if options[:format]
|
250
|
+
if options[:format].respond_to?(:call)
|
251
|
+
options[:format]
|
252
|
+
else
|
253
|
+
sunday = time_zone.parse("2014-03-02 00:00:00")
|
254
|
+
lambda do |key|
|
255
|
+
case period
|
256
|
+
when :hour_of_day
|
257
|
+
key = sunday + key.hours + day_start.seconds
|
258
|
+
when :minute_of_hour
|
259
|
+
key = sunday + key.minutes + day_start.seconds
|
260
|
+
when :day_of_week
|
261
|
+
key = sunday + key.days + (week_start + 1).days
|
262
|
+
when :day_of_month
|
263
|
+
key = Date.new(2014, 1, key).to_time
|
264
|
+
when :month_of_year
|
265
|
+
key = Date.new(2014, key, 1).to_time
|
266
|
+
end
|
267
|
+
I18n.localize(key, format: options[:format], locale: locale)
|
222
268
|
end
|
223
|
-
I18n.localize(key, format: options[:format], locale: locale)
|
224
269
|
end
|
270
|
+
elsif [:day, :week, :month, :quarter, :year].include?(period) && use_dates
|
271
|
+
lambda { |k| k.to_date }
|
272
|
+
else
|
273
|
+
lambda { |k| k }
|
225
274
|
end
|
226
|
-
elsif [:day, :week, :month, :quarter, :year].include?(period) && use_dates
|
227
|
-
lambda { |k| k.to_date }
|
228
|
-
else
|
229
|
-
lambda { |k| k }
|
230
275
|
end
|
231
276
|
end
|
232
277
|
|
@@ -246,23 +291,12 @@ module Groupdate
|
|
246
291
|
end
|
247
292
|
end
|
248
293
|
|
249
|
-
def check_consistent_time_zone_info(data, multiple_groups, group_index)
|
250
|
-
keys = data.keys
|
251
|
-
if multiple_groups
|
252
|
-
keys.map! { |k| k[group_index] }
|
253
|
-
keys.uniq!
|
254
|
-
end
|
255
|
-
|
256
|
-
keys.each do |key|
|
257
|
-
if key != round_time(key)
|
258
|
-
# only need to show what database returned since it will cast in Ruby time zone
|
259
|
-
raise Groupdate::Error, "Database and Ruby have inconsistent time zone info. Database returned #{key}"
|
260
|
-
end
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
294
|
def entire_series?(series_default)
|
265
295
|
options.key?(:series) ? options[:series] : series_default
|
266
296
|
end
|
297
|
+
|
298
|
+
def utc
|
299
|
+
@utc ||= ActiveSupport::TimeZone["Etc/UTC"]
|
300
|
+
end
|
267
301
|
end
|
268
302
|
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.0.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-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|