groupdate 3.2.0 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,304 @@
1
+ module Groupdate
2
+ class SeriesBuilder
3
+ attr_reader :period, :time_zone, :day_start, :week_start, :n_seconds, :options
4
+
5
+ def initialize(period:, time_zone:, day_start:, week_start:, n_seconds:, **options)
6
+ @period = period
7
+ @time_zone = time_zone
8
+ @week_start = week_start
9
+ @day_start = day_start
10
+ @n_seconds = n_seconds
11
+ @options = options
12
+ @week_start_key = Groupdate::Magic::DAYS[@week_start] if @week_start
13
+ end
14
+
15
+ def generate(data, default_value:, series_default: true, multiple_groups: false, group_index: nil)
16
+ series = generate_series(data, multiple_groups, group_index)
17
+ series = handle_multiple(data, series, multiple_groups, group_index)
18
+
19
+ verified_data = {}
20
+ series.each do |k|
21
+ verified_data[k] = data.delete(k)
22
+ end
23
+
24
+ unless entire_series?(series_default)
25
+ series = series.select { |k| verified_data[k] }
26
+ end
27
+
28
+ value = 0
29
+ result = series.to_h do |k|
30
+ value = verified_data[k] || (@options[:carry_forward] && value) || default_value
31
+ key =
32
+ if multiple_groups
33
+ k[0...group_index] + [key_format.call(k[group_index])] + k[(group_index + 1)..-1]
34
+ else
35
+ key_format.call(k)
36
+ end
37
+
38
+ [key, value]
39
+ end
40
+
41
+ result
42
+ end
43
+
44
+ def round_time(time)
45
+ if period == :custom
46
+ return time_zone.at((time.to_time.to_i / n_seconds) * n_seconds)
47
+ end
48
+
49
+ time = time.to_time.in_time_zone(time_zone)
50
+
51
+ if day_start != 0
52
+ # apply day_start to a time object that's not affected by DST
53
+ time = time.change(zone: utc)
54
+ time -= day_start.seconds
55
+ end
56
+
57
+ time =
58
+ case period
59
+ when :second
60
+ time.change(usec: 0)
61
+ when :minute
62
+ time.change(sec: 0)
63
+ when :hour
64
+ time.change(min: 0)
65
+ when :day
66
+ time.beginning_of_day
67
+ when :week
68
+ time.beginning_of_week(@week_start_key)
69
+ when :month
70
+ time.beginning_of_month
71
+ when :quarter
72
+ time.beginning_of_quarter
73
+ when :year
74
+ time.beginning_of_year
75
+ when :hour_of_day
76
+ time.hour
77
+ when :minute_of_hour
78
+ time.min
79
+ when :day_of_week
80
+ time.days_to_week_start(@week_start_key)
81
+ when :day_of_month
82
+ time.day
83
+ when :month_of_year
84
+ time.month
85
+ when :day_of_year
86
+ time.yday
87
+ else
88
+ raise Groupdate::Error, "Invalid period"
89
+ end
90
+
91
+ if day_start != 0 && time.is_a?(Time)
92
+ time += day_start.seconds
93
+ time = time.change(zone: time_zone)
94
+ end
95
+
96
+ time
97
+ end
98
+
99
+ def time_range
100
+ @time_range ||= begin
101
+ time_range = options[:range]
102
+
103
+ if time_range.is_a?(Range)
104
+ # check types
105
+ [time_range.begin, time_range.end].each do |v|
106
+ case v
107
+ when nil, Date, Time
108
+ # good
109
+ else
110
+ raise ArgumentError, "Range bounds should be Date or Time, not #{v.class.name}"
111
+ end
112
+ end
113
+
114
+ start = time_range.begin
115
+ start = start.in_time_zone(time_zone) if start
116
+
117
+ exclude_end = time_range.exclude_end?
118
+
119
+ finish = time_range.end
120
+ finish = finish.in_time_zone(time_zone) if finish
121
+ if time_range.end.is_a?(Date) && !time_range.end.is_a?(DateTime) && !exclude_end
122
+ finish += 1.day
123
+ exclude_end = true
124
+ end
125
+
126
+ if options[:expand_range]
127
+ start = round_time(start) if start
128
+ if finish && !(finish == round_time(finish) && exclude_end)
129
+ finish = round_time(finish) + step
130
+ exclude_end = true
131
+ end
132
+ end
133
+
134
+ time_range = Range.new(start, finish, exclude_end)
135
+ elsif !time_range && options[:last]
136
+ step = step()
137
+ raise ArgumentError, "Cannot use last option with #{period}" unless step
138
+
139
+ # loop instead of multiply to change start_at - see #151
140
+ start_at = now
141
+ (options[:last].to_i - 1).times do
142
+ start_at -= step
143
+ end
144
+
145
+ time_range =
146
+ if options[:current] == false
147
+ round_time(start_at - step)...round_time(now)
148
+ else
149
+ # extend to end of current period
150
+ round_time(start_at)...(round_time(now) + step)
151
+ end
152
+ end
153
+ time_range
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def now
160
+ @now ||= time_zone.now
161
+ end
162
+
163
+ def generate_series(data, multiple_groups, group_index)
164
+ case period
165
+ when :day_of_week
166
+ 0..6
167
+ when :hour_of_day
168
+ 0..23
169
+ when :minute_of_hour
170
+ 0..59
171
+ when :day_of_month
172
+ 1..31
173
+ when :day_of_year
174
+ 1..366
175
+ when :month_of_year
176
+ 1..12
177
+ else
178
+ time_range = self.time_range
179
+ time_range =
180
+ if time_range.is_a?(Range) && time_range.begin && time_range.end
181
+ time_range
182
+ else
183
+ # use first and last values
184
+ sorted_keys =
185
+ if multiple_groups
186
+ data.keys.map { |k| k[group_index] }.sort
187
+ else
188
+ data.keys.sort
189
+ end
190
+
191
+ if time_range.is_a?(Range)
192
+ if sorted_keys.any?
193
+ if time_range.begin
194
+ time_range.begin..sorted_keys.last
195
+ else
196
+ Range.new(sorted_keys.first, time_range.end, time_range.exclude_end?)
197
+ end
198
+ else
199
+ nil..nil
200
+ end
201
+ else
202
+ tr = sorted_keys.first..sorted_keys.last
203
+ if options[:current] == false && sorted_keys.any? && round_time(now) >= tr.last
204
+ tr = tr.first...round_time(now)
205
+ end
206
+ tr
207
+ end
208
+ end
209
+
210
+ if time_range.begin
211
+ series = [round_time(time_range.begin)]
212
+
213
+ step = step()
214
+
215
+ last_step = series.last
216
+ day_start_hour = day_start / 3600
217
+ loop do
218
+ next_step = last_step + step
219
+ next_step = round_time(next_step) if next_step.hour != day_start_hour # add condition to speed up
220
+ break unless time_range.cover?(next_step)
221
+
222
+ if next_step == last_step
223
+ last_step += step
224
+ next
225
+ end
226
+ series << next_step
227
+ last_step = next_step
228
+ end
229
+
230
+ series
231
+ else
232
+ []
233
+ end
234
+ end
235
+ end
236
+
237
+ def key_format
238
+ @key_format ||= begin
239
+ locale = options[:locale] || I18n.locale
240
+
241
+ if options[:format]
242
+ if options[:format].respond_to?(:call)
243
+ options[:format]
244
+ else
245
+ sunday = time_zone.parse("2014-03-02 00:00:00")
246
+ lambda do |key|
247
+ case period
248
+ when :hour_of_day
249
+ key = sunday + key.hours + day_start.seconds
250
+ when :minute_of_hour
251
+ key = sunday + key.minutes + day_start.seconds
252
+ when :day_of_week
253
+ key = sunday + key.days + (week_start + 1).days
254
+ when :day_of_month
255
+ key = Date.new(2014, 1, key).to_time
256
+ when :month_of_year
257
+ key = Date.new(2014, key, 1).to_time
258
+ end
259
+ I18n.localize(key, format: options[:format], locale: locale)
260
+ end
261
+ end
262
+ elsif [:day, :week, :month, :quarter, :year].include?(period)
263
+ lambda { |k| k.to_date }
264
+ else
265
+ lambda { |k| k }
266
+ end
267
+ end
268
+ end
269
+
270
+ def step
271
+ if period == :quarter
272
+ 3.months
273
+ elsif period == :custom
274
+ n_seconds
275
+ elsif 1.respond_to?(period)
276
+ 1.send(period)
277
+ end
278
+ end
279
+
280
+ def handle_multiple(data, series, multiple_groups, group_index)
281
+ reverse = options[:reverse]
282
+
283
+ if multiple_groups
284
+ keys = data.keys.map { |k| k[0...group_index] + k[(group_index + 1)..-1] }.uniq
285
+ series = series.to_a.reverse if reverse
286
+ keys.flat_map do |k|
287
+ series.map { |s| k[0...group_index] + [s] + k[group_index..-1] }
288
+ end
289
+ elsif reverse
290
+ series.to_a.reverse
291
+ else
292
+ series
293
+ end
294
+ end
295
+
296
+ def entire_series?(series_default)
297
+ options.key?(:series) ? options[:series] : series_default
298
+ end
299
+
300
+ def utc
301
+ @utc ||= ActiveSupport::TimeZone["Etc/UTC"]
302
+ end
303
+ end
304
+ end
@@ -1,3 +1,3 @@
1
1
  module Groupdate
