neuron-client 0.1.0 → 0.2.0
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.
- data/README.md +34 -8
- data/lib/neuron-client.rb +61 -12
- data/lib/neuron-client/{connection.rb → admin_connection.rb} +7 -7
- data/lib/neuron-client/api.rb +48 -31
- data/lib/neuron-client/membase_connection.rb +18 -0
- data/lib/neuron-client/model/admin/ad.rb +22 -0
- data/lib/neuron-client/model/admin/ad_zone.rb +15 -0
- data/lib/neuron-client/model/admin/base.rb +91 -0
- data/lib/neuron-client/model/admin/blocked_referer.rb +12 -0
- data/lib/neuron-client/model/admin/blocked_user_agent.rb +12 -0
- data/lib/neuron-client/model/admin/geo_target.rb +16 -0
- data/lib/neuron-client/model/admin/report.rb +15 -0
- data/lib/neuron-client/model/admin/s3_file.rb +12 -0
- data/lib/neuron-client/model/admin/zone.rb +15 -0
- data/lib/neuron-client/model/base.rb +38 -0
- data/lib/neuron-client/model/common/ad.rb +40 -0
- data/lib/neuron-client/model/common/ad_calculations.rb +329 -0
- data/lib/neuron-client/model/common/ad_zone.rb +17 -0
- data/lib/neuron-client/model/common/base.rb +67 -0
- data/lib/neuron-client/model/common/blocked_referer.rb +16 -0
- data/lib/neuron-client/model/common/blocked_user_agent.rb +16 -0
- data/lib/neuron-client/model/common/geo_target.rb +16 -0
- data/lib/neuron-client/model/common/report.rb +21 -0
- data/lib/neuron-client/model/common/s3_file.rb +16 -0
- data/lib/neuron-client/model/common/zone.rb +22 -0
- data/lib/neuron-client/model/common/zone_calculations.rb +41 -0
- data/lib/neuron-client/model/membase/ad.rb +31 -0
- data/lib/neuron-client/model/membase/ad_zone.rb +11 -0
- data/lib/neuron-client/model/membase/blocked_referer.rb +18 -0
- data/lib/neuron-client/model/membase/blocked_user_agent.rb +18 -0
- data/lib/neuron-client/model/membase/geo_target.rb +11 -0
- data/lib/neuron-client/model/membase/report.rb +11 -0
- data/lib/neuron-client/model/membase/s3_file.rb +11 -0
- data/lib/neuron-client/model/membase/zone.rb +19 -0
- data/lib/neuron-client/model/models.rb +14 -0
- data/lib/neuron-client/version.rb +1 -1
- data/neuron-client.gemspec +18 -11
- data/spec/fixtures/vcr_cassettes/s3_file.yml +186 -4
- data/spec/lib/admin_connection_spec.rb +82 -0
- data/spec/lib/api_spec.rb +80 -0
- data/spec/lib/membase_connection_spec.rb +27 -0
- data/spec/lib/model/admin/ad_spec.rb +34 -0
- data/spec/lib/model/admin/ad_zone_spec.rb +19 -0
- data/spec/lib/model/admin/base_spec.rb +11 -0
- data/spec/lib/model/admin/blocked_referer_spec.rb +11 -0
- data/spec/lib/model/admin/blocked_user_agent_spec.rb +11 -0
- data/spec/lib/model/admin/geo_target_spec.rb +30 -0
- data/spec/lib/model/admin/report_spec.rb +21 -0
- data/spec/lib/model/admin/s3_spec.rb +11 -0
- data/spec/lib/model/admin/zone_spec.rb +21 -0
- data/spec/lib/model/base_spec.rb +89 -0
- data/spec/lib/model/common/ad_calculations_spec.rb +1148 -0
- data/spec/lib/model/common/ad_spec.rb +11 -0
- data/spec/lib/model/common/ad_zone_spec.rb +11 -0
- data/spec/lib/model/common/base_spec.rb +11 -0
- data/spec/lib/model/common/blocked_referer_spec.rb +11 -0
- data/spec/lib/model/common/blocked_user_agent_spec.rb +11 -0
- data/spec/lib/model/common/geo_target_spec.rb +11 -0
- data/spec/lib/model/common/report_spec.rb +11 -0
- data/spec/lib/model/common/s3_spec.rb +11 -0
- data/spec/lib/model/common/zone_calculations_spec.rb +54 -0
- data/spec/lib/model/common/zone_spec.rb +11 -0
- data/spec/lib/model/membase/ad_spec.rb +50 -0
- data/spec/lib/model/membase/ad_zone_spec.rb +11 -0
- data/spec/lib/model/membase/base_spec.rb +11 -0
- data/spec/lib/model/membase/blocked_referer_spec.rb +30 -0
- data/spec/lib/model/membase/blocked_user_agent_spec.rb +30 -0
- data/spec/lib/model/membase/geo_target_spec.rb +11 -0
- data/spec/lib/model/membase/report_spec.rb +11 -0
- data/spec/lib/model/membase/s3_spec.rb +11 -0
- data/spec/lib/model/membase/zone_spec.rb +28 -0
- data/spec/lib/old_spec.rb +192 -149
- data/spec/lib/s3_file_spec.rb +45 -42
- data/spec/spec_helper.rb +2 -1
- metadata +296 -57
- data/lib/neuron-client/ad.rb +0 -39
- data/lib/neuron-client/ad_zone.rb +0 -16
- data/lib/neuron-client/blocked_referer.rb +0 -12
- data/lib/neuron-client/blocked_user_agent.rb +0 -12
- data/lib/neuron-client/connected.rb +0 -138
- data/lib/neuron-client/geo_target.rb +0 -16
- data/lib/neuron-client/real_time_stats.rb +0 -0
- data/lib/neuron-client/report.rb +0 -20
- data/lib/neuron-client/s3_file.rb +0 -10
- data/lib/neuron-client/zone.rb +0 -16
@@ -0,0 +1,16 @@
|
|
1
|
+
module Neuron
|
2
|
+
module Client
|
3
|
+
module Model
|
4
|
+
module Admin
|
5
|
+
class GeoTarget < Common::GeoTarget
|
6
|
+
include Base
|
7
|
+
|
8
|
+
def self.query(parameters)
|
9
|
+
response = self.connection.get("geo_targets", parameters)
|
10
|
+
response.map{|hash| self.new(hash[superclass.resource_name])}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Neuron
|
2
|
+
module Client
|
3
|
+
module Model
|
4
|
+
class Base
|
5
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id|^class$)/ }
|
6
|
+
|
7
|
+
def initialize(attrs=nil)
|
8
|
+
@proxied_model = self.class.class_to_proxy.new(attrs)
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(meth, *args, &block)
|
12
|
+
(@proxied_model.methods.include?(meth) ? @proxied_model.send(meth, *args, &block) : super)
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :api
|
17
|
+
def api
|
18
|
+
@api || Neuron::Client::API.default_api
|
19
|
+
end
|
20
|
+
|
21
|
+
def connection
|
22
|
+
api.connection
|
23
|
+
end
|
24
|
+
|
25
|
+
def class_to_proxy
|
26
|
+
module_to_load = api.connection_type.to_s.titleize
|
27
|
+
class_name_to_load = name.split('::').last
|
28
|
+
Neuron::Client::Model.const_get(module_to_load).const_get(class_name_to_load)
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(meth, *args, &block)
|
32
|
+
(class_to_proxy.methods.include?(meth) ? class_to_proxy.send(meth, *args, &block) : super)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Neuron
|
2
|
+
module Client
|
3
|
+
module Model
|
4
|
+
module Common
|
5
|
+
class Ad
|
6
|
+
include Base
|
7
|
+
include AdCalculations
|
8
|
+
|
9
|
+
resource_name("ad")
|
10
|
+
resources_name("ads")
|
11
|
+
|
12
|
+
attr_accessor :name, :approved, :response_type, :parameters, :geo_target_ids, :ad_trackers,
|
13
|
+
:ad_trackers_attributes, :trackers,
|
14
|
+
# redirect
|
15
|
+
:redirect_url,
|
16
|
+
# video
|
17
|
+
:video_api_url, :video_setup_xml, :video_flv_url,
|
18
|
+
:video_clickthru_url, :video_companion_ad_html, :social_links, :social_links_attributes,
|
19
|
+
# caps
|
20
|
+
:frequency_cap_limit, :frequency_cap_window, :overall_cap,
|
21
|
+
:daily_cap, :day_partitions, :ideal_impressions_per_hour,
|
22
|
+
# range
|
23
|
+
:start_datetime, :end_datetime, :time_zone,
|
24
|
+
# timestamps
|
25
|
+
:created_at, :updated_at
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def stringify_day_partitions(days)
|
29
|
+
result = ""
|
30
|
+
168.times do |i|
|
31
|
+
result << (days[i.to_s] || "F")
|
32
|
+
end
|
33
|
+
result
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,329 @@
|
|
1
|
+
module Neuron
|
2
|
+
module Client
|
3
|
+
module Model
|
4
|
+
module Common
|
5
|
+
module AdCalculations
|
6
|
+
# This module expects the following methods to be defined:
|
7
|
+
# start_datetime (Time, String, or nil)
|
8
|
+
# end_datetime (Time, String, or nil)
|
9
|
+
# time_zone (String)
|
10
|
+
# day_partitions (String, or nil, length = 7*24, matches /^[TF]+$/)
|
11
|
+
# daily_cap (FixNum, or nil)
|
12
|
+
# overall_cap (FixNum, or nil)
|
13
|
+
# ideal_impressions_per_hour (Number, or nil)
|
14
|
+
# total_impressed (Integer, >= 0)
|
15
|
+
# today_impressed (Integer, >= 0)
|
16
|
+
|
17
|
+
def active?
|
18
|
+
calculate_active?(Time.now, total_impressed, today_impressed)
|
19
|
+
end
|
20
|
+
|
21
|
+
def pressure
|
22
|
+
calculate_pressure(Time.now, total_impressed, today_impressed)
|
23
|
+
end
|
24
|
+
|
25
|
+
def calculate_active?(time, total, today)
|
26
|
+
time = time.in_time_zone(time_zone)
|
27
|
+
return false unless within_date_range?(time)
|
28
|
+
return false if partitioned? && !partitioned_hour?(time)
|
29
|
+
!cap_met?(total, today)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set the optional "active" parameter to true or false if you already know
|
33
|
+
def calculate_pressure(time, total, today, active=nil)
|
34
|
+
return nil if active == false
|
35
|
+
return nil unless active || calculate_active?(time,total,today)
|
36
|
+
time = time.in_time_zone(time_zone)
|
37
|
+
if daily_capped?
|
38
|
+
if daily_cap_precludes_overall_cap?(time,total,today)
|
39
|
+
calculate_overall_pressure(time, total)
|
40
|
+
else
|
41
|
+
calculate_today_pressure(time, today)
|
42
|
+
end
|
43
|
+
elsif overall_pressure_exists?
|
44
|
+
calculate_overall_pressure(time, total)
|
45
|
+
else
|
46
|
+
(ideal_impressions_per_hour || 1.0).to_f
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def in_ad_time_zone(time)
|
53
|
+
if time.present?
|
54
|
+
(time.is_a?(String) ? Time.parse(time) : time).in_time_zone(time_zone)
|
55
|
+
end
|
56
|
+
rescue
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def start_in_time_zone
|
61
|
+
in_ad_time_zone(start_datetime)
|
62
|
+
end
|
63
|
+
|
64
|
+
def end_in_time_zone
|
65
|
+
in_ad_time_zone(end_datetime)
|
66
|
+
end
|
67
|
+
|
68
|
+
def daily_capped?
|
69
|
+
!daily_cap.nil? && daily_cap >= 0
|
70
|
+
end
|
71
|
+
|
72
|
+
def overall_capped?
|
73
|
+
!overall_cap.nil? && overall_cap >= 0
|
74
|
+
end
|
75
|
+
|
76
|
+
def overall_pressure_exists?
|
77
|
+
overall_capped? && end_datetime.present?
|
78
|
+
end
|
79
|
+
|
80
|
+
def partitioned?
|
81
|
+
!day_partitions.nil? && day_partitions.length == 24*7
|
82
|
+
end
|
83
|
+
|
84
|
+
def within_date_range?(time)
|
85
|
+
return false if start_datetime.present? && time < start_in_time_zone
|
86
|
+
return false if end_datetime.present? && time >= end_in_time_zone
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
# Assume time is in the time zone of the ad.
|
91
|
+
# Doesn't worry about whether or not the time is in the ad's date range.
|
92
|
+
# Assumes ad is partitioned.
|
93
|
+
def partitioned_hour?(time)
|
94
|
+
day_partitions[(time.wday * 24) + time.hour] == 'T'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Assume time is in the time zone of the ad.
|
98
|
+
# Doesn't worry about whether or not the time is in the ad's date range.
|
99
|
+
# Assumes ad is partitioned.
|
100
|
+
# Returns true if there is any active partition the day of the given time.
|
101
|
+
def partitioned_day?(time)
|
102
|
+
day_partitions[(time.wday) * 24, 24].include?('T')
|
103
|
+
end
|
104
|
+
|
105
|
+
# Assumes time is in the time zone of the ad.
|
106
|
+
# Doesn't worry about date range or day partitions.
|
107
|
+
def cap_met?(total_impressed, today_impressed)
|
108
|
+
return true if daily_capped? && (daily_cap <= today_impressed)
|
109
|
+
return true if overall_capped? && (overall_cap <= total_impressed)
|
110
|
+
false
|
111
|
+
end
|
112
|
+
|
113
|
+
# Assumes time is in time_zone of the ad.
|
114
|
+
# Returns true if there's no way we can hit the overall cap
|
115
|
+
# without busting the daily cap.
|
116
|
+
def daily_cap_precludes_overall_cap?(time,total_impressed,today_impressed)
|
117
|
+
return false unless daily_capped? && overall_capped?
|
118
|
+
return false unless self.end_datetime.present?
|
119
|
+
a = remaining_impressions_via_daily_cap(time, today_impressed)
|
120
|
+
b = remaining_impressions_via_overall_cap(total_impressed)
|
121
|
+
a < b
|
122
|
+
end
|
123
|
+
|
124
|
+
def remaining_impressions_via_overall_cap(total_impressed)
|
125
|
+
[overall_cap - total_impressed, 0].max
|
126
|
+
end
|
127
|
+
|
128
|
+
def remaining_impressions_today(today_impressed)
|
129
|
+
[daily_cap - today_impressed, 0].max
|
130
|
+
end
|
131
|
+
|
132
|
+
def remaining_impressions_via_daily_cap(time, today_impressed)
|
133
|
+
days = remaining_days(time) - 1 # exclude today
|
134
|
+
daily_cap * days + remaining_impressions_today(today_impressed)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Assumes time is in time_zone of the ad.
|
138
|
+
# Assume daily_cap is present.
|
139
|
+
def calculate_today_pressure(time, today_impressed)
|
140
|
+
hours = remaining_hours_today(time)
|
141
|
+
if hours <= 0
|
142
|
+
0.0
|
143
|
+
else
|
144
|
+
impressions = remaining_impressions_today(today_impressed)
|
145
|
+
2 * impressions / hours.to_f
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Assumes time is in time_zone of the ad.
|
150
|
+
# Assume overall_cap is present.
|
151
|
+
# Assume end_datetime is present.
|
152
|
+
def calculate_overall_pressure(time, total_impressed)
|
153
|
+
hours = remaining_hours(time)
|
154
|
+
if hours <= 0
|
155
|
+
0.0
|
156
|
+
else
|
157
|
+
impressions = remaining_impressions_via_overall_cap(total_impressed)
|
158
|
+
impressions / hours.to_f
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# remaining_days : the total integer number of days that an ad will run
|
163
|
+
# (even for part of the day) between now and the
|
164
|
+
# end_datetime.
|
165
|
+
# Assume all dates/times are in the time_zone of the ad.
|
166
|
+
# Assume end_datetime is present.
|
167
|
+
def remaining_days(time)
|
168
|
+
ending = end_in_time_zone
|
169
|
+
beginning = [time, start_in_time_zone].compact.max
|
170
|
+
return 0 unless beginning < ending
|
171
|
+
if partitioned?
|
172
|
+
end_of_beginning_week = beginning_of_week(beginning) + 7.days
|
173
|
+
beginning_of_end_week = beginning_of_week(ending)
|
174
|
+
|
175
|
+
if end_of_beginning_week < beginning_of_end_week
|
176
|
+
head_days = partitioned_days(beginning, end_of_beginning_week)
|
177
|
+
tail_days = partitioned_days(beginning_of_end_week, ending)
|
178
|
+
whole_weeks = (beginning_of_end_week-end_of_beginning_week) / 7.days
|
179
|
+
head_days + (whole_weeks * days_per_week) + tail_days
|
180
|
+
else
|
181
|
+
partitioned_days(beginning, ending)
|
182
|
+
end
|
183
|
+
else
|
184
|
+
(ending.beginning_of_day.to_datetime -
|
185
|
+
beginning.beginning_of_day.to_datetime) + 1
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# remaining_hours : the total number of hours that an ad will run from now
|
190
|
+
# to the end_datetime, taking day parting into account.
|
191
|
+
# Assume all dates/times are in the time_zone of the ad.
|
192
|
+
def remaining_hours(time)
|
193
|
+
ending = end_in_time_zone
|
194
|
+
beginning = [time, start_in_time_zone].compact.max
|
195
|
+
return 0 unless beginning < ending
|
196
|
+
if partitioned?
|
197
|
+
|
198
|
+
end_of_beginning_week = beginning_of_week(beginning) + 7.days
|
199
|
+
beginning_of_end_week = beginning_of_week(ending)
|
200
|
+
|
201
|
+
if end_of_beginning_week < beginning_of_end_week
|
202
|
+
head_hours = partitioned_hours(beginning, end_of_beginning_week)
|
203
|
+
tail_hours = partitioned_hours(beginning_of_end_week, ending)
|
204
|
+
whole_weeks = (beginning_of_end_week-end_of_beginning_week) / 7.days
|
205
|
+
head_hours + (whole_weeks * hours_per_week) + tail_hours
|
206
|
+
else
|
207
|
+
partitioned_hours(beginning, ending)
|
208
|
+
end
|
209
|
+
else
|
210
|
+
actual_hours(beginning, ending)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# remaining_hours_today : the total number of hours that an ad will run
|
215
|
+
# between now and the end of the day, taking day
|
216
|
+
# parting into account.
|
217
|
+
# Assume all dates/times are in the time_zone of the ad.
|
218
|
+
def remaining_hours_today(time)
|
219
|
+
beginning = [time, start_in_time_zone].compact.max
|
220
|
+
ending = [time.beginning_of_day + 1.day, end_in_time_zone].compact.min
|
221
|
+
return 0 unless beginning < ending
|
222
|
+
if partitioned?
|
223
|
+
partitioned_hours(beginning, ending)
|
224
|
+
else
|
225
|
+
actual_hours(beginning, ending)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Assume time is in the time_zone of the ad.
|
230
|
+
# Assume a week begins on Sunday.
|
231
|
+
def beginning_of_week(time)
|
232
|
+
recent_monday = time.monday
|
233
|
+
sunday_before_recent_monday = recent_monday - 1.day
|
234
|
+
if (time - sunday_before_recent_monday) < 7.days
|
235
|
+
sunday_before_recent_monday
|
236
|
+
else
|
237
|
+
sunday_before_recent_monday + 7.days
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def beginning_of_hour(time)
|
242
|
+
time.change(:min => 0, :sec => 0, :usec => 0)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Assume beginning and ending are in the ad's time_zone.
|
246
|
+
# Assume beginning and ending are within the ad's date range.
|
247
|
+
# Assume ad is partitioned.
|
248
|
+
def partitioned_hours(beginning, ending)
|
249
|
+
return 0 unless beginning < ending
|
250
|
+
total = 0.0
|
251
|
+
this_hour = beginning_of_hour(beginning)
|
252
|
+
last_hour = beginning_of_hour(ending)
|
253
|
+
if this_hour.to_i == last_hour.to_i
|
254
|
+
if partitioned_hour?(this_hour)
|
255
|
+
total = actual_hours(beginning, ending)
|
256
|
+
end
|
257
|
+
else
|
258
|
+
next_hour = this_hour + 1.hour
|
259
|
+
if partitioned_hour?(this_hour)
|
260
|
+
total += actual_hours(beginning, next_hour)
|
261
|
+
end
|
262
|
+
each_hour(next_hour, last_hour) do |hour|
|
263
|
+
total += 1 if partitioned_hour?(hour)
|
264
|
+
end
|
265
|
+
if partitioned_hour?(last_hour)
|
266
|
+
total += actual_hours(last_hour, ending)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
total
|
270
|
+
end
|
271
|
+
|
272
|
+
# Assume beginning and ending are in the ad's time_zone.
|
273
|
+
# Assume beginning and ending are within the ad's date range.
|
274
|
+
# Assume ad is partitioned.
|
275
|
+
# Assume beginning and ending are within a week of each other.
|
276
|
+
# Computes integer number of days where any part of the day is active.
|
277
|
+
def partitioned_days(beginning, ending)
|
278
|
+
return 0 unless beginning < ending
|
279
|
+
|
280
|
+
end_of_beginning_day = beginning.beginning_of_day + 1.day
|
281
|
+
beginning_of_end_day = ending.beginning_of_day
|
282
|
+
|
283
|
+
if end_of_beginning_day < beginning_of_end_day
|
284
|
+
days = 0
|
285
|
+
days += 1 if partitioned_hours(beginning, end_of_beginning_day) > 0
|
286
|
+
days += 1 if partitioned_hours(beginning_of_end_day, ending) > 0
|
287
|
+
each_day(end_of_beginning_day, beginning_of_end_day) do |day|
|
288
|
+
days += 1 if partitioned_day?(day)
|
289
|
+
end
|
290
|
+
days
|
291
|
+
else
|
292
|
+
(partitioned_hours(beginning,ending) > 0) ? 1 : 0
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def each_day(beginning, ending)
|
297
|
+
time = beginning.clone
|
298
|
+
while (time < ending)
|
299
|
+
yield time
|
300
|
+
time += 1.day
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def each_hour(beginning, ending)
|
305
|
+
time = beginning.clone
|
306
|
+
while(time < ending)
|
307
|
+
yield time
|
308
|
+
time += 1.hour
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def actual_hours(beginning, ending)
|
313
|
+
(ending.to_f - beginning.to_f) / 3600
|
314
|
+
end
|
315
|
+
|
316
|
+
# Assumes ad is partitioned.
|
317
|
+
def days_per_week
|
318
|
+
(0..6).map{|d| day_partitions[d*24,24].include?('T') ? 1 : 0 }.sum
|
319
|
+
end
|
320
|
+
|
321
|
+
# Assumes ad is partitioned.
|
322
|
+
def hours_per_week
|
323
|
+
day_partitions.count("T")
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|