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 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