groupdate 4.0.2 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 624c0ae83a0c0952676b3d3efb79826b2b470d0a5efab6da31523ca8284db8f3
4
- data.tar.gz: 6c15c17557aee5b8babd8b2939af9325449a4831ed0ee24901c27d37b7352fe3
3
+ metadata.gz: e80d5888009185498959b5a739bc9a4517d45b801391a91cfcfc7a0f0b914b68
4
+ data.tar.gz: 2a6b502d0526c534ca2ffdfbe28cf1fa1c394e3643c9a9546e1c288305022642
5
5
  SHA512:
6
- metadata.gz: dfa2c03830675352029e74768bad800acd7ac56edc9dbcdb01f58787f34858120c4f9f1d1436ee4bdb38480922b111d746b75e3f0b7ff59f5bb770e74e01c4c9
7
- data.tar.gz: f300e95cb3a7098486c910a4e94a8ecd4313fad14988b78fc68495783d99f49fef643f8361a4566f9ad2737238393a88c79efbc7df41cd28251d962916264626
6
+ metadata.gz: '0867e9d04c7cf962813e9ed6ce4c3e8e94f4e468b198ade9278dabe03d5ae6b2538190cb427f074ac469b1ed19edc094f741461b550a6629de99b82aeca592c5'
7
+ data.tar.gz: 32244b184845925291bbc4cf399d0967413bd17456c9b534a9184f19e965654a8bad239a44d5b0555f7b63090d75e501ddaef449c83becfaf48d9ad136230ca0
@@ -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: %w[day week]).count
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', 'Etc/UTC');
245
+ SELECT CONVERT_TZ(NOW(), '+00:00', 'Pacific/Honolulu');
233
246
  ```
234
247
 
235
248
  It should return the time instead of `NULL`.
@@ -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 { |v| v = yield(v); v ? series_builder.round_time(v) : nil }
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
- check_time_zone_support(result, multiple_groups)
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
- case period
82
- when :day_of_week
83
- lambda { |k| (k.to_i - 1 - week_start) % 7 }
84
- when :hour_of_day, :day_of_month, :month_of_year, :minute_of_hour
85
- lambda { |k| k.to_i }
86
- else
87
- utc = ActiveSupport::TimeZone["UTC"]
88
- lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
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
- Hash[result.map { |k, v| [multiple_groups ? k[0...group_index] + [cast_method.call(k[group_index])] + k[(group_index + 1)..-1] : cast_method.call(k), v] }]
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 check_time_zone_support(result, multiple_groups)
97
- missing_time_zone_support = multiple_groups ? (result.keys.first && result.keys.first[group_index].nil?) : result.key?(nil)
98
- if missing_time_zone_support
99
- raise Groupdate::Error, "Be sure to install time zone support - https://github.com/ankane/groupdate#for-mysql"
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
- # doesn't matter whether we include the end of a ... range - it will be excluded later
171
- ["#{column} >= ? AND #{column} <= ?", @time_range.first, @time_range.last]
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[k] || (@options[:carry_forward] && value) || default_value
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) - day_start.seconds
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
- time.is_a?(Time) ? time + day_start.seconds : time
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
- while (next_step = round_time(last_step + step)) && time_range.cover?(next_step)
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
@@ -1,3 +1,3 @@
1
1
  module Groupdate
2
- VERSION = "4.0.2"
2
+ VERSION = "4.1.0"
3
3
  end
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.2
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-10-15 00:00:00.000000000 Z
11
+ date: 2018-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport