groupdate 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 81ffab786c4714072fad2155f8475839010822af
4
- data.tar.gz: 33dd72742440609989c5c61cb267574f8a5611b3
3
+ metadata.gz: 20e5986d43228afba669a7b5928f555a302680bc
4
+ data.tar.gz: e0190e033c4fa7a3b51eefa09254a61f44d55337
5
5
  SHA512:
6
- metadata.gz: 0df29d96f26fb544e20d768f980c930cf53806c3a4da0ba1e1e4963a814ad8fee09049c3996d9e3f45c9547cdd3147effcf9b3d22ef2deff06170fb2f75b1023
7
- data.tar.gz: 75b9c0e7c01cd1d936e4fbd9832a460827a1340d6a95b17fdde3a1898236edacc26daf9a87968e38f9210222083d7717b92bfcfa016090eefcd291f09f1ab083
6
+ metadata.gz: 059e84d54ea52420c4d2fa12ae479623652b704b03246c937f249548b9f55db8ca1e3a469404350b62c8b127d9276cefdc22f2f463d62fcdb35aec6de90baaa8
7
+ data.tar.gz: 5d61f760f0cc7a01d83df61b11b687c20761fd345e8e6152ae4b7f1955d57d54ba6a2e03ec759364ab12da0baf94007b99a8db1990be3708a04e82ad2e89a22f
data/.travis.yml CHANGED
@@ -9,3 +9,7 @@ before_script:
9
9
  - mysql -e 'create database groupdate_test;'
10
10
  - mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
11
11
  - psql -c 'create database groupdate_test;' -U postgres
12
+ notifications:
13
+ email:
14
+ on_success: never
15
+ on_failure: change
data/README.md CHANGED
@@ -123,7 +123,7 @@ Awesome, but you want to see the first week of May. Pass a range as the third a
123
123
  # pretend today is May 7
124
124
  time_range = 6.days.ago..Time.now
125
125
 
126
- User.group_by_day(:created_at, Time.zone, time_range).count(:created_at)
126
+ User.group_by_day(:created_at, Time.zone, time_range).count
127
127
  # {
128
128
  # 2013-05-01 00:00:00 UTC => 0,
129
129
  # 2013-05-02 00:00:00 UTC => 1,
@@ -133,16 +133,8 @@ User.group_by_day(:created_at, Time.zone, time_range).count(:created_at)
133
133
  # 2013-05-06 00:00:00 UTC => 0,
134
134
  # 2013-05-07 00:00:00 UTC => 0
135
135
  # }
136
- ```
137
-
138
- Wow, SQL magic!
139
-
140
- **Note:** Be sure to pass the column name to `count`. Otherwise, you get `1` for empty groups.
141
-
142
- For the day of the week and hour of the day, just pass `true`.
143
136
 
144
- ```ruby
145
- User.group_by_day_of_week(:created_at, Time.zone, true).count(:created_at)
137
+ User.group_by_day_of_week(:created_at, Time.zone, time_range).count
146
138
  # {
147
139
  # 0 => 0,
148
140
  # 1 => 1,
@@ -154,6 +146,10 @@ User.group_by_day_of_week(:created_at, Time.zone, true).count(:created_at)
154
146
  # }
