groupdate 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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: ''