2
- VERSION = "3.2.0"
2
+ VERSION = "6.2.1"
3
3
  end
data/lib/groupdate.rb CHANGED
@@ -1,22 +1,52 @@
1
+ # dependencies
2
+ require "active_support"
1
3
  require "active_support/core_ext/module/attribute_accessors"
2
4
  require "active_support/time"
3
- require "groupdate/version"
5
+
6
+ # modules
4
7
  require "groupdate/magic"
8
+ require "groupdate/series_builder"
9
+ require "groupdate/version"
10
+
11
+ # adapters
12
+ require "groupdate/adapters/base_adapter"
13
+ require "groupdate/adapters/mysql_adapter"
14
+ require "groupdate/adapters/postgresql_adapter"
15
+ require "groupdate/adapters/sqlite_adapter"
5
16
 
6
17
  module Groupdate
7
18
  class Error < RuntimeError; end
8
19
 
9
- PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :day_of_month, :month_of_year]
10
- # backwards compatibility for anyone who happened to use it
11
- FIELDS = PERIODS
20
+ PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :minute_of_hour, :day_of_month, :day_of_year, :month_of_year]
12
21
  METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
13
22
 
14
- mattr_accessor :week_start, :day_start, :time_zone, :dates
15
- self.week_start = :sun
23
+ mattr_accessor :week_start, :day_start, :time_zone
24
+ self.week_start = :sunday
16
25
  self.day_start = 0
