groupdate 4.0.2 → 4.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 +7 -0
- data/README.md +16 -3
- data/lib/groupdate/magic.rb +43 -15
- data/lib/groupdate/relation_builder.rb +2 -2
- data/lib/groupdate/series_builder.rb +41 -5
- 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: e80d5888009185498959b5a739bc9a4517d45b801391a91cfcfc7a0f0b914b68
|
4
|
+
data.tar.gz: 2a6b502d0526c534ca2ffdfbe28cf1fa1c394e3643c9a9546e1c288305022642
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0867e9d04c7cf962813e9ed6ce4c3e8e94f4e468b198ade9278dabe03d5ae6b2538190cb427f074ac469b1ed19edc094f741461b550a6629de99b82aeca592c5'
|
7
|
+
data.tar.gz: 32244b184845925291bbc4cf399d0967413bd17456c9b534a9184f19e965654a8bad239a44d5b0555f7b63090d75e501ddaef449c83becfaf48d9ad136230ca0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## 4.1.0
|
2
|
+
|
3
|
+
- Many performance improvements
|
4
|
+
- Added check for consistent time zone info
|
5
|
+
- Fixed error message for invalid queries with MySQL and SQLite
|
6
|
+
- Fixed issue with enumerable methods ignoring nils
|
7
|
+
|
1
8
|
## 4.0.2
|
2
9
|
|
3
10
|
- Make `current` option work without `last`
|
data/README.md
CHANGED
@@ -48,7 +48,7 @@ and
|
|
48
48
|
- day_of_month
|
49
49
|
- month_of_year
|
50
50
|
|
51
|
-
Use it anywhere you can use `group`.
|
51
|
+
Use it anywhere you can use `group`. Works with `count`, `sum`, `minimum`, `maximum`, `average`, and [`median`](https://github.com/ankane/active_median).
|
52
52
|
|
53
53
|
### Time Zones
|
54
54
|
|
@@ -177,7 +177,7 @@ User.group_by_period(:day, :created_at).count
|
|
177
177
|
Limit groupings with the `permit` option.
|
178
178
|
|
179
179
|
```ruby
|
180
|
-
User.group_by_period(params[:period], :created_at, permit:
|
180
|
+
User.group_by_period(params[:period], :created_at, permit: ["day", "week"]).count
|
181
181
|
```
|
182
182
|
|
183
183
|
Raises an `ArgumentError` for unpermitted periods.
|
@@ -190,6 +190,19 @@ If grouping on date columns which don’t need time zone conversion, use:
|
|
190
190
|
User.group_by_week(:created_on, time_zone: false).count
|
191
191
|
```
|
192
192
|
|
193
|
+
### User Input
|
194
|
+
|
195
|
+
If passing user input as the column, be sure to sanitize it first [like you must](https://rails-sqli.org/) with `group`.
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
column = params[:column]
|
199
|
+
|
200
|
+
# check against permitted columns
|
201
|
+
raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
|
202
|
+
|
203
|
+
User.group_by_day(column).count
|
204
|
+
```
|
205
|
+
|
193
206
|
## Arrays and Hashes
|
194
207
|
|
195
208
|
```ruby
|
@@ -229,7 +242,7 @@ or copy and paste [these statements](https://gist.githubusercontent.com/ankane/1
|
|
229
242
|
You can confirm it worked with:
|
230
243
|
|
231
244
|
```sql
|
232
|
-
SELECT CONVERT_TZ(NOW(), '+00:00', '
|
245
|
+
SELECT CONVERT_TZ(NOW(), '+00:00', 'Pacific/Honolulu');
|
233
246
|
```
|
234
247
|
|
235
248
|
It should return the time instead of `NULL`.
|
data/lib/groupdate/magic.rb
CHANGED
@@ -48,7 +48,11 @@ module Groupdate
|
|
48
48
|
|
49
49
|
class Enumerable < Magic
|
50
50
|
def group_by(enum, &_block)
|
51
|
-
group = enum.group_by
|
51
|
+
group = enum.group_by do |v|
|
52
|
+
v = yield(v)
|
53
|
+
raise ArgumentError, "Not a time" unless v.respond_to?(:to_time)
|
54
|
+
series_builder.round_time(v)
|
55
|
+
end
|
52
56
|
series_builder.generate(group, default_value: [], series_default: false)
|
53
57
|
end
|
54
58
|
|
@@ -66,7 +70,7 @@ module Groupdate
|
|
66
70
|
def perform(relation, result, default_value:)
|
67
71
|
multiple_groups = relation.group_values.size > 1
|
68
72
|
|
69
|
-
|
73
|
+
check_nils(result, multiple_groups, relation)
|
70
74
|
result = cast_result(result, multiple_groups)
|
71
75
|
|
72
76
|
series_builder.generate(
|
@@ -78,25 +82,49 @@ module Groupdate
|
|
78
82
|
end
|
79
83
|
|
80
84
|
def cast_method
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
85
|
+
@cast_method ||= begin
|
86
|
+
case period
|
87
|
+
when :day_of_week
|
88
|
+
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
|
+
else
|
92
|
+
utc = ActiveSupport::TimeZone["UTC"]
|
93
|
+
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
94
|
+
end
|
89
95
|
end
|
90
96
|
end
|
91
97
|
|
92
98
|
def cast_result(result, multiple_groups)
|
93
|
-
|
99
|
+
new_result = {}
|
100
|
+
result.each do |k, v|
|
101
|
+
if multiple_groups
|
102
|
+
k[group_index] = cast_method.call(k[group_index])
|
103
|
+
else
|
104
|
+
k = cast_method.call(k)
|
105
|
+
end
|
106
|
+
new_result[k] = v
|
107
|
+
end
|
108
|
+
new_result
|
109
|
+
end
|
110
|
+
|
111
|
+
def time_zone_support?(relation)
|
112
|
+
if relation.connection.adapter_name =~ /mysql/i
|
113
|
+
sql = relation.send(:sanitize_sql_array, ["SELECT CONVERT_TZ(NOW(), '+00:00', ?)", time_zone.tzinfo.name])
|
114
|
+
!relation.connection.select_all(sql).first.values.first.nil?
|
115
|
+
else
|
116
|
+
true
|
117
|
+
end
|
94
118
|
end
|
95
119
|
|
96
|
-
def
|
97
|
-
|
98
|
-
if
|
99
|
-
|
120
|
+
def check_nils(result, multiple_groups, relation)
|
121
|
+
has_nils = multiple_groups ? (result.keys.first && result.keys.first[group_index].nil?) : result.key?(nil)
|
122
|
+
if has_nils
|
123
|
+
if time_zone_support?(relation)
|
124
|
+
raise Groupdate::Error, "Invalid query - be sure to use a date or time column"
|
125
|
+
else
|
126
|
+
raise Groupdate::Error, "Database missing time zone support for #{time_zone.tzinfo.name} - see https://github.com/ankane/groupdate#for-mysql"
|
127
|
+
end
|
100
128
|
end
|
101
129
|
end
|
102
130
|
|
@@ -167,8 +167,8 @@ module Groupdate
|
|
167
167
|
|
168
168
|
def where_clause
|
169
169
|
if @time_range.is_a?(Range)
|
170
|
-
|
171
|
-
["#{column} >= ? AND #{column}
|
170
|
+
op = @time_range.exclude_end? ? "<" : "<="
|
171
|
+
["#{column} >= ? AND #{column} #{op} ?", @time_range.first, @time_range.last]
|
172
172
|
else
|
173
173
|
["#{column} IS NOT NULL"]
|
174
174
|
end
|
@@ -2,12 +2,15 @@ module Groupdate
|
|
2
2
|
class SeriesBuilder
|
3
3
|
attr_reader :period, :time_zone, :day_start, :week_start, :options
|
4
4
|
|
5
|
+
CHECK_PERIODS = [:day, :week, :month, :quarter, :year]
|
6
|
+
|
5
7
|
def initialize(period:, time_zone:, day_start:, week_start:, **options)
|
6
8
|
@period = period
|
7
9
|
@time_zone = time_zone
|
8
10
|
@week_start = week_start
|
9
11
|
@day_start = day_start
|
10
12
|
@options = options
|
13
|
+
@round_time = {}
|
11
14
|
end
|
12
15
|
|
13
16
|
def generate(data, default_value:, series_default: true, multiple_groups: false, group_index: nil)
|
@@ -19,8 +22,8 @@ module Groupdate
|
|
19
22
|
end
|
20
23
|
|
21
24
|
value = 0
|
22
|
-
Hash[series.map do |k|
|
23
|
-
value = data
|
25
|
+
result = Hash[series.map do |k|
|
26
|
+
value = data.delete(k) || (@options[:carry_forward] && value) || default_value
|
24
27
|
key =
|
25
28
|
if multiple_groups
|
26
29
|
k[0...group_index] + [key_format.call(k[group_index])] + k[(group_index + 1)..-1]
|
@@ -30,10 +33,21 @@ module Groupdate
|
|
30
33
|
|
31
34
|
[key, value]
|
32
35
|
end]
|
36
|
+
|
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
|
+
result
|
33
44
|
end
|
34
45
|
|
35
46
|
def round_time(time)
|
36
|
-
time = time.to_time.in_time_zone(time_zone)
|
47
|
+
time = time.to_time.in_time_zone(time_zone)
|
48
|
+
|
49
|
+
# only if day_start != 0 for performance
|
50
|
+
time -= day_start.seconds if day_start != 0
|
37
51
|
|
38
52
|
time =
|
39
53
|
case period
|
@@ -69,7 +83,10 @@ module Groupdate
|
|
69
83
|
raise Groupdate::Error, "Invalid period"
|
70
84
|
end
|
71
85
|
|
72
|
-
|
86
|
+
# only if day_start != 0 for performance
|
87
|
+
time += day_start.seconds if day_start != 0 && time.is_a?(Time)
|
88
|
+
|
89
|
+
time
|
73
90
|
end
|
74
91
|
|
75
92
|
def time_range
|
@@ -157,7 +174,11 @@ module Groupdate
|
|
157
174
|
end
|
158
175
|
|
159
176
|
last_step = series.last
|
160
|
-
|
177
|
+
loop do
|
178
|
+
next_step = last_step + step
|
179
|
+
next_step = round_time(next_step) if next_step.hour != 0 # add condition to speed up
|
180
|
+
break unless time_range.cover?(next_step)
|
181
|
+
|
161
182
|
if next_step == last_step
|
162
183
|
last_step += step
|
163
184
|
next
|
@@ -221,6 +242,21 @@ module Groupdate
|
|
221
242
|
end
|
222
243
|
end
|
223
244
|
|
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
|
+
|
224
260
|
def entire_series?(series_default)
|
225
261
|
options.key?(:series) ? options[:series] : series_default
|
226
262
|
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.0
|
4
|
+
version: 4.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: 2018-
|
11
|
+
date: 2018-11-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|