Commit 46e7a6976a651c537b288aa240fad26ecdf12867
1 parent
81c1bce0
Exists in
master
and in
17 other branches
Moved logic into Groupdate::Magic class
Showing
6 changed files
with
280 additions
and
278 deletions
Show diff stats
lib/groupdate.rb
lib/groupdate/enumerable.rb
1 | 1 | module Enumerable |
2 | 2 | |
3 | - time_fields = %w(second minute hour day week month year) | |
4 | - number_fields = %w(day_of_week hour_of_day) | |
5 | - (time_fields + number_fields).each do |field| | |
3 | + %i(second minute hour day week month year day_of_week hour_of_day).each do |field| | |
6 | 4 | define_method :"group_by_#{field}" do |options = {}, &block| |
7 | - time_zone = options[:time_zone] || Groupdate.time_zone || Time.zone || "Etc/UTC" | |
8 | - if time_zone.is_a?(ActiveSupport::TimeZone) or time_zone = ActiveSupport::TimeZone[time_zone] | |
9 | - time_zone_object = time_zone | |
10 | - time_zone = time_zone.tzinfo.name | |
11 | - else | |
12 | - raise "Unrecognized time zone" | |
13 | - end | |
14 | - | |
15 | - # for week | |
16 | - week_start = [:mon, :tue, :wed, :thu, :fri, :sat, :sun].index((options[:week_start] || options[:start] || Groupdate.week_start).to_sym) | |
17 | - if field == "week" and !week_start | |
18 | - raise "Unrecognized :week_start option" | |
19 | - end | |
20 | - | |
21 | - # for day | |
22 | - day_start = (options[:day_start] || Groupdate.day_start).to_i | |
23 | - | |
24 | - range = options.has_key?(:range) ? options[:range] : true | |
25 | - | |
26 | - Groupdate::Series.new(self, field, nil, time_zone_object, range, week_start, day_start, 0, options.slice(:reverse)).perform(:group_by, &block) | |
5 | + Groupdate::Magic.new(field, options).group_by(self, &block) | |
27 | 6 | end |
28 | 7 | end |
29 | 8 | ... | ... |
... | ... | @@ -0,0 +1,263 @@ |
1 | +module Groupdate | |
2 | + class Magic | |
3 | + attr_accessor :field, :options | |
4 | + | |
5 | + def initialize(field, options) | |
6 | + @field = field | |
7 | + @options = options | |
8 | + | |
9 | + if !time_zone | |
10 | + raise "Unrecognized time zone" | |
11 | + end | |
12 | + | |
13 | + if field == :week and !week_start | |
14 | + raise "Unrecognized :week_start option" | |
15 | + end | |
16 | + end | |
17 | + | |
18 | + def time_zone | |
19 | + @time_zone ||= begin | |
20 | + time_zone = options[:time_zone] || Groupdate.time_zone || Time.zone || "Etc/UTC" | |
21 | + time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone] | |
22 | + end | |
23 | + end | |
24 | + | |
25 | + def week_start | |
26 | + @week_start ||= [:mon, :tue, :wed, :thu, :fri, :sat, :sun].index((options[:week_start] || options[:start] || Groupdate.week_start).to_sym) | |
27 | + end | |
28 | + | |
29 | + def day_start | |
30 | + @day_start ||= (options[:day_start] || Groupdate.day_start).to_i | |
31 | + end | |
32 | + | |
33 | + def group_by(enum, &block) | |
34 | + series(enum.group_by{|v| round_time(block.call(v)) }, []) | |
35 | + end | |
36 | + | |
37 | + def time_range | |
38 | + @time_range ||= begin | |
39 | + time_range = options[:range] | |
40 | + if !time_range and options[:last] | |
41 | + step = 1.send(field) if 1.respond_to?(field) | |
42 | + if step | |
43 | + now = Time.now | |
44 | + time_range = round_time(now - (options[:last].to_i - 1).send(field))..now | |
45 | + end | |
46 | + end | |
47 | + time_range | |
48 | + end | |
49 | + end | |
50 | + | |
51 | + def relation(column, relation) | |
52 | + column = relation.connection.quote_table_name(column) | |
53 | + time_zone = self.time_zone.tzinfo.name | |
54 | + | |
55 | + adapter_name = relation.connection.adapter_name | |
56 | + query = | |
57 | + case adapter_name | |
58 | + when "MySQL", "Mysql2" | |
59 | + case field | |
60 | + when :day_of_week # Sunday = 0, Monday = 1, etc | |
61 | + # use CONCAT for consistent return type (String) | |
62 | + ["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} HOUR), '+00:00', ?)) - 1", time_zone] | |
63 | + when :hour_of_day | |
64 | + ["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start}) % 24", time_zone] | |
65 | + when :week | |
66 | + ["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} HOUR)) % 7) DAY) - INTERVAL #{day_start} HOUR, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} HOUR, ?, '+00:00')", time_zone, time_zone, time_zone] | |
67 | + else | |
68 | + format = | |
69 | + case field | |
70 | + when :second | |
71 | + "%Y-%m-%d %H:%i:%S" | |
72 | + when :minute | |
73 | + "%Y-%m-%d %H:%i:00" | |
74 | + when :hour | |
75 | + "%Y-%m-%d %H:00:00" | |
76 | + when :day | |
77 | + "%Y-%m-%d 00:00:00" | |
78 | + when :month | |
79 | + "%Y-%m-01 00:00:00" | |
80 | + else # year | |
81 | + "%Y-01-01 00:00:00" | |
82 | + end | |
83 | + | |
84 | + ["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} HOUR), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} HOUR)", time_zone, time_zone] | |
85 | + end | |
86 | + when "PostgreSQL", "PostGIS" | |
87 | + case field | |
88 | + when :day_of_week | |
89 | + ["EXTRACT(DOW from (#{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} hour'))::integer", time_zone] | |
90 | + when :hour_of_day | |
91 | + ["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} hour')::integer", time_zone] | |
92 | + when :week # start on Sunday, not PostgreSQL default Monday | |
93 | + ["(DATE_TRUNC('#{field}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start}' hour) AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start}' hour) AT TIME ZONE ?", time_zone, time_zone] | |
94 | + else | |
95 | + ["(DATE_TRUNC('#{field}', (#{column}::timestamptz - INTERVAL '#{day_start} hour') AT TIME ZONE ?) + INTERVAL '#{day_start} hour') AT TIME ZONE ?", time_zone, time_zone] | |
96 | + end | |
97 | + else | |
98 | + raise "Connection adapter not supported: #{adapter_name}" | |
99 | + end | |
100 | + | |
101 | + group = relation.group(Groupdate::OrderHack.new(relation.send(:sanitize_sql_array, query), field, time_zone)) | |
102 | + if options[:series] == false | |
103 | + group | |
104 | + else | |
105 | + relation = | |
106 | + if time_range.is_a?(Range) | |
107 | + # doesn't matter whether we include the end of a ... range - it will be excluded later | |
108 | + group.where("#{column} >= ? AND #{column} <= ?", time_range.first, time_range.last) | |
109 | + else | |
110 | + group.where("#{column} IS NOT NULL") | |
111 | + end | |
112 | + | |
113 | + # TODO do not change object state | |
114 | + @group_index = group.group_values.size - 1 | |
115 | + | |
116 | + Groupdate::Series.new(self, relation) | |
117 | + end | |
118 | + end | |
119 | + | |
120 | + def series(count, default_value, multiple_groups = false, reverse = false) | |
121 | + series = | |
122 | + case field | |
123 | + when :day_of_week | |
124 | + 0..6 | |
125 | + when :hour_of_day | |
126 | + 0..23 | |
127 | + else | |
128 | + time_range = self.time_range | |
129 | + time_range = | |
130 | + if time_range.is_a?(Range) | |
131 | + time_range | |
132 | + else | |
133 | + # use first and last values | |
134 | + sorted_keys = | |
135 | + if multiple_groups | |
136 | + count.keys.map{|k| k[@group_index] }.sort | |
137 | + else | |
138 | + count.keys.sort | |
139 | + end | |
140 | + sorted_keys.first..sorted_keys.last | |
141 | + end | |
142 | + | |
143 | + if time_range.first | |
144 | + series = [round_time(time_range.first)] | |
145 | + | |
146 | + step = 1.send(field) | |
147 | + | |
148 | + while time_range.cover?(series.last + step) | |
149 | + series << series.last + step | |
150 | + end | |
151 | + | |
152 | + if multiple_groups | |
153 | + keys = count.keys.map{|k| k[0...@group_index] + k[(@group_index + 1)..-1] }.uniq | |
154 | + series = series.reverse if reverse | |
155 | + keys.flat_map do |k| | |
156 | + series.map{|s| k[0...@group_index] + [s] + k[@group_index..-1] } | |
157 | + end | |
158 | + else | |
159 | + series | |
160 | + end | |
161 | + else | |
162 | + [] | |
163 | + end | |
164 | + end | |
165 | + | |
166 | + # reversed above if multiple groups | |
167 | + if !multiple_groups and reverse | |
168 | + series = series.to_a.reverse | |
169 | + end | |
170 | + | |
171 | + key_format = | |
172 | + if options[:format] | |
173 | + if options[:format].respond_to?(:call) | |
174 | + options[:format] | |
175 | + else | |
176 | + sunday = time_zone.parse("2014-03-02 00:00:00") | |
177 | + lambda do |key| | |
178 | + case field | |
179 | + when :hour_of_day | |
180 | + key = sunday + key.hours + day_start.hours | |
181 | + when :day_of_week | |
182 | + key = sunday + key.days | |
183 | + end | |
184 | + key.strftime(options[:format].to_s) | |
185 | + end | |
186 | + end | |
187 | + else | |
188 | + lambda{|k| k } | |
189 | + end | |
190 | + | |
191 | + Hash[series.map do |k| | |
192 | + [multiple_groups ? k[0...@group_index] + [key_format.call(k[@group_index])] + k[(@group_index + 1)..-1] : key_format.call(k), count[k] || default_value] | |
193 | + end] | |
194 | + end | |
195 | + | |
196 | + def perform(relation, method, *args, &block) | |
197 | + # undo reverse since we do not want this to appear in the query | |
198 | + reverse = relation.reverse_order_value | |
199 | + if reverse | |
200 | + relation = relation.reverse_order | |
201 | + end | |
202 | + order = relation.order_values.first | |
203 | + if order.is_a?(String) | |
204 | + parts = order.split(" ") | |
205 | + reverse_order = (parts.size == 2 && parts[0].to_sym == field && parts[1].to_s.downcase == "desc") | |
206 | + reverse = !reverse if reverse_order | |
207 | + end | |
208 | + | |
209 | + multiple_groups = relation.group_values.size > 1 | |
210 | + | |
211 | + cast_method = | |
212 | + case field | |
213 | + when :day_of_week, :hour_of_day | |
214 | + lambda{|k| k.to_i } | |
215 | + else | |
216 | + utc = ActiveSupport::TimeZone["UTC"] | |
217 | + lambda{|k| (k.is_a?(String) ? utc.parse(k) : k.to_time).in_time_zone(time_zone) } | |
218 | + end | |
219 | + | |
220 | + count = | |
221 | + begin | |
222 | + Hash[ relation.send(method, *args, &block).map{|k, v| [multiple_groups ? k[0...@group_index] + [cast_method.call(k[@group_index])] + k[(@group_index + 1)..-1] : cast_method.call(k), v] } ] | |
223 | + rescue NoMethodError | |
224 | + raise "Be sure to install time zone support - https://github.com/ankane/groupdate#for-mysql" | |
225 | + end | |
226 | + | |
227 | + series(count, 0, multiple_groups, reverse) | |
228 | + end | |
229 | + | |
230 | + def round_time(time) | |
231 | + time = time.to_time.in_time_zone(time_zone) - day_start.hours | |
232 | + | |
233 | + time = | |
234 | + case field | |
235 | + when :second | |
236 | + time.change(:usec => 0) | |
237 | + when :minute | |
238 | + time.change(:sec => 0) | |
239 | + when :hour | |
240 | + time.change(:min => 0) | |
241 | + when :day | |
242 | + time.beginning_of_day | |
243 | + when :week | |
244 | + # same logic as MySQL group | |
245 | + weekday = (time.wday - 1) % 7 | |
246 | + (time - ((7 - week_start + weekday) % 7).days).midnight | |
247 | + when :month | |
248 | + time.beginning_of_month | |
249 | + when :year | |
250 | + time.beginning_of_year | |
251 | + when :hour_of_day | |
252 | + time.hour | |
253 | + when :day_of_week | |
254 | + (7 - week_start + ((time.wday - 1) % 7) % 7) | |
255 | + else | |
256 | + raise "Invalid field" | |
257 | + end | |
258 | + | |
259 | + time.is_a?(Time) ? time + day_start.hours : time | |
260 | + end | |
261 | + | |
262 | + end | |
263 | +end | ... | ... |
lib/groupdate/order_hack.rb
lib/groupdate/scopes.rb
... | ... | @@ -4,83 +4,17 @@ require "active_record" |
4 | 4 | |
5 | 5 | module Groupdate |
6 | 6 | module Scopes |
7 | - time_fields = %w(second minute hour day week month year) | |
8 | - number_fields = %w(day_of_week hour_of_day) | |
9 | - (time_fields + number_fields).each do |field| | |
7 | + | |
8 | + %i(second minute hour day week month year day_of_week hour_of_day).each do |field| | |
10 | 9 | define_method :"group_by_#{field}" do |*args| |
11 | 10 | args = args.dup |
12 | 11 | options = args[-1].is_a?(Hash) ? args.pop : {} |
13 | - column = connection.quote_table_name(args[0]) | |
14 | - time_zone = args[1] || options[:time_zone] || Groupdate.time_zone || Time.zone || "Etc/UTC" | |
15 | - if time_zone.is_a?(ActiveSupport::TimeZone) or time_zone = ActiveSupport::TimeZone[time_zone] | |
16 | - time_zone_object = time_zone | |
17 | - time_zone = time_zone.tzinfo.name | |
18 | - else | |
19 | - raise "Unrecognized time zone" | |
20 | - end | |
21 | - | |
22 | - # for week | |
23 | - week_start = [:mon, :tue, :wed, :thu, :fri, :sat, :sun].index((options[:week_start] || options[:start] || Groupdate.week_start).to_sym) | |
24 | - if field == "week" and !week_start | |
25 | - raise "Unrecognized :week_start option" | |
26 | - end | |
27 | - | |
28 | - # for day | |
29 | - day_start = (options[:day_start] || Groupdate.day_start).to_i | |
12 | + options[:time_zone] ||= args[1] unless args[1].nil? | |
13 | + options[:range] ||= args[2] unless args[2].nil? | |
30 | 14 | |
31 | - query = | |
32 | - case connection.adapter_name | |
33 | - when "MySQL", "Mysql2" | |
34 | - case field | |
35 | - when "day_of_week" # Sunday = 0, Monday = 1, etc | |
36 | - # use CONCAT for consistent return type (String) | |
37 | - ["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} HOUR), '+00:00', ?)) - 1", time_zone] | |
38 | - when "hour_of_day" | |
39 | - ["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start}) % 24", time_zone] | |
40 | - when "week" | |
41 | - ["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} HOUR)) % 7) DAY) - INTERVAL #{day_start} HOUR, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} HOUR, ?, '+00:00')", time_zone, time_zone, time_zone] | |
42 | - else | |
43 | - format = | |
44 | - case field | |
45 | - when "second" | |
46 | - "%Y-%m-%d %H:%i:%S" | |
47 | - when "minute" | |
48 | - "%Y-%m-%d %H:%i:00" | |
49 | - when "hour" | |
50 | - "%Y-%m-%d %H:00:00" | |
51 | - when "day" | |
52 | - "%Y-%m-%d 00:00:00" | |
53 | - when "month" | |
54 | - "%Y-%m-01 00:00:00" | |
55 | - else # year | |
56 | - "%Y-01-01 00:00:00" | |
57 | - end | |
58 | - | |
59 | - ["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} HOUR), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} HOUR)", time_zone, time_zone] | |
60 | - end | |
61 | - when "PostgreSQL", "PostGIS" | |
62 | - case field | |
63 | - when "day_of_week" | |
64 | - ["EXTRACT(DOW from (#{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} hour'))::integer", time_zone] | |
65 | - when "hour_of_day" | |
66 | - ["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} hour')::integer", time_zone] | |
67 | - when "week" # start on Sunday, not PostgreSQL default Monday | |
68 | - ["(DATE_TRUNC('#{field}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start}' hour) AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start}' hour) AT TIME ZONE ?", time_zone, time_zone] | |
69 | - else | |
70 | - ["(DATE_TRUNC('#{field}', (#{column}::timestamptz - INTERVAL '#{day_start} hour') AT TIME ZONE ?) + INTERVAL '#{day_start} hour') AT TIME ZONE ?", time_zone, time_zone] | |
71 | - end | |
72 | - else | |
73 | - raise "Connection adapter not supported: #{connection.adapter_name}" | |
74 | - end | |
75 | - | |
76 | - group = group(Groupdate::OrderHack.new(sanitize_sql_array(query), field, time_zone)) | |
77 | - range = args[2] || options[:range] || true | |
78 | - unless options[:series] == false | |
79 | - Series.new(group, field, column, time_zone_object, range, week_start, day_start, group.group_values.size - 1, options) | |
80 | - else | |
81 | - group | |
82 | - end | |
15 | + Groupdate::Magic.new(field, options).relation(args[0], self) | |
83 | 16 | end |
84 | 17 | end |
18 | + | |
85 | 19 | end |
86 | 20 | end | ... | ... |
lib/groupdate/series.rb
1 | 1 | module Groupdate |
2 | 2 | class Series |
3 | - attr_accessor :relation | |
3 | + attr_accessor :magic, :relation | |
4 | 4 | |
5 | - def initialize(relation, field, column, time_zone, time_range, week_start, day_start, group_index, options) | |
5 | + def initialize(magic, relation) | |
6 | + @magic = magic | |
6 | 7 | @relation = relation |
7 | - @field = field | |
8 | - @column = column | |
9 | - @time_zone = time_zone | |
10 | - @time_range = time_range | |
11 | - @week_start = week_start | |
12 | - @day_start = day_start | |
13 | - @group_index = group_index | |
14 | - @options = options | |
15 | - end | |
16 | - | |
17 | - def perform(method = nil, *args, &block) | |
18 | - utc = ActiveSupport::TimeZone["UTC"] | |
19 | - | |
20 | - time_range = @time_range | |
21 | - if !time_range.is_a?(Range) and @options[:last] | |
22 | - step = 1.send(@field) if 1.respond_to?(@field) | |
23 | - if step | |
24 | - now = Time.now | |
25 | - time_range = round_time(now - (@options[:last].to_i - 1).send(@field))..now | |
26 | - end | |
27 | - end | |
28 | - | |
29 | - if @relation.is_a?(Enumerable) | |
30 | - multiple_groups = false | |
31 | - reverse = @options[:reverse] || false | |
32 | - count = @relation.group_by{|v| round_time(block.call(v)) } | |
33 | - default_value = [] | |
34 | - else | |
35 | - relation = | |
36 | - if time_range.is_a?(Range) | |
37 | - # doesn't matter whether we include the end of a ... range - it will be excluded later | |
38 | - @relation.where("#{@column} >= ? AND #{@column} <= ?", time_range.first, time_range.last) | |
39 | - else | |
40 | - @relation.where("#{@column} IS NOT NULL") | |
41 | - end | |
42 | - | |
43 | - # undo reverse since we do not want this to appear in the query | |
44 | - reverse = relation.reverse_order_value | |
45 | - if reverse | |
46 | - relation = relation.reverse_order | |
47 | - end | |
48 | - order = relation.order_values.first | |
49 | - if order.is_a?(String) | |
50 | - parts = order.split(" ") | |
51 | - reverse_order = (parts.size == 2 && parts[0] == @field && parts[1].to_s.downcase == "desc") | |
52 | - reverse = !reverse if reverse_order | |
53 | - end | |
54 | - | |
55 | - multiple_groups = relation.group_values.size > 1 | |
56 | - | |
57 | - cast_method = | |
58 | - case @field | |
59 | - when "day_of_week", "hour_of_day" | |
60 | - lambda{|k| k.to_i } | |
61 | - else | |
62 | - lambda{|k| (k.is_a?(String) ? utc.parse(k) : k.to_time).in_time_zone(@time_zone) } | |
63 | - end | |
64 | - | |
65 | - count = | |
66 | - begin | |
67 | - Hash[ relation.send(method, *args, &block).map{|k, v| [multiple_groups ? k[0...@group_index] + [cast_method.call(k[@group_index])] + k[(@group_index + 1)..-1] : cast_method.call(k), v] } ] | |
68 | - rescue NoMethodError | |
69 | - raise "Be sure to install time zone support - https://github.com/ankane/groupdate#for-mysql" | |
70 | - end | |
71 | - | |
72 | - default_value = 0 | |
73 | - end | |
74 | - | |
75 | - series = | |
76 | - case @field | |
77 | - when "day_of_week" | |
78 | - 0..6 | |
79 | - when "hour_of_day" | |
80 | - 0..23 | |
81 | - else | |
82 | - time_range = | |
83 | - if time_range.is_a?(Range) | |
84 | - time_range | |
85 | - else | |
86 | - # use first and last values | |
87 | - sorted_keys = | |
88 | - if multiple_groups | |
89 | - count.keys.map{|k| k[@group_index] }.sort | |
90 | - else | |
91 | - count.keys.sort | |
92 | - end | |
93 | - sorted_keys.first..sorted_keys.last | |
94 | - end | |
95 | - | |
96 | - if time_range.first | |
97 | - series = [round_time(time_range.first)] | |
98 | - | |
99 | - step = 1.send(@field) | |
100 | - | |
101 | - while time_range.cover?(series.last + step) | |
102 | - series << series.last + step | |
103 | - end | |
104 | - | |
105 | - if multiple_groups | |
106 | - keys = count.keys.map{|k| k[0...@group_index] + k[(@group_index + 1)..-1] }.uniq | |
107 | - series = series.reverse if reverse | |
108 | - keys.flat_map do |k| | |
109 | - series.map{|s| k[0...@group_index] + [s] + k[@group_index..-1] } | |
110 | - end | |
111 | - else | |
112 | - series | |
113 | - end | |
114 | - else | |
115 | - [] | |
116 | - end | |
117 | - end | |
118 | - | |
119 | - # reversed above if multiple groups | |
120 | - if !multiple_groups and reverse | |
121 | - series = series.to_a.reverse | |
122 | - end | |
123 | - | |
124 | - key_format = | |
125 | - if @options[:format] | |
126 | - if @options[:format].respond_to?(:call) | |
127 | - @options[:format] | |
128 | - else | |
129 | - sunday = @time_zone.parse("2014-03-02 00:00:00") | |
130 | - lambda do |key| | |
131 | - case @field | |
132 | - when "hour_of_day" | |
133 | - key = sunday + key.hours + @day_start.hours | |
134 | - when "day_of_week" | |
135 | - key = sunday + key.days | |
136 | - end | |
137 | - key.strftime(@options[:format].to_s) | |
138 | - end | |
139 | - end | |
140 | - else | |
141 | - lambda{|k| k } | |
142 | - end | |
143 | - | |
144 | - Hash[series.map do |k| | |
145 | - [multiple_groups ? k[0...@group_index] + [key_format.call(k[@group_index])] + k[(@group_index + 1)..-1] : key_format.call(k), count[k] || default_value] | |
146 | - end] | |
147 | - end | |
148 | - | |
149 | - def round_time(time) | |
150 | - time = time.to_time.in_time_zone(@time_zone) - @day_start.hours | |
151 | - | |
152 | - time = | |
153 | - case @field | |
154 | - when "second" | |
155 | - time.change(:usec => 0) | |
156 | - when "minute" | |
157 | - time.change(:sec => 0) | |
158 | - when "hour" | |
159 | - time.change(:min => 0) | |
160 | - when "day" | |
161 | - time.beginning_of_day | |
162 | - when "week" | |
163 | - # same logic as MySQL group | |
164 | - weekday = (time.wday - 1) % 7 | |
165 | - (time - ((7 - @week_start + weekday) % 7).days).midnight | |
166 | - when "month" | |
167 | - time.beginning_of_month | |
168 | - when "hour_of_day" | |
169 | - time.hour | |
170 | - when "day_of_week" | |
171 | - (7 - @week_start + ((time.wday - 1) % 7) % 7) | |
172 | - else # year | |
173 | - time.beginning_of_year | |
174 | - end | |
175 | - | |
176 | - time.is_a?(Time) ? time + @day_start.hours : time | |
177 | - end | |
178 | - | |
179 | - def clone | |
180 | - Groupdate::Series.new(@relation, @field, @column, @time_zone, @time_range, @week_start, @day_start, @group_index, @options) | |
181 | 8 | end |
182 | 9 | |
183 | 10 | # clone to prevent modifying original variables |
184 | 11 | def method_missing(method, *args, &block) |
185 | 12 | # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb |
186 | 13 | if ActiveRecord::Calculations.method_defined?(method) |
187 | - clone.perform(method, *args, &block) | |
14 | + magic.perform(relation, method, *args, &block) | |
188 | 15 | elsif @relation.respond_to?(method) |
189 | - series = clone | |
190 | - series.relation = @relation.send(method, *args, &block) | |
191 | - series | |
16 | + Groupdate::Series.new(magic, relation.send(method, *args, &block)) | |
192 | 17 | else |
193 | 18 | super |
194 | 19 | end |
195 | 20 | end |
196 | 21 | |
197 | 22 | def respond_to?(method, include_all = false) |
198 | - ActiveRecord::Calculations.method_defined?(method) || @relation.respond_to?(method) || super | |
23 | + ActiveRecord::Calculations.method_defined?(method) || relation.respond_to?(method) || super | |
199 | 24 | end |
200 | 25 | |
201 | - end # Series | |
26 | + end | |
202 | 27 | end | ... | ... |