17
- self.dates = true
26
+
27
+ # api for gems like ActiveMedian
28
+ def self.process_result(relation, result, **options)
29
+ if relation.groupdate_values
30
+ result = Groupdate::Magic::Relation.process_result(relation, result, **options)
31
+ end
32
+ result
33
+ end
34
+
35
+ def self.adapters
36
+ @adapters ||= {}
37
+ end
38
+
39
+ def self.register_adapter(name, adapter)
40
+ Array(name).each do |n|
41
+ adapters[n] = adapter
42
+ end
43
+ end
18
44
  end
19
45
 
46
+ Groupdate.register_adapter ["Mysql2", "Mysql2Spatial", "Mysql2Rgeo"], Groupdate::Adapters::MySQLAdapter
47
+ Groupdate.register_adapter ["PostgreSQL", "PostGIS", "Redshift"], Groupdate::Adapters::PostgreSQLAdapter
48
+ Groupdate.register_adapter "SQLite", Groupdate::Adapters::SQLiteAdapter
49
+
20
50
  require "groupdate/enumerable"
21
51
 
22
52
  ActiveSupport.on_load(:active_record) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: groupdate
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 6.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-31 00:00:00.000000000 Z
11
+ date: 2023-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,153 +16,41 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3'
19
+ version: '5.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: '3'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.3'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.3'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: minitest
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: activerecord
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: pg
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: mysql2
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 0.3.20
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: 0.3.20
111
- - !ruby/object:Gem::Dependency
112
- name: sqlite3
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- description: The simplest way to group temporal data
126
- email:
127
- - andrew@chartkick.com
26
+ version: '5.2'
27
+ description:
28
+ email: andrew@ankane.org
128
29
  executables: []
129
30
  extensions: []
130
31
  extra_rdoc_files: []
131
32
  files:
132
- - ".gitignore"
133
- - ".travis.yml"
134
33
  - CHANGELOG.md
135
- - Gemfile
34
+ - CONTRIBUTING.md
136
35
  - LICENSE.txt
137
36
  - README.md
138
- - Rakefile
139
- - groupdate.gemspec
140
37
  - lib/groupdate.rb
141
38
  - lib/groupdate/active_record.rb
142
- - lib/groupdate/calculations.rb
39
+ - lib/groupdate/adapters/base_adapter.rb
40
+ - lib/groupdate/adapters/mysql_adapter.rb
41
+ - lib/groupdate/adapters/postgresql_adapter.rb
42
+ - lib/groupdate/adapters/sqlite_adapter.rb
143
43
  - lib/groupdate/enumerable.rb
144
44
  - lib/groupdate/magic.rb
145
- - lib/groupdate/order_hack.rb
146
- - lib/groupdate/scopes.rb
147
- - lib/groupdate/series.rb
45
+ - lib/groupdate/query_methods.rb
46
+ - lib/groupdate/relation.rb
47
+ - lib/groupdate/series_builder.rb
148
48
  - lib/groupdate/version.rb
