groupdate 3.2.1 → 4.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/.travis.yml +1 -3
- data/CHANGELOG.md +12 -0
- data/CONTRIBUTING.md +3 -0
- data/LICENSE.txt +1 -1
- data/README.md +9 -31
- data/Rakefile +0 -15
- data/groupdate.gemspec +1 -1
- data/lib/groupdate.rb +0 -2
- data/lib/groupdate/active_record.rb +4 -51
- data/lib/groupdate/enumerable.rb +3 -2
- data/lib/groupdate/magic.rb +219 -196
- data/lib/groupdate/query_methods.rb +25 -0
- data/lib/groupdate/relation.rb +19 -0
- data/lib/groupdate/version.rb +1 -1
- data/test/gemfiles/redshift.gemfile +2 -2
- data/test/sqlite_test.rb +0 -5
- data/test/test_helper.rb +29 -31
- metadata +5 -15
- data/lib/groupdate/calculations.rb +0 -26
- data/lib/groupdate/order_hack.rb +0 -11
- data/lib/groupdate/scopes.rb +0 -24
- data/lib/groupdate/series.rb +0 -34
- data/test/gemfiles/activerecord31.gemfile +0 -6
- data/test/gemfiles/activerecord32.gemfile +0 -6
- data/test/gemfiles/activerecord40.gemfile +0 -6
- data/test/gemfiles/activerecord41.gemfile +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6b498ed092c226ed3e7eeec42f68ff8a4b092c1
|
4
|
+
data.tar.gz: a43c5abd7af5dcc458825c8e151184fc30d98626
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6379307ca9ef9c5d3435490bb61cea9d8afd15abd9c106aabbf55bb2e4d3d20f2dcd5f2e5e8e12cf338cb48c113d81ea2265e7085bce643b88a0011bd73c7f4
|
7
|
+
data.tar.gz: a1c6d9161d0051153b313b5501d3043df99de23105748e336a3c2446edeb5acfb3708be638c95cb23811205840f2dd2799f571124f369dbfbe562df70259087d
|
data/.travis.yml
CHANGED
@@ -8,14 +8,12 @@ gemfile:
|
|
8
8
|
- test/gemfiles/activerecord50.gemfile
|
9
9
|
- test/gemfiles/activerecord42.gemfile
|
10
10
|
sudo: false
|
11
|
-
script:
|
11
|
+
script: bundle exec rake test
|
12
12
|
before_install:
|
13
13
|
- gem install bundler
|
14
14
|
- mysql -e 'create database groupdate_test;'
|
15
15
|
- mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
|
16
16
|
- psql -c 'create database groupdate_test;' -U postgres
|
17
|
-
env:
|
18
|
-
- TRAVIS=t
|
19
17
|
notifications:
|
20
18
|
email:
|
21
19
|
on_success: never
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 4.0.0
|
2
|
+
|
3
|
+
- Custom calculation methods are supported by default - `groupdate_calculation_methods` is no longer needed
|
4
|
+
|
5
|
+
Breaking changes
|
6
|
+
|
7
|
+
- Dropped support for Rails < 4.2
|
8
|
+
- Invalid options now throw an `ArgumentError`
|
9
|
+
- `group_by` methods return an `ActiveRecord::Relation` instead of a `Groupdate::Series`
|
10
|
+
- `week_start` now affects `day_of_week`
|
11
|
+
- Removed support for `reverse_order` (was never supported in Rails 5)
|
12
|
+
|
1
13
|
## 3.2.1
|
2
14
|
|
3
15
|
- Added `minute_of_hour`
|
data/CONTRIBUTING.md
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -208,37 +208,6 @@ Count
|
|
208
208
|
Hash[ users.group_by_day { |u| u.created_at }.map { |k, v| [k, v.size] } ]
|
209
209
|
```
|
210
210
|
|
211
|
-
## Custom Calculation Methods
|
212
|
-
|
213
|
-
Groupdate knows all of the calculations defined by `ActiveRecord` like `count`,
|
214
|
-
`sum`, or `average`. However you may have your own class level calculation
|
215
|
-
methods that you need to tell Groupdate about. All you have to do is define the
|
216
|
-
class method `groupdate_calculation_methods` returning an array of the method
|
217
|
-
names as symbols.
|
218
|
-
|
219
|
-
```ruby
|
220
|
-
class User < ApplicationRecord
|
221
|
-
def self.groupdate_calculation_methods
|
222
|
-
[:total_sign_ins]
|
223
|
-
end
|
224
|
-
|
225
|
-
def self.total_sign_ins
|
226
|
-
all.sum(:sign_ins)
|
227
|
-
end
|
228
|
-
end
|
229
|
-
```
|
230
|
-
|
231
|
-
Then you can use your custom calculation method:
|
232
|
-
|
233
|
-
```ruby
|
234
|
-
User.group_by_week(:created_at).total_sign_ins
|
235
|
-
```
|
236
|
-
|
237
|
-
Note that even if your method uses one of the calculations from `ActiveRecord`,
|
238
|
-
you'll still need to add it to the `groupdate_calculation_methods` array to have
|
239
|
-
it return the Hash of dates to values. Otherwise it will return a
|
240
|
-
`Groupdate::Series` object.
|
241
|
-
|
242
211
|
## Installation
|
243
212
|
|
244
213
|
Add this line to your application’s Gemfile:
|
@@ -281,6 +250,15 @@ Groupdate.time_zone = false
|
|
281
250
|
|
282
251
|
## Upgrading
|
283
252
|
|
253
|
+
### 4.0
|
254
|
+
|
255
|
+
Groupdate 4.0 brings a number of improvements. Here are a few to be aware of:
|
256
|
+
|
257
|
+
- `group_by` methods return an `ActiveRecord::Relation` instead of a `Groupdate::Series`
|
258
|
+
- Invalid options now throw an `ArgumentError`
|
259
|
+
- `week_start` now affects `day_of_week`
|
260
|
+
- Custom calculation methods are supported by default
|
261
|
+
|
284
262
|
### 3.0
|
285
263
|
|
286
264
|
Groupdate 3.0 brings a number of improvements. Here are a few to be aware of:
|
data/Rakefile
CHANGED
@@ -7,18 +7,3 @@ Rake::TestTask.new do |t|
|
|
7
7
|
t.test_files = FileList["test/**/*_test.rb"].exclude(/redshift/)
|
8
8
|
t.warning = false
|
9
9
|
end
|
10
|
-
|
11
|
-
namespace :test do
|
12
|
-
Rake::TestTask.new(:postgresql) do |t|
|
13
|
-
t.libs << "test"
|
14
|
-
t.pattern = "test/postgresql_test.rb"
|
15
|
-
end
|
16
|
-
Rake::TestTask.new(:mysql) do |t|
|
17
|
-
t.libs << "test"
|
18
|
-
t.pattern = "test/mysql_test.rb"
|
19
|
-
end
|
20
|
-
Rake::TestTask.new(:redshift) do |t|
|
21
|
-
t.libs << "test"
|
22
|
-
t.pattern = "test/redshift_test.rb"
|
23
|
-
end
|
24
|
-
end
|
data/groupdate.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_dependency "activesupport", ">=
|
20
|
+
spec.add_dependency "activesupport", ">= 4.2"
|
21
21
|
|
22
22
|
spec.add_development_dependency "bundler"
|
23
23
|
spec.add_development_dependency "rake"
|
data/lib/groupdate.rb
CHANGED
@@ -7,8 +7,6 @@ module Groupdate
|
|
7
7
|
class Error < RuntimeError; end
|
8
8
|
|
9
9
|
PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :minute_of_hour, :day_of_month, :month_of_year]
|
10
|
-
# backwards compatibility for anyone who happened to use it
|
11
|
-
FIELDS = PERIODS
|
12
10
|
METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
|
13
11
|
|
14
12
|
mattr_accessor :week_start, :day_start, :time_zone, :dates
|
@@ -1,53 +1,6 @@
|
|
1
1
|
require "active_record"
|
2
|
-
require "groupdate/
|
3
|
-
require "groupdate/
|
4
|
-
require "groupdate/calculations"
|
5
|
-
require "groupdate/series"
|
2
|
+
require "groupdate/query_methods"
|
3
|
+
require "groupdate/relation"
|
6
4
|
|
7
|
-
ActiveRecord::Base.
|
8
|
-
|
9
|
-
module ActiveRecord
|
10
|
-
class Relation
|
11
|
-
if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR < 2
|
12
|
-
|
13
|
-
def method_missing_with_hack(method, *args, &block)
|
14
|
-
if Groupdate::METHODS.include?(method)
|
15
|
-
scoping { @klass.send(method, *args, &block) }
|
16
|
-
else
|
17
|
-
method_missing_without_hack(method, *args, &block)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
alias_method_chain :method_missing, :hack
|
21
|
-
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
module ActiveRecord
|
27
|
-
module Associations
|
28
|
-
class CollectionProxy
|
29
|
-
if ActiveRecord::VERSION::MAJOR == 3
|
30
|
-
delegate(*Groupdate::METHODS, to: :scoped)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# hack for issue before Rails 5
|
37
|
-
# https://github.com/rails/rails/issues/7121
|
38
|
-
module ActiveRecord
|
39
|
-
module Calculations
|
40
|
-
private
|
41
|
-
|
42
|
-
if ActiveRecord::VERSION::MAJOR < 5
|
43
|
-
def column_alias_for_with_hack(*keys)
|
44
|
-
if keys.first.is_a?(Groupdate::OrderHack)
|
45
|
-
keys.first.field
|
46
|
-
else
|
47
|
-
column_alias_for_without_hack(*keys)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
alias_method_chain :column_alias_for, :hack
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
5
|
+
ActiveRecord::Base.extend(Groupdate::QueryMethods)
|
6
|
+
ActiveRecord::Relation.include(Groupdate::Relation)
|
data/lib/groupdate/enumerable.rb
CHANGED
@@ -2,7 +2,7 @@ module Enumerable
|
|
2
2
|
Groupdate::PERIODS.each do |period|
|
3
3
|
define_method :"group_by_#{period}" do |*args, &block|
|
4
4
|
if block
|
5
|
-
Groupdate::Magic.
|
5
|
+
Groupdate::Magic::Enumerable.group_by(self, period, args[0] || {}, &block)
|
6
6
|
elsif respond_to?(:scoping)
|
7
7
|
scoping { @klass.send(:"group_by_#{period}", *args, &block) }
|
8
8
|
else
|
@@ -16,8 +16,9 @@ module Enumerable
|
|
16
16
|
period = args[0]
|
17
17
|
options = args[1] || {}
|
18
18
|
|
19
|
+
options = options.dup
|
19
20
|
# to_sym is unsafe on user input, so convert to strings
|
20
|
-
permitted_periods = ((options
|
21
|
+
permitted_periods = ((options.delete(:permit) || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
|
21
22
|
if permitted_periods.include?(period.to_s)
|
22
23
|
send("group_by_#{period}", options, &block)
|
23
24
|
else
|
data/lib/groupdate/magic.rb
CHANGED
@@ -4,205 +4,17 @@ module Groupdate
|
|
4
4
|
class Magic
|
5
5
|
attr_accessor :period, :options
|
6
6
|
|
7
|
-
def initialize(period
|
7
|
+
def initialize(period:, **options)
|
8
8
|
@period = period
|
9
9
|
@options = options
|
10
10
|
|
11
|
-
|
11
|
+
unknown_keywords = options.keys - [:day_start, :time_zone, :dates, :series, :week_start, :format, :locale, :range, :reverse]
|
12
|
+
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
12
13
|
|
14
|
+
raise Groupdate::Error, "Unrecognized time zone" unless time_zone
|
13
15
|
raise Groupdate::Error, "Unrecognized :week_start option" if period == :week && !week_start
|
14
16
|
end
|
15
17
|
|
16
|
-
def group_by(enum, &_block)
|
17
|
-
group = enum.group_by { |v| v = yield(v); v ? round_time(v) : nil }
|
18
|
-
series(group, [], false, false, false)
|
19
|
-
end
|
20
|
-
|
21
|
-
def relation(column, relation)
|
22
|
-
if relation.default_timezone == :local
|
23
|
-
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
24
|
-
end
|
25
|
-
|
26
|
-
time_zone = self.time_zone.tzinfo.name
|
27
|
-
|
28
|
-
adapter_name = relation.connection.adapter_name
|
29
|
-
query =
|
30
|
-
case adapter_name
|
31
|
-
when "MySQL", "Mysql2", "Mysql2Spatial", 'Mysql2Rgeo'
|
32
|
-
case period
|
33
|
-
when :day_of_week # Sunday = 0, Monday = 1, etc
|
34
|
-
# use CONCAT for consistent return type (String)
|
35
|
-
["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1", time_zone]
|
36
|
-
when :hour_of_day
|
37
|
-
["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
|
38
|
-
when :minute_of_hour
|
39
|
-
["(EXTRACT(MINUTE from CONVERT_TZ(#{column}, '+00:00', ?)))", time_zone]
|
40
|
-
when :day_of_month
|
41
|
-
["DAYOFMONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
42
|
-
when :month_of_year
|
43
|
-
["MONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
44
|
-
when :week
|
45
|
-
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} second)) % 7) DAY) - INTERVAL #{day_start} second, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} second, ?, '+00:00')", time_zone, time_zone, time_zone]
|
46
|
-
when :quarter
|
47
|
-
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(EXTRACT(YEAR FROM CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)), '-', LPAD(1 + 3 * (QUARTER(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone, time_zone]
|
48
|
-
else
|
49
|
-
format =
|
50
|
-
case period
|
51
|
-
when :second
|
52
|
-
"%Y-%m-%d %H:%i:%S"
|
53
|
-
when :minute
|
54
|
-
"%Y-%m-%d %H:%i:00"
|
55
|
-
when :hour
|
56
|
-
"%Y-%m-%d %H:00:00"
|
57
|
-
when :day
|
58
|
-
"%Y-%m-%d 00:00:00"
|
59
|
-
when :month
|
60
|
-
"%Y-%m-01 00:00:00"
|
61
|
-
else # year
|
62
|
-
"%Y-01-01 00:00:00"
|
63
|
-
end
|
64
|
-
|
65
|
-
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone]
|
66
|
-
end
|
67
|
-
when "PostgreSQL", "PostGIS"
|
68
|
-
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 :hour_of_day
|
72
|
-
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
73
|
-
when :minute_of_hour
|
74
|
-
["EXTRACT(MINUTE from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
75
|
-
when :day_of_month
|
76
|
-
["EXTRACT(DAY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
77
|
-
when :month_of_year
|
78
|
-
["EXTRACT(MONTH from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
79
|
-
when :week # start on Sunday, not PostgreSQL default Monday
|
80
|
-
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
81
|
-
else
|
82
|
-
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
83
|
-
end
|
84
|
-
when "SQLite"
|
85
|
-
raise Groupdate::Error, "Time zones not supported for SQLite" unless self.time_zone.utc_offset.zero?
|
86
|
-
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
87
|
-
raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
|
88
|
-
|
89
|
-
if period == :week
|
90
|
-
["strftime('%%Y-%%m-%%d 00:00:00 UTC', #{column}, '-6 days', 'weekday 0')"]
|
91
|
-
else
|
92
|
-
format =
|
93
|
-
case period
|
94
|
-
when :hour_of_day
|
95
|
-
"%H"
|
96
|
-
when :minute_of_hour
|
97
|
-
"%M"
|
98
|
-
when :day_of_week
|
99
|
-
"%w"
|
100
|
-
when :day_of_month
|
101
|
-
"%d"
|
102
|
-
when :month_of_year
|
103
|
-
"%m"
|
104
|
-
when :second
|
105
|
-
"%Y-%m-%d %H:%M:%S UTC"
|
106
|
-
when :minute
|
107
|
-
"%Y-%m-%d %H:%M:00 UTC"
|
108
|
-
when :hour
|
109
|
-
"%Y-%m-%d %H:00:00 UTC"
|
110
|
-
when :day
|
111
|
-
"%Y-%m-%d 00:00:00 UTC"
|
112
|
-
when :month
|
113
|
-
"%Y-%m-01 00:00:00 UTC"
|
114
|
-
when :quarter
|
115
|
-
raise Groupdate::Error, "Quarter not supported for SQLite"
|
116
|
-
else # year
|
117
|
-
"%Y-01-01 00:00:00 UTC"
|
118
|
-
end
|
119
|
-
|
120
|
-
["strftime('#{format.gsub(/%/, '%%')}', #{column})"]
|
121
|
-
end
|
122
|
-
when "Redshift"
|
123
|
-
case period
|
124
|
-
when :day_of_week # Sunday = 0, Monday = 1, etc.
|
125
|
-
["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
126
|
-
when :hour_of_day
|
127
|
-
["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
128
|
-
when :minute_of_hour
|
129
|
-
["EXTRACT(MINUTE from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
130
|
-
when :day_of_month
|
131
|
-
["EXTRACT(DAY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
132
|
-
when :month_of_year
|
133
|
-
["EXTRACT(MONTH from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
134
|
-
when :week # start on Sunday, not Redshift default Monday
|
135
|
-
# Redshift does not return timezone information; it
|
136
|
-
# always says it is in UTC time, so we must convert
|
137
|
-
# back to UTC to play properly with the rest of Groupdate.
|
138
|
-
#
|
139
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
140
|
-
else
|
141
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
142
|
-
end
|
143
|
-
else
|
144
|
-
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
145
|
-
end
|
146
|
-
|
147
|
-
if adapter_name == "MySQL" && period == :week
|
148
|
-
query[0] = "CAST(#{query[0]} AS DATETIME)"
|
149
|
-
end
|
150
|
-
|
151
|
-
group = relation.group(Groupdate::OrderHack.new(relation.send(:sanitize_sql_array, query), period, time_zone))
|
152
|
-
relation =
|
153
|
-
if time_range.is_a?(Range)
|
154
|
-
# doesn't matter whether we include the end of a ... range - it will be excluded later
|
155
|
-
group.where("#{column} >= ? AND #{column} <= ?", time_range.first, time_range.last)
|
156
|
-
else
|
157
|
-
group.where("#{column} IS NOT NULL")
|
158
|
-
end
|
159
|
-
|
160
|
-
# TODO do not change object state
|
161
|
-
@group_index = group.group_values.size - 1
|
162
|
-
|
163
|
-
Groupdate::Series.new(self, relation)
|
164
|
-
end
|
165
|
-
|
166
|
-
def perform(relation, method, *args, &block)
|
167
|
-
# undo reverse since we do not want this to appear in the query
|
168
|
-
reverse = relation.send(:reverse_order_value)
|
169
|
-
relation = relation.except(:reverse_order) if reverse
|
170
|
-
order = relation.order_values.first
|
171
|
-
if order.is_a?(String)
|
172
|
-
parts = order.split(" ")
|
173
|
-
reverse_order = (parts.size == 2 && (parts[0].to_sym == period || (activerecord42? && parts[0] == "#{relation.quoted_table_name}.#{relation.quoted_primary_key}")) && parts[1].to_s.downcase == "desc")
|
174
|
-
if reverse_order
|
175
|
-
reverse = !reverse
|
176
|
-
relation = relation.reorder(relation.order_values[1..-1])
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
multiple_groups = relation.group_values.size > 1
|
181
|
-
|
182
|
-
cast_method =
|
183
|
-
case period
|
184
|
-
when :day_of_week, :hour_of_day, :day_of_month, :month_of_year, :minute_of_hour
|
185
|
-
lambda { |k| k.to_i }
|
186
|
-
else
|
187
|
-
utc = ActiveSupport::TimeZone["UTC"]
|
188
|
-
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
189
|
-
end
|
190
|
-
|
191
|
-
result = relation.send(method, *args, &block)
|
192
|
-
if result.is_a?(Hash)
|
193
|
-
missing_time_zone_support = multiple_groups ? (result.keys.first && result.keys.first[@group_index].nil?) : result.key?(nil)
|
194
|
-
if missing_time_zone_support
|
195
|
-
raise Groupdate::Error, "Be sure to install time zone support - https://github.com/ankane/groupdate#for-mysql"
|
196
|
-
end
|
197
|
-
result = 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] }]
|
198
|
-
|
199
|
-
series(result, (options.key?(:default_value) ? options[:default_value] : 0), multiple_groups, reverse)
|
200
|
-
else
|
201
|
-
# for ActiveRecord::Calculations methods that don't call calculate, like pluck
|
202
|
-
result
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
18
|
protected
|
207
19
|
|
208
20
|
def time_zone
|
@@ -343,7 +155,7 @@ module Groupdate
|
|
343
155
|
when :minute_of_hour
|
344
156
|
key = sunday + key.minutes + day_start.seconds
|
345
157
|
when :day_of_week
|
346
|
-
key = sunday + key.days
|
158
|
+
key = sunday + key.days + (week_start + 1).days
|
347
159
|
when :day_of_month
|
348
160
|
key = Date.new(2014, 1, key).to_time
|
349
161
|
when :month_of_year
|
@@ -398,7 +210,7 @@ module Groupdate
|
|
398
210
|
when :minute_of_hour
|
399
211
|
time.min
|
400
212
|
when :day_of_week
|
401
|
-
time.wday
|
213
|
+
(time.wday - 1 - week_start) % 7
|
402
214
|
when :day_of_month
|
403
215
|
time.day
|
404
216
|
when :month_of_year
|
@@ -410,8 +222,219 @@ module Groupdate
|
|
410
222
|
time.is_a?(Time) ? time + day_start.seconds : time
|
411
223
|
end
|
412
224
|
|
413
|
-
|
414
|
-
|
225
|
+
class Enumerable < Magic
|
226
|
+
def group_by(enum, &_block)
|
227
|
+
group = enum.group_by { |v| v = yield(v); v ? round_time(v) : nil }
|
228
|
+
series(group, [], false, false, false)
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.group_by(enum, period, options, &block)
|
232
|
+
Groupdate::Magic::Enumerable.new(period: period, **options).group_by(enum, &block)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class Relation < Magic
|
237
|
+
def initialize(**options)
|
238
|
+
super(**options.reject { |k, _| [:default_value, :carry_forward, :last, :current].include?(k) })
|
239
|
+
@options = options
|
240
|
+
end
|
241
|
+
|
242
|
+
def relation(column, relation)
|
243
|
+
if relation.default_timezone == :local
|
244
|
+
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
245
|
+
end
|
246
|
+
|
247
|
+
time_zone = self.time_zone.tzinfo.name
|
248
|
+
|
249
|
+
adapter_name = relation.connection.adapter_name
|
250
|
+
query =
|
251
|
+
case adapter_name
|
252
|
+
when "MySQL", "Mysql2", "Mysql2Spatial", 'Mysql2Rgeo'
|
253
|
+
case period
|
254
|
+
when :day_of_week
|
255
|
+
["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1", time_zone]
|
256
|
+
when :hour_of_day
|
257
|
+
["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
|
258
|
+
when :minute_of_hour
|
259
|
+
["(EXTRACT(MINUTE from CONVERT_TZ(#{column}, '+00:00', ?)))", time_zone]
|
260
|
+
when :day_of_month
|
261
|
+
["DAYOFMONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
262
|
+
when :month_of_year
|
263
|
+
["MONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
264
|
+
when :week
|
265
|
+
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} second)) % 7) DAY) - INTERVAL #{day_start} second, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} second, ?, '+00:00')", time_zone, time_zone, time_zone]
|
266
|
+
when :quarter
|
267
|
+
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(EXTRACT(YEAR FROM CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)), '-', LPAD(1 + 3 * (QUARTER(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone, time_zone]
|
268
|
+
else
|
269
|
+
format =
|
270
|
+
case period
|
271
|
+
when :second
|
272
|
+
"%Y-%m-%d %H:%i:%S"
|
273
|
+
when :minute
|
274
|
+
"%Y-%m-%d %H:%i:00"
|
275
|
+
when :hour
|
276
|
+
"%Y-%m-%d %H:00:00"
|
277
|
+
when :day
|
278
|
+
"%Y-%m-%d 00:00:00"
|
279
|
+
when :month
|
280
|
+
"%Y-%m-01 00:00:00"
|
281
|
+
else # year
|
282
|
+
"%Y-01-01 00:00:00"
|
283
|
+
end
|
284
|
+
|
285
|
+
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone]
|
286
|
+
end
|
287
|
+
when "PostgreSQL", "PostGIS"
|
288
|
+
case period
|
289
|
+
when :day_of_week
|
290
|
+
["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
291
|
+
when :hour_of_day
|
292
|
+
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
293
|
+
when :minute_of_hour
|
294
|
+
["EXTRACT(MINUTE from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
295
|
+
when :day_of_month
|
296
|
+
["EXTRACT(DAY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
297
|
+
when :month_of_year
|
298
|
+
["EXTRACT(MONTH from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
299
|
+
when :week # start on Sunday, not PostgreSQL default Monday
|
300
|
+
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
301
|
+
else
|
302
|
+
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
303
|
+
end
|
304
|
+
when "SQLite"
|
305
|
+
raise Groupdate::Error, "Time zones not supported for SQLite" unless self.time_zone.utc_offset.zero?
|
306
|
+
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
307
|
+
raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
|
308
|
+
|
309
|
+
if period == :week
|
310
|
+
["strftime('%%Y-%%m-%%d 00:00:00 UTC', #{column}, '-6 days', 'weekday 0')"]
|
311
|
+
else
|
312
|
+
format =
|
313
|
+
case period
|
314
|
+
when :hour_of_day
|
315
|
+
"%H"
|
316
|
+
when :minute_of_hour
|
317
|
+
"%M"
|
318
|
+
when :day_of_week
|
319
|
+
"%w"
|
320
|
+
when :day_of_month
|
321
|
+
"%d"
|
322
|
+
when :month_of_year
|
323
|
+
"%m"
|
324
|
+
when :second
|
325
|
+
"%Y-%m-%d %H:%M:%S UTC"
|
326
|
+
when :minute
|
327
|
+
"%Y-%m-%d %H:%M:00 UTC"
|
328
|
+
when :hour
|
329
|
+
"%Y-%m-%d %H:00:00 UTC"
|
330
|
+
when :day
|
331
|
+
"%Y-%m-%d 00:00:00 UTC"
|
332
|
+
when :month
|
333
|
+
"%Y-%m-01 00:00:00 UTC"
|
334
|
+
when :quarter
|
335
|
+
raise Groupdate::Error, "Quarter not supported for SQLite"
|
336
|
+
else # year
|
337
|
+
"%Y-01-01 00:00:00 UTC"
|
338
|
+
end
|
339
|
+
|
340
|
+
["strftime('#{format.gsub(/%/, '%%')}', #{column})"]
|
341
|
+
end
|
342
|
+
when "Redshift"
|
343
|
+
case period
|
344
|
+
when :day_of_week
|
345
|
+
["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
346
|
+
when :hour_of_day
|
347
|
+
["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
348
|
+
when :minute_of_hour
|
349
|
+
["EXTRACT(MINUTE from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
350
|
+
when :day_of_month
|
351
|
+
["EXTRACT(DAY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
352
|
+
when :month_of_year
|
353
|
+
["EXTRACT(MONTH from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
354
|
+
when :week # start on Sunday, not Redshift default Monday
|
355
|
+
# Redshift does not return timezone information; it
|
356
|
+
# always says it is in UTC time, so we must convert
|
357
|
+
# back to UTC to play properly with the rest of Groupdate.
|
358
|
+
#
|
359
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
360
|
+
else
|
361
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
362
|
+
end
|
363
|
+
else
|
364
|
+
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
365
|
+
end
|
366
|
+
|
367
|
+
if adapter_name == "MySQL" && period == :week
|
368
|
+
query[0] = "CAST(#{query[0]} AS DATETIME)"
|
369
|
+
end
|
370
|
+
|
371
|
+
group_str = relation.send(:sanitize_sql_array, query)
|
372
|
+
|
373
|
+
# cleaner queries in logs
|
374
|
+
# Postgres
|
375
|
+
group_str = group_str.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
|
376
|
+
# MySQL
|
377
|
+
group_str = group_str.gsub("DATE_SUB(#{column}, INTERVAL 0 second)", "#{column}")
|
378
|
+
if group_str.start_with?("DATE_ADD(") && group_str.end_with?(", INTERVAL 0 second)")
|
379
|
+
group_str = group_str[9..-21]
|
380
|
+
end
|
381
|
+
|
382
|
+
group = relation.group(group_str)
|
383
|
+
relation =
|
384
|
+
if time_range.is_a?(Range)
|
385
|
+
# doesn't matter whether we include the end of a ... range - it will be excluded later
|
386
|
+
group.where("#{column} >= ? AND #{column} <= ?", time_range.first, time_range.last)
|
387
|
+
else
|
388
|
+
group.where("#{column} IS NOT NULL")
|
389
|
+
end
|
390
|
+
|
391
|
+
# TODO do not change object state
|
392
|
+
@group_index = group.group_values.size - 1
|
393
|
+
|
394
|
+
relation
|
395
|
+
end
|
396
|
+
|
397
|
+
def perform(relation, result)
|
398
|
+
multiple_groups = relation.group_values.size > 1
|
399
|
+
|
400
|
+
cast_method =
|
401
|
+
case period
|
402
|
+
when :day_of_week
|
403
|
+
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
404
|
+
when :hour_of_day, :day_of_month, :month_of_year, :minute_of_hour
|
405
|
+
lambda { |k| k.to_i }
|
406
|
+
else
|
407
|
+
utc = ActiveSupport::TimeZone["UTC"]
|
408
|
+
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
409
|
+
end
|
410
|
+
|
411
|
+
missing_time_zone_support = multiple_groups ? (result.keys.first && result.keys.first[@group_index].nil?) : result.key?(nil)
|
412
|
+
if missing_time_zone_support
|
413
|
+
raise Groupdate::Error, "Be sure to install time zone support - https://github.com/ankane/groupdate#for-mysql"
|
414
|
+
end
|
415
|
+
result = 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] }]
|
416
|
+
|
417
|
+
series(result, (options.key?(:default_value) ? options[:default_value] : 0), multiple_groups, @reverse)
|
418
|
+
end
|
419
|
+
|
420
|
+
def self.generate_relation(relation, field:, **options)
|
421
|
+
magic = Groupdate::Magic::Relation.new(**options)
|
422
|
+
|
423
|
+
# generate ActiveRecord relation
|
424
|
+
relation = magic.relation(field, relation)
|
425
|
+
|
426
|
+
# add Groupdate info
|
427
|
+
(relation.groupdate_values ||= []) << magic
|
428
|
+
|
429
|
+
relation
|
430
|
+
end
|
431
|
+
|
432
|
+
def self.process_result(relation, result)
|
433
|
+
relation.groupdate_values.reverse.each do |gv|
|
434
|
+
result = gv.perform(relation, result)
|
435
|
+
end
|
436
|
+
result
|
437
|
+
end
|
415
438
|
end
|
416
439
|
end
|
417
440
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Groupdate
|
2
|
+
module QueryMethods
|
3
|
+
Groupdate::PERIODS.each do |period|
|
4
|
+
define_method :"group_by_#{period}" do |field, time_zone = nil, range = nil, **options|
|
5
|
+
Groupdate::Magic::Relation.generate_relation(self,
|
6
|
+
period: period,
|
7
|
+
field: field,
|
8
|
+
time_zone: time_zone,
|
9
|
+
range: range,
|
10
|
+
**options
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def group_by_period(period, field, permit: nil, **options)
|
16
|
+
# to_sym is unsafe on user input, so convert to strings
|
17
|
+
permitted_periods = ((permit || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
|
18
|
+
if permitted_periods.include?(period.to_s)
|
19
|
+
send("group_by_#{period}", field, **options)
|
20
|
+
else
|
21
|
+
raise ArgumentError, "Unpermitted period"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Groupdate
|
4
|
+
module Relation
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_accessor :groupdate_values
|
9
|
+
end
|
10
|
+
|
11
|
+
def calculate(*args, &block)
|
12
|
+
if groupdate_values
|
13
|
+
Groupdate::Magic::Relation.process_result(self, super)
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/groupdate/version.rb
CHANGED
data/test/sqlite_test.rb
CHANGED
@@ -17,11 +17,6 @@ class TestSqlite < Minitest::Test
|
|
17
17
|
skip
|
18
18
|
end
|
19
19
|
|
20
|
-
def test_zeros_datetime
|
21
|
-
skip if ENV["TRAVIS"]
|
22
|
-
super
|
23
|
-
end
|
24
|
-
|
25
20
|
def call_method(method, field, options)
|
26
21
|
if method == :quarter || options[:time_zone] || options[:day_start] || options[:week_start] || Groupdate.week_start != :sun || (Time.zone && options[:time_zone] != false)
|
27
22
|
error = assert_raises(Groupdate::Error) { super }
|
data/test/test_helper.rb
CHANGED
@@ -10,7 +10,7 @@ Minitest::Test = Minitest::Unit::TestCase unless defined?(Minitest::Test)
|
|
10
10
|
ENV["TZ"] = "UTC"
|
11
11
|
|
12
12
|
# for debugging
|
13
|
-
|
13
|
+
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"]
|
14
14
|
|
15
15
|
# rails does this in activerecord/lib/active_record/railtie.rb
|
16
16
|
ActiveRecord::Base.default_timezone = :utc
|
@@ -19,17 +19,9 @@ ActiveRecord::Base.time_zone_aware_attributes = true
|
|
19
19
|
class User < ActiveRecord::Base
|
20
20
|
has_many :posts
|
21
21
|
|
22
|
-
def self.groupdate_calculation_methods
|
23
|
-
[:custom_count, :undefined_calculation]
|
24
|
-
end
|
25
|
-
|
26
22
|
def self.custom_count
|
27
23
|
count
|
28
24
|
end
|
29
|
-
|
30
|
-
def self.unlisted_calculation
|
31
|
-
count
|
32
|
-
end
|
33
25
|
end
|
34
26
|
|
35
27
|
class Post < ActiveRecord::Base
|
@@ -86,24 +78,6 @@ module TestDatabase
|
|
86
78
|
assert_equal expected, User.where("id = 0").group_by_day(:created_at, range: Date.parse("2013-05-01")..Date.parse("2013-05-01 23:59:59 UTC")).count
|
87
79
|
end
|
88
80
|
|
89
|
-
def test_order_hour_of_day
|
90
|
-
assert_equal 23, User.group_by_hour_of_day(:created_at).order("hour_of_day desc").count.keys.first
|
91
|
-
end
|
92
|
-
|
93
|
-
def test_order_hour_of_day_case
|
94
|
-
assert_equal 23, User.group_by_hour_of_day(:created_at).order("hour_of_day DESC").count.keys.first
|
95
|
-
end
|
96
|
-
|
97
|
-
def test_order_hour_of_day_reverse
|
98
|
-
skip if ActiveRecord::VERSION::MAJOR == 5
|
99
|
-
assert_equal 23, User.group_by_hour_of_day(:created_at).reverse_order.count.keys.first
|
100
|
-
end
|
101
|
-
|
102
|
-
def test_order_hour_of_day_order_reverse
|
103
|
-
skip if ActiveRecord::VERSION::MAJOR == 5
|
104
|
-
assert_equal 0, User.group_by_hour_of_day(:created_at).order("hour_of_day desc").reverse_order.count.keys.first
|
105
|
-
end
|
106
|
-
|
107
81
|
def test_table_name
|
108
82
|
# This test is to ensure there's not an error when using the table
|
109
83
|
# name as part of the column name.
|
@@ -403,10 +377,6 @@ module TestDatabase
|
|
403
377
|
assert_equal expected, User.group_by_day(:created_at).custom_count
|
404
378
|
end
|
405
379
|
|
406
|
-
def test_using_unlisted_calculation_method_returns_new_series_instance
|
407
|
-
assert_instance_of Groupdate::Series, User.group_by_day(:created_at).unlisted_calculation
|
408
|
-
end
|
409
|
-
|
410
380
|
def test_using_listed_but_undefined_custom_calculation_method_raises_error
|
411
381
|
assert_raises(NoMethodError) do
|
412
382
|
User.group_by_day(:created_at).undefined_calculation
|
@@ -426,6 +396,12 @@ module TestDatabase
|
|
426
396
|
assert_equal [0], User.group_by_hour_of_day(:created_at).pluck(0)
|
427
397
|
end
|
428
398
|
|
399
|
+
# test relation
|
400
|
+
|
401
|
+
def test_relation
|
402
|
+
assert User.group_by_day(:created_at).is_a?(ActiveRecord::Relation)
|
403
|
+
end
|
404
|
+
|
429
405
|
private
|
430
406
|
|
431
407
|
def call_method(method, field, options)
|
@@ -799,6 +775,24 @@ module TestGroupdate
|
|
799
775
|
assert_result :day_of_week, 3, "2013-01-02 10:00:00", true, day_start: 2
|
800
776
|
end
|
801
777
|
|
778
|
+
# day of week week start monday
|
779
|
+
|
780
|
+
def test_day_of_week_end_of_day_week_start_mon
|
781
|
+
assert_result :day_of_week, 1, "2013-01-01 23:59:59", false, week_start: :mon
|
782
|
+
end
|
783
|
+
|
784
|
+
def test_day_of_week_start_of_day_week_start_mon
|
785
|
+
assert_result :day_of_week, 2, "2013-01-02 00:00:00", false, week_start: :mon
|
786
|
+
end
|
787
|
+
|
788
|
+
def test_day_of_week_end_of_week_with_time_zone_week_start_mon
|
789
|
+
assert_result :day_of_week, 1, "2013-01-02 07:59:59", true, week_start: :mon
|
790
|
+
end
|
791
|
+
|
792
|
+
def test_day_of_week_start_of_week_with_time_zone_week_start_mon
|
793
|
+
assert_result :day_of_week, 2, "2013-01-02 08:00:00", true, week_start: :mon
|
794
|
+
end
|
795
|
+
|
802
796
|
# day of month
|
803
797
|
|
804
798
|
def test_day_of_month_end_of_day
|
@@ -1101,6 +1095,10 @@ module TestGroupdate
|
|
1101
1095
|
assert_format :day_of_week, "Sat", "%a", week_start: :mon
|
1102
1096
|
end
|
1103
1097
|
|
1098
|
+
def test_format_day_of_week_week_start
|
1099
|
+
assert_equal "Mon", call_method(:day_of_week, :created_at, week_start: :mon, format: "%a", series: true).keys.first
|
1100
|
+
end
|
1101
|
+
|
1104
1102
|
def test_format_day_of_month
|
1105
1103
|
create_user "2014-03-01"
|
1106
1104
|
assert_format :day_of_month, " 1", "%e"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: groupdate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '4.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: '
|
26
|
+
version: '4.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -141,18 +141,12 @@ files:
|
|
141
141
|
- groupdate.gemspec
|
142
142
|
- lib/groupdate.rb
|
143
143
|
- lib/groupdate/active_record.rb
|
144
|
-
- lib/groupdate/calculations.rb
|
145
144
|
- lib/groupdate/enumerable.rb
|
146
145
|
- lib/groupdate/magic.rb
|
147
|
-
- lib/groupdate/
|
148
|
-
- lib/groupdate/
|
149
|
-
- lib/groupdate/series.rb
|
146
|
+
- lib/groupdate/query_methods.rb
|
147
|
+
- lib/groupdate/relation.rb
|
150
148
|
- lib/groupdate/version.rb
|
151
149
|
- test/enumerable_test.rb
|
152
|
-
- test/gemfiles/activerecord31.gemfile
|
153
|
-
- test/gemfiles/activerecord32.gemfile
|
154
|
-
- test/gemfiles/activerecord40.gemfile
|
155
|
-
- test/gemfiles/activerecord41.gemfile
|
156
150
|
- test/gemfiles/activerecord42.gemfile
|
157
151
|
- test/gemfiles/activerecord50.gemfile
|
158
152
|
- test/gemfiles/activerecord52.gemfile
|
@@ -188,10 +182,6 @@ specification_version: 4
|
|
188
182
|
summary: The simplest way to group temporal data
|
189
183
|
test_files:
|
190
184
|
- test/enumerable_test.rb
|
191
|
-
- test/gemfiles/activerecord31.gemfile
|
192
|
-
- test/gemfiles/activerecord32.gemfile
|
193
|
-
- test/gemfiles/activerecord40.gemfile
|
194
|
-
- test/gemfiles/activerecord41.gemfile
|
195
185
|
- test/gemfiles/activerecord42.gemfile
|
196
186
|
- test/gemfiles/activerecord50.gemfile
|
197
187
|
- test/gemfiles/activerecord52.gemfile
|
@@ -1,26 +0,0 @@
|
|
1
|
-
module Groupdate
|
2
|
-
class Calculations
|
3
|
-
attr_reader :relation
|
4
|
-
|
5
|
-
def initialize(relation)
|
6
|
-
@relation = relation
|
7
|
-
end
|
8
|
-
|
9
|
-
def include?(method)
|
10
|
-
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb
|
11
|
-
ActiveRecord::Calculations.method_defined?(method) || custom_calculations.include?(method)
|
12
|
-
end
|
13
|
-
|
14
|
-
def custom_calculations
|
15
|
-
return [] if !model.respond_to?(:groupdate_calculation_methods)
|
16
|
-
model.groupdate_calculation_methods
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def model
|
22
|
-
return if !relation.respond_to?(:klass)
|
23
|
-
relation.klass
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
data/lib/groupdate/order_hack.rb
DELETED
data/lib/groupdate/scopes.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
module Groupdate
|
2
|
-
module Scopes
|
3
|
-
Groupdate::PERIODS.each do |period|
|
4
|
-
define_method :"group_by_#{period}" do |field, *args|
|
5
|
-
args = args.dup
|
6
|
-
options = args[-1].is_a?(Hash) ? args.pop : {}
|
7
|
-
options[:time_zone] ||= args[0] unless args[0].nil?
|
8
|
-
options[:range] ||= args[1] unless args[1].nil?
|
9
|
-
|
10
|
-
Groupdate::Magic.new(period, options).relation(field, self)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
def group_by_period(period, field, options = {})
|
15
|
-
# to_sym is unsafe on user input, so convert to strings
|
16
|
-
permitted_periods = ((options[:permit] || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
|
17
|
-
if permitted_periods.include?(period.to_s)
|
18
|
-
send("group_by_#{period}", field, options)
|
19
|
-
else
|
20
|
-
raise ArgumentError, "Unpermitted period"
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
data/lib/groupdate/series.rb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
module Groupdate
|
2
|
-
class Series
|
3
|
-
attr_accessor :magic, :relation
|
4
|
-
|
5
|
-
def initialize(magic, relation)
|
6
|
-
@magic = magic
|
7
|
-
@relation = relation
|
8
|
-
@calculations = Groupdate::Calculations.new(relation)
|
9
|
-
end
|
10
|
-
|
11
|
-
# clone to prevent modifying original variables
|
12
|
-
def method_missing(method, *args, &block)
|
13
|
-
if @calculations.include?(method)
|
14
|
-
magic.perform(relation, method, *args, &block)
|
15
|
-
elsif relation.respond_to?(method, true)
|
16
|
-
Groupdate::Series.new(magic, relation.send(method, *args, &block))
|
17
|
-
else
|
18
|
-
super
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def respond_to?(method, include_all = false)
|
23
|
-
@calculations.include?(method) || relation.respond_to?(method) || super
|
24
|
-
end
|
25
|
-
|
26
|
-
def reverse_order_value
|
27
|
-
nil
|
28
|
-
end
|
29
|
-
|
30
|
-
def unscoped
|
31
|
-
@relation.unscoped
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|