groupdate 3.2.0 → 6.2.1
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 +5 -5
- data/CHANGELOG.md +146 -27
- data/CONTRIBUTING.md +75 -0
- data/LICENSE.txt +1 -1
- data/README.md +67 -44
- data/lib/groupdate/active_record.rb +4 -51
- data/lib/groupdate/adapters/base_adapter.rb +51 -0
- data/lib/groupdate/adapters/mysql_adapter.rb +63 -0
- data/lib/groupdate/adapters/postgresql_adapter.rb +46 -0
- data/lib/groupdate/adapters/sqlite_adapter.rb +51 -0
- data/lib/groupdate/enumerable.rb +9 -14
- data/lib/groupdate/magic.rb +202 -333
- data/lib/groupdate/query_methods.rb +18 -0
- data/lib/groupdate/relation.rb +19 -0
- data/lib/groupdate/series_builder.rb +304 -0
- data/lib/groupdate/version.rb +1 -1
- data/lib/groupdate.rb +37 -7
- metadata +20 -145
- data/.gitignore +0 -19
- data/.travis.yml +0 -25
- data/Gemfile +0 -6
- data/Rakefile +0 -24
- data/groupdate.gemspec +0 -37
- 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 -30
- data/test/enumerable_test.rb +0 -60
- 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
- data/test/gemfiles/activerecord42.gemfile +0 -6
- data/test/gemfiles/redshift.gemfile +0 -7
- data/test/mysql_test.rb +0 -15
- data/test/postgresql_test.rb +0 -15
- data/test/redshift_test.rb +0 -18
- data/test/sqlite_test.rb +0 -29
- data/test/test_helper.rb +0 -1207
@@ -0,0 +1,51 @@
|
|
1
|
+
module Groupdate
|
2
|
+
module Adapters
|
3
|
+
class BaseAdapter
|
4
|
+
attr_reader :period, :column, :day_start, :week_start, :n_seconds
|
5
|
+
|
6
|
+
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:, n_seconds:)
|
7
|
+
@relation = relation
|
8
|
+
@column = column
|
9
|
+
@period = period
|
10
|
+
@time_zone = time_zone
|
11
|
+
@time_range = time_range
|
12
|
+
@week_start = week_start
|
13
|
+
@day_start = day_start
|
14
|
+
@n_seconds = n_seconds
|
15
|
+
|
16
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
17
|
+
if ActiveRecord.default_timezone == :local
|
18
|
+
raise Groupdate::Error, "ActiveRecord.default_timezone must be :utc to use Groupdate"
|
19
|
+
end
|
20
|
+
else
|
21
|
+
if relation.default_timezone == :local
|
22
|
+
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate
|
28
|
+
@relation.group(group_clause).where(*where_clause)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def where_clause
|
34
|
+
if @time_range.is_a?(Range)
|
35
|
+
if @time_range.end
|
36
|
+
op = @time_range.exclude_end? ? "<" : "<="
|
37
|
+
if @time_range.begin
|
38
|
+
["#{column} >= ? AND #{column} #{op} ?", @time_range.begin, @time_range.end]
|
39
|
+
else
|
40
|
+
["#{column} #{op} ?", @time_range.end]
|
41
|
+
end
|
42
|
+
else
|
43
|
+
["#{column} >= ?", @time_range.begin]
|
44
|
+
end
|
45
|
+
else
|
46
|
+
["#{column} IS NOT NULL"]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Groupdate
|
2
|
+
module Adapters
|
3
|
+
class MySQLAdapter < BaseAdapter
|
4
|
+
def group_clause
|
5
|
+
time_zone = @time_zone.tzinfo.name
|
6
|
+
day_start_column = "CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL ? second"
|
7
|
+
|
8
|
+
query =
|
9
|
+
case period
|
10
|
+
when :minute_of_hour
|
11
|
+
["MINUTE(#{day_start_column})", time_zone, day_start]
|
12
|
+
when :hour_of_day
|
13
|
+
["HOUR(#{day_start_column})", time_zone, day_start]
|
14
|
+
when :day_of_week
|
15
|
+
["DAYOFWEEK(#{day_start_column}) - 1", time_zone, day_start]
|
16
|
+
when :day_of_month
|
17
|
+
["DAYOFMONTH(#{day_start_column})", time_zone, day_start]
|
18
|
+
when :day_of_year
|
19
|
+
["DAYOFYEAR(#{day_start_column})", time_zone, day_start]
|
20
|
+
when :month_of_year
|
21
|
+
["MONTH(#{day_start_column})", time_zone, day_start]
|
22
|
+
when :week
|
23
|
+
["CAST(DATE_FORMAT(#{day_start_column} - INTERVAL ((? + DAYOFWEEK(#{day_start_column})) % 7) DAY, '%Y-%m-%d') AS DATE)", time_zone, day_start, 12 - week_start, time_zone, day_start]
|
24
|
+
when :quarter
|
25
|
+
["CAST(CONCAT(YEAR(#{day_start_column}), '-', LPAD(1 + 3 * (QUARTER(#{day_start_column}) - 1), 2, '00'), '-01') AS DATE)", time_zone, day_start, time_zone, day_start]
|
26
|
+
when :day, :month, :year
|
27
|
+
format =
|
28
|
+
case period
|
29
|
+
when :day
|
30
|
+
"%Y-%m-%d"
|
31
|
+
when :month
|
32
|
+
"%Y-%m-01"
|
33
|
+
else # year
|
34
|
+
"%Y-01-01"
|
35
|
+
end
|
36
|
+
|
37
|
+
["CAST(DATE_FORMAT(#{day_start_column}, ?) AS DATE)", time_zone, day_start, format]
|
38
|
+
when :custom
|
39
|
+
["FROM_UNIXTIME((UNIX_TIMESTAMP(#{column}) DIV ?) * ?)", n_seconds, n_seconds]
|
40
|
+
else
|
41
|
+
format =
|
42
|
+
case period
|
43
|
+
when :second
|
44
|
+
"%Y-%m-%d %H:%i:%S"
|
45
|
+
when :minute
|
46
|
+
"%Y-%m-%d %H:%i:00"
|
47
|
+
else # hour
|
48
|
+
"%Y-%m-%d %H:00:00"
|
49
|
+
end
|
50
|
+
|
51
|
+
["CONVERT_TZ(DATE_FORMAT(#{day_start_column}, ?) + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, format, day_start, time_zone]
|
52
|
+
end
|
53
|
+
|
54
|
+
clean_group_clause(@relation.send(:sanitize_sql_array, query))
|
55
|
+
end
|
56
|
+
|
57
|
+
def clean_group_clause(clause)
|
58
|
+
# zero quoted in Active Record 7+
|
59
|
+
clause.gsub(/ (\-|\+) INTERVAL 0 second/, "").gsub(/ (\-|\+) INTERVAL '0' second/, "")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Groupdate
|
2
|
+
module Adapters
|
3
|
+
class PostgreSQLAdapter < BaseAdapter
|
4
|
+
def group_clause
|
5
|
+
time_zone = @time_zone.tzinfo.name
|
6
|
+
day_start_column = "#{column}::timestamptz AT TIME ZONE ? - INTERVAL ?"
|
7
|
+
day_start_interval = "#{day_start} second"
|
8
|
+
|
9
|
+
query =
|
10
|
+
case period
|
11
|
+
when :minute_of_hour
|
12
|
+
["EXTRACT(MINUTE FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
13
|
+
when :hour_of_day
|
14
|
+
["EXTRACT(HOUR FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
15
|
+
when :day_of_week
|
16
|
+
["EXTRACT(DOW FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
17
|
+
when :day_of_month
|
18
|
+
["EXTRACT(DAY FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
19
|
+
when :day_of_year
|
20
|
+
["EXTRACT(DOY FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
21
|
+
when :month_of_year
|
22
|
+
["EXTRACT(MONTH FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
23
|
+
when :week
|
24
|
+
["(DATE_TRUNC('day', #{day_start_column} - INTERVAL '1 day' * ((? + EXTRACT(DOW FROM #{day_start_column})::integer) % 7)) + INTERVAL ?)::date", time_zone, day_start_interval, 13 - week_start, time_zone, day_start_interval, day_start_interval]
|
25
|
+
when :custom
|
26
|
+
if @relation.connection.adapter_name == "Redshift"
|
27
|
+
["TIMESTAMP 'epoch' + (FLOOR(EXTRACT(EPOCH FROM #{column}::timestamp) / ?) * ?) * INTERVAL '1 second'", n_seconds, n_seconds]
|
28
|
+
else
|
29
|
+
["TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM #{column}::timestamptz) / ?) * ?)", n_seconds, n_seconds]
|
30
|
+
end
|
31
|
+
when :day, :month, :quarter, :year
|
32
|
+
["DATE_TRUNC(?, #{day_start_column})::date", period, time_zone, day_start_interval]
|
33
|
+
else
|
34
|
+
# day start is always 0 for seconds, minute, hour
|
35
|
+
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
36
|
+
end
|
37
|
+
|
38
|
+
clean_group_clause(@relation.send(:sanitize_sql_array, query))
|
39
|
+
end
|
40
|
+
|
41
|
+
def clean_group_clause(clause)
|
42
|
+
clause.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Groupdate
|
2
|
+
module Adapters
|
3
|
+
class SQLiteAdapter < BaseAdapter
|
4
|
+
def group_clause
|
5
|
+
raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
|
6
|
+
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
7
|
+
|
8
|
+
query =
|
9
|
+
if period == :week
|
10
|
+
["strftime('%Y-%m-%d', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
|
11
|
+
elsif period == :custom
|
12
|
+
["datetime((strftime('%s', #{column}) / ?) * ?, 'unixepoch')", n_seconds, n_seconds]
|
13
|
+
else
|
14
|
+
format =
|
15
|
+
case period
|
16
|
+
when :minute_of_hour
|
17
|
+
"%M"
|
18
|
+
when :hour_of_day
|
19
|
+
"%H"
|
20
|
+
when :day_of_week
|
21
|
+
"%w"
|
22
|
+
when :day_of_month
|
23
|
+
"%d"
|
24
|
+
when :day_of_year
|
25
|
+
"%j"
|
26
|
+
when :month_of_year
|
27
|
+
"%m"
|
28
|
+
when :second
|
29
|
+
"%Y-%m-%d %H:%M:%S UTC"
|
30
|
+
when :minute
|
31
|
+
"%Y-%m-%d %H:%M:00 UTC"
|
32
|
+
when :hour
|
33
|
+
"%Y-%m-%d %H:00:00 UTC"
|
34
|
+
when :day
|
35
|
+
"%Y-%m-%d"
|
36
|
+
when :month
|
37
|
+
"%Y-%m-01"
|
38
|
+
when :quarter
|
39
|
+
raise Groupdate::Error, "Quarter not supported for SQLite"
|
40
|
+
else # year
|
41
|
+
"%Y-01-01"
|
42
|
+
end
|
43
|
+
|
44
|
+
["strftime(?, #{column})", format]
|
45
|
+
end
|
46
|
+
|
47
|
+
@relation.send(:sanitize_sql_array, query)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/groupdate/enumerable.rb
CHANGED
@@ -1,30 +1,25 @@
|
|
1
1
|
module Enumerable
|
2
2
|
Groupdate::PERIODS.each do |period|
|
3
|
-
define_method :"group_by_#{period}" do |*args, &block|
|
3
|
+
define_method :"group_by_#{period}" do |*args, **options, &block|
|
4
4
|
if block
|
5
|
-
|
5
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" if args.any?
|
6
|
+
Groupdate::Magic::Enumerable.group_by(self, period, options, &block)
|
6
7
|
elsif respond_to?(:scoping)
|
7
|
-
scoping { @klass.
|
8
|
+
scoping { @klass.group_by_period(period, *args, **options, &block) }
|
8
9
|
else
|
9
10
|
raise ArgumentError, "no block given"
|
10
11
|
end
|
11
12
|
end
|
12
13
|
end
|
13
14
|
|
14
|
-
def group_by_period(*args, &block)
|
15
|
+
def group_by_period(period, *args, **options, &block)
|
15
16
|
if block || !respond_to?(:scoping)
|
16
|
-
|
17
|
-
options = args[1] || {}
|
17
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 1)" if args.any?
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
if permitted_periods.include?(period.to_s)
|
22
|
-
send("group_by_#{period}", options, &block)
|
23
|
-
else
|
24
|
-
raise ArgumentError, "Unpermitted period"
|
25
|
-
end
|
19
|
+
Groupdate::Magic.validate_period(period, options.delete(:permit))
|
20
|
+
send("group_by_#{period}", **options, &block)
|
26
21
|
else
|
27
|
-
scoping { @klass.
|
22
|
+
scoping { @klass.group_by_period(period, *args, **options, &block) }
|
28
23
|
end
|
29
24
|
end
|
30
25
|
end
|