149
- - test/enumerable_test.rb
150
- - test/gemfiles/activerecord31.gemfile
151
- - test/gemfiles/activerecord32.gemfile
152
- - test/gemfiles/activerecord40.gemfile
153
- - test/gemfiles/activerecord41.gemfile
154
- - test/gemfiles/activerecord42.gemfile
155
- - test/gemfiles/redshift.gemfile
156
- - test/mysql_test.rb
157
- - test/postgresql_test.rb
158
- - test/redshift_test.rb
159
- - test/sqlite_test.rb
160
- - test/test_helper.rb
161
49
  homepage: https://github.com/ankane/groupdate
162
50
  licenses:
163
51
  - MIT
164
52
  metadata: {}
165
- post_install_message:
53
+ post_install_message:
166
54
  rdoc_options: []
167
55
  require_paths:
168
56
  - lib
@@ -170,28 +58,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
170
58
  requirements:
171
59
  - - ">="
172
60
  - !ruby/object:Gem::Version
173
- version: '0'
61
+ version: '2.6'
174
62
  required_rubygems_version: !ruby/object:Gem::Requirement
175
63
  requirements:
176
64
  - - ">="
177
65
  - !ruby/object:Gem::Version
178
66
  version: '0'
179
67
  requirements: []
180
- rubyforge_project:
181
- rubygems_version: 2.6.8
182
- signing_key:
68
+ rubygems_version: 3.4.10
69
+ signing_key:
183
70
  specification_version: 4
184
71
  summary: The simplest way to group temporal data
185
- test_files:
186
- - test/enumerable_test.rb
187
- - test/gemfiles/activerecord31.gemfile
188
- - test/gemfiles/activerecord32.gemfile
189
- - test/gemfiles/activerecord40.gemfile
190
- - test/gemfiles/activerecord41.gemfile
191
- - test/gemfiles/activerecord42.gemfile
192
- - test/gemfiles/redshift.gemfile
193
- - test/mysql_test.rb
194
- - test/postgresql_test.rb
195
- - test/redshift_test.rb
196
- - test/sqlite_test.rb
197
- - test/test_helper.rb
72
+ test_files: []
data/.gitignore DELETED
@@ -1,19 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- *.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
18
- .ruby-version
19
- .ruby-gemset
data/.travis.yml DELETED
@@ -1,25 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.2.4
4
- - jruby-9.1.5.0
5
- gemfile:
6
- - Gemfile
7
- - test/gemfiles/activerecord32.gemfile
8
- - test/gemfiles/activerecord40.gemfile
9
- - test/gemfiles/activerecord41.gemfile
10
- - test/gemfiles/activerecord42.gemfile
11
- sudo: false
12
- script: RUBYOPT=W0 bundle exec rake test
13
- before_install:
14
- - gem install bundler
15
- - mysql -e 'create database groupdate_test;'
16
- - mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
17
- - psql -c 'create database groupdate_test;' -U postgres
18
- notifications:
19
- email:
20
- on_success: never
21
- on_failure: change
22
- matrix:
23
- allow_failures:
24
- - rvm: jruby-9.1.5.0
25
- gemfile: Gemfile
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in groupdate.gemspec
4
- gemspec
5
-
6
- gem "activerecord", "~> 5.0.0"
data/Rakefile DELETED
@@ -1,24 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- task default: :test
5
- Rake::TestTask.new do |t|
6
- t.libs << "test"
7
- t.test_files = FileList["test/**/*_test.rb"].exclude(/redshift/)
8
- t.warning = false
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 DELETED
@@ -1,37 +0,0 @@
1
- # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "groupdate/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "groupdate"
8
- spec.version = Groupdate::VERSION
9
- spec.authors = ["Andrew Kane"]
10
- spec.email = ["andrew@chartkick.com"]
11
- spec.description = "The simplest way to group temporal data"
12
- spec.summary = "The simplest way to group temporal data"
13
- spec.homepage = "https://github.com/ankane/groupdate"
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_dependency "activesupport", ">= 3"
22
-
23
- spec.add_development_dependency "bundler", "~> 1.3"
24
- spec.add_development_dependency "rake"
25
- spec.add_development_dependency "minitest"
26
- spec.add_development_dependency "activerecord"
27
-
28
- if RUBY_PLATFORM == "java"
29
- spec.add_development_dependency "activerecord-jdbcpostgresql-adapter"
30
- spec.add_development_dependency "activerecord-jdbcmysql-adapter"
31
- spec.add_development_dependency "activerecord-jdbcsqlite3-adapter"
32
- else
33
- spec.add_development_dependency "pg"
34
- spec.add_development_dependency "mysql2", "~> 0.3.20"
35
- spec.add_development_dependency "sqlite3"
36
- end
37
- end