groupdate 3.2.1 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|