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