155
147
  ```
156
148
 
149
+ Results are returned in ascending order, so no need to sort.
150
+
151
+ Also, this form of the method returns a Groupdate::Series instead of an ActiveRecord::Relation. ActiveRecord::Relation method calls (like `where` and `joins`) should come before this.
152
+
157
153
  ## Installation
158
154
 
159
155
  Add this line to your application's Gemfile:
data/lib/groupdate.rb CHANGED
@@ -1,171 +1,15 @@
1
1
  require "groupdate/version"
2
- require "active_record"
2
+ require "groupdate/scopes"
3
3
 
4
- module Groupdate
5
- extend ActiveSupport::Concern
6
-
7
- # Pattern from kaminari
8
- # https://github.com/amatsuda/kaminari/blob/master/lib/kaminari/models/active_record_extension.rb
9
- included do
10
- # Future subclasses will pick up the model extension
11
- class << self
12
- def inherited_with_groupdate(kls) #:nodoc:
13
- inherited_without_groupdate kls
14
- kls.send(:include, ClassMethods) if kls.superclass == ActiveRecord::Base
15
- end
16
- alias_method_chain :inherited, :groupdate
17
- end
18
-
19
- # Existing subclasses pick up the model extension as well
20
- self.descendants.each do |kls|
21
- kls.send(:include, ClassMethods) if kls.superclass == ActiveRecord::Base
22
- end
23
- end
24
-
25
- module ClassMethods
26
- extend ActiveSupport::Concern
27
-
28
- included do
29
- # Field list from
30
- # http://www.postgresql.org/docs/9.1/static/functions-datetime.html
31
- time_fields = %w(second minute hour day week month year)
32
- number_fields = %w(day_of_week hour_of_day)
33
- (time_fields + number_fields).each do |field|
34
- self.scope :"group_by_#{field}", lambda {|*args|
35
- column = connection.quote_table_name(args[0])
36
- time_zone = args[1] || Time.zone || "Etc/UTC"
37
- if time_zone.is_a?(ActiveSupport::TimeZone) or time_zone = ActiveSupport::TimeZone[time_zone]
38
- time_zone = time_zone.tzinfo.name
39
- else
40
- raise "Unrecognized time zone"
41
- end
42
- query =
43
- case connection.adapter_name
44
- when "MySQL", "Mysql2"
45
- case field
46
- when "day_of_week" # Sunday = 0, Monday = 1, etc
47
- # use CONCAT for consistent return type (String)
48
- ["DAYOFWEEK(CONVERT_TZ(#{column}, '+00:00', ?)) - 1", time_zone]
49
- when "hour_of_day"
50
- ["EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?))", time_zone]
51
- when "week"
52
- ["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL (DAYOFWEEK(CONVERT_TZ(#{column}, '+00:00', ?)) - 1) DAY), '+00:00', ?), '%Y-%m-%d 00:00:00'), ?, '+00:00')", time_zone, time_zone, time_zone]
53
- else
54
- format =
55
- case field
56
- when "second"
57
- "%Y-%m-%d %H:%i:%S"
58
- when "minute"
59
- "%Y-%m-%d %H:%i:00"
60
- when "hour"
61
- "%Y-%m-%d %H:00:00"
62
- when "day"
63
- "%Y-%m-%d 00:00:00"
64
- when "month"
65
- "%Y-%m-01 00:00:00"
66
- else # year
67
- "%Y-01-01 00:00:00"
68
- end
69
-
70
- ["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(#{column}, '+00:00', ?), '#{format}'), ?, '+00:00')", time_zone, time_zone]
71
- end
72
- when "PostgreSQL"
73
- case field
74
- when "day_of_week"
75
- ["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ?)", time_zone]
76
- when "hour_of_day"
77
- ["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ?)", time_zone]
78
- when "week" # start on Sunday, not PostgreSQL default Monday
79
- ["(DATE_TRUNC('#{field}', (#{column}::timestamptz + INTERVAL '1 day') AT TIME ZONE ?) - INTERVAL '1 day') AT TIME ZONE ?", time_zone, time_zone]
80
- else
81
- ["DATE_TRUNC('#{field}', #{column}::timestamptz AT TIME ZONE ?) AT TIME ZONE ?", time_zone, time_zone]
82
- end
83
- else
84
- raise "Connection adapter not supported: #{connection.adapter_name}"
85
- end
86
-
87
- if args[2] # zeros
88
- if time_fields.include?(field)
89
- range = args[2]
90
- unless range.is_a?(Range)
91
- raise "Expecting a range"
92
- end
93
-
94
- # determine start time
95
- time = range.first.in_time_zone(time_zone)
96
- starts_at =
97
- case field
98
- when "second"
99
- time.change(:usec => 0)
100
- when "minute"
101
- time.change(:sec => 0)
102
- when "hour"
103
- time.change(:min => 0)
104
- when "day"
105
- time.beginning_of_day
106
- when "week"
107
- time.beginning_of_week(:sunday)
108
- when "month"
109
- time.beginning_of_month
110
- else # year
111
- time.beginning_of_year
112
- end
113
-
114
- series = [starts_at]
115
-
116
- step = 1.send(field)
117
-
118
- while range.cover?(series.last + step)
119
- series << series.last + step
120
- end
121
- end
122
-
123
- derived_table =
124
- case connection.adapter_name
125
- when "PostgreSQL"
126
- case field
127
- when "day_of_week", "hour_of_day"
128
- max = field == "day_of_week" ? 6 : 23
129
- "SELECT generate_series(0, #{max}, 1) AS #{field}"
130
- else
131
- sanitize_sql_array(["SELECT (generate_series(CAST(? AS timestamptz) AT TIME ZONE ?, ?, ?) AT TIME ZONE ?) AS #{field}", starts_at, time_zone, series.last, "1 #{field}", time_zone])
132
- end
133
- else # MySQL
134
- case field
135
- when "day_of_week", "hour_of_day"
136
- max = field == "day_of_week" ? 6 : 23
137
- (0..max).map{|i| "SELECT #{i} AS #{field}" }.join(" UNION ")
138
- else
139
- sanitize_sql_array([series.map{|i| "SELECT CAST(? AS DATETIME) AS #{field}" }.join(" UNION ")] + series)
140
- end
141
- end
142
-
143
- joins("RIGHT OUTER JOIN (#{derived_table}) groupdate_series ON groupdate_series.#{field} = (#{sanitize_sql_array(query)})").group(Groupdate::OrderHack.new("groupdate_series.#{field}", field))
144
- else
145
- group(Groupdate::OrderHack.new(sanitize_sql_array(query), field))
146
- end
147
- }
148
- end
149
- end
150
- end
151
-
152
- class OrderHack < String
153
- attr_reader :field
154
-
155
- def initialize(str, field)
156
- super(str)
157
- @field = field
158
- end
159
- end
160
- end
161
-
162
- ActiveRecord::Base.send :include, Groupdate
4
+ ActiveRecord::Base.send :include, Groupdate::Scopes
163
5
 
164
6
  # hack for **unfixed** rails issue
165
7
  # https://github.com/rails/rails/issues/7121
166
8
  module ActiveRecord
167
9
  module Calculations
168
10
 
11
+ private
12
+
169
13
  def column_alias_for_with_hack(*keys)
170
14
  if keys.first.is_a?(Groupdate::OrderHack)
171
15
  keys.first.field
@@ -0,0 +1,11 @@
1
+ module Groupdate
2
+ class OrderHack < String
3
+ attr_reader :field, :time_zone
4
+
5
+ def initialize(str, field, time_zone)
6
+ super(str)
7
+ @field = field
8
+ @time_zone = time_zone
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,101 @@
1
+ require "groupdate/order_hack"
2
+ require "groupdate/series"
3
+ require "active_record"
4
+
5
+ module Groupdate
6
+ module Scopes
7
+ extend ActiveSupport::Concern
8
+
9
+ # Pattern from kaminari
10
+ # https://github.com/amatsuda/kaminari/blob/master/lib/kaminari/models/active_record_extension.rb
11
+ included do
12
+ # Future subclasses will pick up the model extension
13
+ class << self
14
+ def inherited_with_groupdate(kls) #:nodoc:
15
+ inherited_without_groupdate kls
16
+ kls.send(:include, ClassMethods) if kls.superclass == ActiveRecord::Base
17
+ end
18
+ alias_method_chain :inherited, :groupdate
19
+ end
20
+
21
+ # Existing subclasses pick up the model extension as well
22
+ self.descendants.each do |kls|
23
+ kls.send(:include, ClassMethods) if kls.superclass == ActiveRecord::Base
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ extend ActiveSupport::Concern
29
+
30
+ included do
31
+ # Field list from
32
+ # http://www.postgresql.org/docs/9.1/static/functions-datetime.html
33
+ time_fields = %w(second minute hour day week month year)
34
+ number_fields = %w(day_of_week hour_of_day)
35
+ (time_fields + number_fields).each do |field|
36
+ # no define_singleton_method in ruby 1.8
37
+ (class << self; self end).send :define_method, :"group_by_#{field}" do |*args|
38
+ column = connection.quote_table_name(args[0])
39
+ time_zone = args[1] || Time.zone || "Etc/UTC"
40
+ if time_zone.is_a?(ActiveSupport::TimeZone) or time_zone = ActiveSupport::TimeZone[time_zone]
41
+ time_zone = time_zone.tzinfo.name
42
+ else
43
+ raise "Unrecognized time zone"
44
+ end
45
+ query =
46
+ case connection.adapter_name
47
+ when "MySQL", "Mysql2"
48
+ case field
49
+ when "day_of_week" # Sunday = 0, Monday = 1, etc
50
+ # use CONCAT for consistent return type (String)
51
+ ["DAYOFWEEK(CONVERT_TZ(#{column}, '+00:00', ?)) - 1", time_zone]
52
+ when "hour_of_day"
53
+ ["EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?))", time_zone]
54
+ when "week"
55
+ ["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL (DAYOFWEEK(CONVERT_TZ(#{column}, '+00:00', ?)) - 1) DAY), '+00:00', ?), '%Y-%m-%d 00:00:00'), ?, '+00:00')", time_zone, time_zone, time_zone]
56
+ else
57
+ format =
58
+ case field
59
+ when "second"
60
+ "%Y-%m-%d %H:%i:%S"
61
+ when "minute"
62
+ "%Y-%m-%d %H:%i:00"
63
+ when "hour"
64
+ "%Y-%m-%d %H:00:00"
65
+ when "day"
66
+ "%Y-%m-%d 00:00:00"
67
+ when "month"
68
+ "%Y-%m-01 00:00:00"
69
+ else # year
70
+ "%Y-01-01 00:00:00"
71
+ end
72
+
73
+ ["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(#{column}, '+00:00', ?), '#{format}'), ?, '+00:00')", time_zone, time_zone]
74
+ end
75
+ when "PostgreSQL"
76
+ case field
77
+ when "day_of_week"
78
+ ["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ?)", time_zone]
79
+ when "hour_of_day"
80
+ ["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ?)", time_zone]
81
+ when "week" # start on Sunday, not PostgreSQL default Monday
82
+ ["(DATE_TRUNC('#{field}', (#{column}::timestamptz + INTERVAL '1 day') AT TIME ZONE ?) - INTERVAL '1 day') AT TIME ZONE ?", time_zone, time_zone]
83
+ else
84
+ ["DATE_TRUNC('#{field}', #{column}::timestamptz AT TIME ZONE ?) AT TIME ZONE ?", time_zone, time_zone]
85
+ end
86
+ else
87
+ raise "Connection adapter not supported: #{connection.adapter_name}"
88
+ end
89
+
90
+ group = group(Groupdate::OrderHack.new(sanitize_sql_array(query), field, time_zone))
91
+ if args[2]
92
+ Series.new(group, field, column, time_zone, args[2])
93
+ else
94
+ group
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,89 @@
1
+ module Groupdate
2
+ class Series
3
+
4
+ def initialize(relation, field, column, time_zone, time_range)
5
+ @relation = relation
6
+ if time_range.is_a?(Range)
7
+ @relation = relation.where("#{column} BETWEEN ? AND ?", time_range.first, time_range.last)
8
+ end
9
+ @field = field
10
+ @time_zone = time_zone
11
+ @time_range = time_range
12
+ end
13
+
14
+ def build_series(count)
15
+ cast_method =
16
+ case @field
17
+ when "day_of_week", "hour_of_day"
18
+ lambda{|k| k.to_i }
19
+ else
20
+ lambda{|k| k.is_a?(Time) ? k : Time.parse(k) }
21
+ end
22
+
23
+ count = Hash[count.map do |k, v|
24
+ [cast_method.call(k), v]
25
+ end]
26
+
27
+ series =
28
+ case @field
29
+ when "day_of_week"
30
+ 0..6
31
+ when "hour_of_day"
32
+ 0..23
33
+ else
34
+ time_range =
35
+ if @time_range.is_a?(Range)
36
+ @time_range
37
+ else
38
+ # use first and last values
39
+ sorted_keys = count.keys.sort
40
+ sorted_keys.first..sorted_keys.last
41
+ end
42
+
43
+ # determine start time
44
+ time = time_range.first.in_time_zone(@time_zone)
45
+ starts_at =
46
+ case @field
47
+ when "second"
48
+ time.change(:usec => 0)
49
+ when "minute"
50
+ time.change(:sec => 0)
51
+ when "hour"
52
+ time.change(:min => 0)
53
+ when "day"
54
+ time.beginning_of_day
55
+ when "week"
56
+ time.beginning_of_week(:sunday)
57
+ when "month"
58
+ time.beginning_of_month
59
+ else # year
60
+ time.beginning_of_year
61
+ end
62
+
63
+ series = [starts_at]
64
+
65
+ step = 1.send(@field)
66
+
67
+ while time_range.cover?(series.last + step)
68
+ series << series.last + step
69
+ end
70
+
71
+ series.map{|s| s.to_time }
72
+ end
73
+
74
+ Hash[series.map do |k|
75
+ [k, count[k] || 0]
76
+ end]
77
+ end
78
+
79
+ def method_missing(method, *args, &block)
80
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb
81
+ if ActiveRecord::Calculations.method_defined?(method)
82
+ build_series(@relation.send(method, *args, &block))
83
+ else
84
+ raise NoMethodError, "valid methods are: #{ActiveRecord::Calculations.instance_methods.join(", ")}"
85
+ end
86
+ end
87
+
88
+ end # Series
89
+ end
@@ -1,3 +1,3 @@
1
1
  module Groupdate
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
@@ -122,6 +122,11 @@ describe Groupdate do
122
122
  assert_group_number_tz :day_of_week, "2013-03-03 00:00:00 UTC", 6
123
123
  end
124
124
 
125
+ it "works with previous scopes" do
126
+ create_user "2013-05-01 00:00:00 UTC"
127
+ assert_equal({}, User.where("id = 0").group_by_day(:created_at).count)
128
+ end
129
+
125
130
  describe "returns zeros" do
126
131
 
127
132
  it "group_by_second" do
@@ -172,7 +177,7 @@ describe Groupdate do
172
177
  create_user "2013-05-01 00:00:00 UTC"
173
178
  expected = {}
174
179
  7.times do |n|
175
- expected[number_key(n, true)] = n == 3 ? 1 : 0
180
+ expected[n] = n == 3 ? 1 : 0
176
181
  end
177
182
  assert_equal(expected, User.group_by_day_of_week(:created_at, Time.zone, true).count(:created_at))
178
183
  end
@@ -181,7 +186,7 @@ describe Groupdate do
181
186
  create_user "2013-05-01 20:00:00 UTC"
182
187
  expected = {}
183
188
  24.times do |n|
184
- expected[number_key(n, true)] = n == 20 ? 1 : 0
189
+ expected[n] = n == 20 ? 1 : 0
185
190
  end
186
191
  assert_equal(expected, User.group_by_hour_of_day(:created_at, Time.zone, true).count(:created_at))
187
192
  end
@@ -189,9 +194,17 @@ describe Groupdate do
189
194
  it "excludes end" do
190
195
  create_user "2013-05-02 00:00:00 UTC"
191
196
  expected = {
192
- time_key("2013-05-01 00:00:00 UTC") => 0
197
+ Time.parse("2013-05-01 00:00:00 UTC") => 0
198
+ }
199
+ assert_equal(expected, User.group_by_day(:created_at, Time.zone, Time.parse("2013-05-01 00:00:00 UTC")...Time.parse("2013-05-02 00:00:00 UTC")).count)
200
+ end
201
+
202
+ it "works with previous scopes" do
203
+ create_user "2013-05-01 00:00:00 UTC"
204
+ expected = {
205
+ Time.parse("2013-05-01 00:00:00 UTC") => 0
193
206
  }
194
- assert_equal(expected, User.group_by_day(:created_at, Time.zone, Time.parse("2013-05-01 00:00:00 UTC")...Time.parse("2013-05-02 00:00:00 UTC")).count(:created_at))
207
+ assert_equal(expected, User.where("id = 0").group_by_day(:created_at, Time.zone, Time.parse("2013-05-01 00:00:00 UTC")..Time.parse("2013-05-01 23:59:59 UTC")).count)
195
208
  end
196
209
 
197
210
  end
@@ -203,7 +216,7 @@ describe Groupdate do
203
216
 
204
217
  def assert_group(method, created_at, key, time_zone = nil)
205
218
  create_user created_at
206
- assert_equal(ordered_hash({time_key(key) => 1}), User.send(:"group_by_#{method}", :created_at, time_zone).order(method).count)
219
+ assert_equal(ordered_hash({time_key(key) => 1}), User.send(:"group_by_#{method}", :created_at, time_zone).order(method.to_s).count)
207
220
  end
208
221
 
209
222
  def assert_group_tz(method, created_at, key)
@@ -212,7 +225,7 @@ describe Groupdate do
212
225
 
213
226
  def assert_group_number(method, created_at, key, time_zone = nil)
214
227
  create_user created_at
215
- assert_equal(ordered_hash({number_key(key) => 1}), User.send(:"group_by_#{method}", :created_at, time_zone).order(method).count)
228
+ assert_equal(ordered_hash({number_key(key) => 1}), User.send(:"group_by_#{method}", :created_at, time_zone).order(method.to_s).count)
216
229
  end
217
230
 
218
231
  def assert_group_number_tz(method, created_at, key)
@@ -223,9 +236,9 @@ describe Groupdate do
223
236
  create_user created_at
224
237
  expected = {}
225
238
  keys.each_with_index do |key, i|
226
- expected[time_key(key, java_hack)] = i == 1 ? 1 : 0
239
+ expected[Time.parse(key)] = i == 1 ? 1 : 0
227
240
  end
228
- assert_equal(expected, User.send(:"group_by_#{method}", :created_at, time_zone, Time.parse(range_start)..Time.parse(range_end)).order(method).count(:created_at))
241
+ assert_equal(expected, User.send(:"group_by_#{method}", :created_at, time_zone, Time.parse(range_start)..Time.parse(range_end)).count)
229
242
  end
230
243
 
231
244
  def assert_zeros_tz(method, created_at, keys, range_start, range_end)
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: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-02 00:00:00.000000000 Z
11
+ date: 2013-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -109,6 +109,9 @@ files:
109
109
  - Rakefile
110
110
  - groupdate.gemspec
111
111
  - lib/groupdate.rb
112
+ - lib/groupdate/order_hack.rb
113
+ - lib/groupdate/scopes.rb
114
+ - lib/groupdate/series.rb
112
115
  - lib/groupdate/version.rb
113
116
  - test/groupdate_test.rb
114
117
  homepage: ''