neuron-client 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/README.md +34 -8
  2. data/lib/neuron-client.rb +61 -12
  3. data/lib/neuron-client/{connection.rb → admin_connection.rb} +7 -7
  4. data/lib/neuron-client/api.rb +48 -31
  5. data/lib/neuron-client/membase_connection.rb +18 -0
  6. data/lib/neuron-client/model/admin/ad.rb +22 -0
  7. data/lib/neuron-client/model/admin/ad_zone.rb +15 -0
  8. data/lib/neuron-client/model/admin/base.rb +91 -0
  9. data/lib/neuron-client/model/admin/blocked_referer.rb +12 -0
  10. data/lib/neuron-client/model/admin/blocked_user_agent.rb +12 -0
  11. data/lib/neuron-client/model/admin/geo_target.rb +16 -0
  12. data/lib/neuron-client/model/admin/report.rb +15 -0
  13. data/lib/neuron-client/model/admin/s3_file.rb +12 -0
  14. data/lib/neuron-client/model/admin/zone.rb +15 -0
  15. data/lib/neuron-client/model/base.rb +38 -0
  16. data/lib/neuron-client/model/common/ad.rb +40 -0
  17. data/lib/neuron-client/model/common/ad_calculations.rb +329 -0
  18. data/lib/neuron-client/model/common/ad_zone.rb +17 -0
  19. data/lib/neuron-client/model/common/base.rb +67 -0
  20. data/lib/neuron-client/model/common/blocked_referer.rb +16 -0
  21. data/lib/neuron-client/model/common/blocked_user_agent.rb +16 -0
  22. data/lib/neuron-client/model/common/geo_target.rb +16 -0
  23. data/lib/neuron-client/model/common/report.rb +21 -0
  24. data/lib/neuron-client/model/common/s3_file.rb +16 -0
  25. data/lib/neuron-client/model/common/zone.rb +22 -0
  26. data/lib/neuron-client/model/common/zone_calculations.rb +41 -0
  27. data/lib/neuron-client/model/membase/ad.rb +31 -0
  28. data/lib/neuron-client/model/membase/ad_zone.rb +11 -0
  29. data/lib/neuron-client/model/membase/blocked_referer.rb +18 -0
  30. data/lib/neuron-client/model/membase/blocked_user_agent.rb +18 -0
  31. data/lib/neuron-client/model/membase/geo_target.rb +11 -0
  32. data/lib/neuron-client/model/membase/report.rb +11 -0
  33. data/lib/neuron-client/model/membase/s3_file.rb +11 -0
  34. data/lib/neuron-client/model/membase/zone.rb +19 -0
  35. data/lib/neuron-client/model/models.rb +14 -0
  36. data/lib/neuron-client/version.rb +1 -1
  37. data/neuron-client.gemspec +18 -11
  38. data/spec/fixtures/vcr_cassettes/s3_file.yml +186 -4
  39. data/spec/lib/admin_connection_spec.rb +82 -0
  40. data/spec/lib/api_spec.rb +80 -0
  41. data/spec/lib/membase_connection_spec.rb +27 -0
  42. data/spec/lib/model/admin/ad_spec.rb +34 -0
  43. data/spec/lib/model/admin/ad_zone_spec.rb +19 -0
  44. data/spec/lib/model/admin/base_spec.rb +11 -0
  45. data/spec/lib/model/admin/blocked_referer_spec.rb +11 -0
  46. data/spec/lib/model/admin/blocked_user_agent_spec.rb +11 -0
  47. data/spec/lib/model/admin/geo_target_spec.rb +30 -0
  48. data/spec/lib/model/admin/report_spec.rb +21 -0
  49. data/spec/lib/model/admin/s3_spec.rb +11 -0
  50. data/spec/lib/model/admin/zone_spec.rb +21 -0
  51. data/spec/lib/model/base_spec.rb +89 -0
  52. data/spec/lib/model/common/ad_calculations_spec.rb +1148 -0
  53. data/spec/lib/model/common/ad_spec.rb +11 -0
  54. data/spec/lib/model/common/ad_zone_spec.rb +11 -0
  55. data/spec/lib/model/common/base_spec.rb +11 -0
  56. data/spec/lib/model/common/blocked_referer_spec.rb +11 -0
  57. data/spec/lib/model/common/blocked_user_agent_spec.rb +11 -0
  58. data/spec/lib/model/common/geo_target_spec.rb +11 -0
  59. data/spec/lib/model/common/report_spec.rb +11 -0
  60. data/spec/lib/model/common/s3_spec.rb +11 -0
  61. data/spec/lib/model/common/zone_calculations_spec.rb +54 -0
  62. data/spec/lib/model/common/zone_spec.rb +11 -0
  63. data/spec/lib/model/membase/ad_spec.rb +50 -0
  64. data/spec/lib/model/membase/ad_zone_spec.rb +11 -0
  65. data/spec/lib/model/membase/base_spec.rb +11 -0
  66. data/spec/lib/model/membase/blocked_referer_spec.rb +30 -0
  67. data/spec/lib/model/membase/blocked_user_agent_spec.rb +30 -0
  68. data/spec/lib/model/membase/geo_target_spec.rb +11 -0
  69. data/spec/lib/model/membase/report_spec.rb +11 -0
  70. data/spec/lib/model/membase/s3_spec.rb +11 -0
  71. data/spec/lib/model/membase/zone_spec.rb +28 -0
  72. data/spec/lib/old_spec.rb +192 -149
  73. data/spec/lib/s3_file_spec.rb +45 -42
  74. data/spec/spec_helper.rb +2 -1
  75. metadata +296 -57
  76. data/lib/neuron-client/ad.rb +0 -39
  77. data/lib/neuron-client/ad_zone.rb +0 -16
  78. data/lib/neuron-client/blocked_referer.rb +0 -12
  79. data/lib/neuron-client/blocked_user_agent.rb +0 -12
  80. data/lib/neuron-client/connected.rb +0 -138
  81. data/lib/neuron-client/geo_target.rb +0 -16
  82. data/lib/neuron-client/real_time_stats.rb +0 -0
  83. data/lib/neuron-client/report.rb +0 -20
  84. data/lib/neuron-client/s3_file.rb +0 -10
  85. data/lib/neuron-client/zone.rb +0 -16
@@ -0,0 +1,11 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ describe Base do
6
+
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ describe BlockedReferer do
6
+
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ describe BlockedUserAgent do
6
+
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ describe GeoTarget do
6
+ describe "self.query(parameters)" do
7
+ it "should call the expected methods and return the expected value" do
8
+ c = stub(:connection)
9
+ GeoTarget.should_receive(:connection).and_return(c)
10
+ p = stub(:parameters)
11
+ gta = stub(:geo_target_attributes)
12
+ gta2 = stub(:geo_target_attributes)
13
+ r = [
14
+ {'geo_target' => gta},
15
+ {'geo_target' => gta2}
16
+ ]
17
+ c.should_receive(:get).with('geo_targets', p).and_return(r)
18
+ g = stub(:geo_target)
19
+ g2 = stub(:geo_target2)
20
+ GeoTarget.should_receive(:new).with(gta).and_return(g)
21
+ GeoTarget.should_receive(:new).with(gta2).and_return(g2)
22
+
23
+ GeoTarget.query(p).should == [g, g2]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ describe Report do
6
+ describe "result" do
7
+ it "should call the expected methods and return the expected result" do
8
+ r = Report.allocate
9
+ c = stub(:connection)
10
+ Report.should_receive(:connection).and_return(c)
11
+ r.should_receive(:id).and_return(7)
12
+ c.should_receive(:get).with('reports/7/result', :format => '').and_return('result_value')
13
+
14
+ r.result.should == 'result_value'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ describe S3File do
6
+
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ describe Zone do
6
+ describe "unlink(ad_id)" do
7
+ it "should call the expected method and return the expected results" do
8
+ z = Zone.allocate
9
+ c = stub(:connection)
10
+ Zone.should_receive(:connection).and_return(c)
11
+ z.should_receive(:id).and_return(1)
12
+ c.should_receive(:delete).with('zones/1/ads/2').and_return('result_value')
13
+
14
+ z.unlink(2).should == 'result_value'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,89 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Mock
5
+ class BaseMockModel; end
6
+ end
7
+
8
+ class BaseMockModel < Base
9
+
10
+ end
11
+
12
+ describe Base do
13
+ describe "instance methods" do
14
+ describe "initialize(attrs=nil)" do
15
+ it "should set @proxied_model appropriately" do
16
+ pending "Not sure what the deal is here, it works outside of unit testing"
17
+
18
+ ctp = stub(:class_to_proxy)
19
+ Base.any_instance.should_receive(:class_to_proxy).and_return(ctp)
20
+ p = stub(:proxy)
21
+ ctp.should_receive(:new).with('attributes').and_return(p)
22
+ b = Base.new('attributes')
23
+
24
+ b.instance_variable_get(:@proxied_model).should == p
25
+ end
26
+ end
27
+
28
+ describe "method_missing(meth, *args, &block)" do
29
+ context "when method exists on the proxied model" do
30
+ it "should call the method on the proxied model"
31
+ end
32
+ context "when method does not exist on the proxied model" do
33
+ it "should call super.method_missing"
34
+ end
35
+ end
36
+ end
37
+
38
+ describe "class methods" do
39
+ describe "api" do
40
+ context "when @api exists" do
41
+ it "should return the expected value" do
42
+ a = stub(:api)
43
+ Base.instance_variable_set(:@api, a)
44
+
45
+ Base.api.should == a
46
+ end
47
+ end
48
+ context "when @api does not exist" do
49
+ it "should return the expected value" do
50
+ Base.instance_variable_set(:@api, nil)
51
+ a = stub(:default_api)
52
+ Neuron::Client::API.should_receive(:default_api).and_return(a)
53
+
54
+ Base.api.should == a
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "connection" do
60
+ it "should call the expected methods and return the expected result" do
61
+ a = stub(:api)
62
+ Base.should_receive(:api).and_return(a)
63
+ c = stub(:connection)
64
+ a.should_receive(:connection).and_return(c)
65
+
66
+ Base.connection.should == c
67
+ end
68
+ end
69
+
70
+ describe "class_to_proxy" do
71
+ it "should call the expected methods and return the expected result" do
72
+ BaseMockModel.stub_chain(:api, :connection_type).and_return(:mock)
73
+ BaseMockModel.class_to_proxy.should == Neuron::Client::Model::Mock::BaseMockModel
74
+ end
75
+ end
76
+
77
+ describe "method_missing(meth, *args, &block)" do
78
+ context "when method exists on the class to proxy" do
79
+ it "should call the method on the proxied model"
80
+ end
81
+ context "when method does not exist on the class to proxy" do
82
+ it "should call super.method_missing"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,1148 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Common
5
+
6
+ class FakeAd
7
+ include AdCalculations
8
+ attr_accessor :start_datetime, :end_datetime, :time_zone, :day_partitions,
9
+ :daily_cap, :overall_cap, :ideal_impressions_per_hour, :total_impressed,
10
+ :today_impressed
11
+ def initialize(opts={})
12
+ opts.each { |sym, value| self.send("#{sym}=", value) }
13
+ self.time_zone ||= "Beijing"
14
+ self.total_impressed ||= 0
15
+ self.today_impressed ||= 0
16
+ end
17
+ end
18
+
19
+ describe AdCalculations do
20
+ context "given an Ad with date range Sat, 01 Jan 2011, 00:00 -> Tue, 01 Feb 2011, 12:00; Beijing time; Day parted for 9:00 - 17:00, M-F; Daily cap of 10; Overall cap of 100; total_impressed of 50; today_impressed of 5" do
21
+ before(:each) do
22
+ @ad = FakeAd.new(
23
+ :start_datetime => Time.new(2011,1,1,0,0,0,"+08:00"),
24
+ :end_datetime => Time.new(2011,2,1,12,0,0,"+08:00"),
25
+ :time_zone => "Beijing",
26
+ :day_partitions => "F"*24 + ("F"*9 + "T"*8 + "F"*7)*5 + "F"*24,
27
+ :daily_cap => 10,
28
+ :overall_cap => 100,
29
+ :ideal_impressions_per_hour => nil,
30
+ :total_impressed => 50,
31
+ :today_impressed => 5
32
+ )
33
+ end
34
+ it "should be inactive on December 10th, 2010" do
35
+ Timecop.freeze(Time.new(2010,12,10)) do
36
+ @ad.active?.should be_false
37
+ end
38
+ end
39
+ it "should be inactive on January 1st, 2011" do
40
+ Timecop.freeze(Time.new(2011,1,1,10,0,0,"+08:00")) do
41
+ @ad.active?.should be_false
42
+ end
43
+ end
44
+ it "should be inactive on January 3rd, 2011 at 5:00 AM" do
45
+ Timecop.freeze(Time.new(2011,1,3,5,0,0,"+08:00")) do
46
+ @ad.active?.should be_false
47
+ end
48
+ end
49
+ context "on January 3rd, 2011 at 10:00 AM" do
50
+ before(:each) { Timecop.freeze(Time.new(2011,1,3,10,0,0,"+08:00")) }
51
+ after(:each) { Timecop.return }
52
+ it "should be active" do
53
+ @ad.active?.should be_true
54
+ end
55
+ it "should have a pressure of 10/7" do
56
+ @ad.pressure.should == 10 / 7.0
57
+ end
58
+ end
59
+ context "on January 3rd, 2011 at 16:00" do
60
+ before(:each) { Timecop.freeze(Time.new(2011,1,3,16,0,0,"+08:00")) }
61
+ after(:each) { Timecop.return }
62
+ it "should be active" do
63
+ @ad.active?.should be_true
64
+ end
65
+ it "should have a pressure of 10/1" do
66
+ @ad.pressure.should == 10
67
+ end
68
+ end
69
+ context "on January 27th, 2011 at 16:00" do
70
+ before(:each) { Timecop.freeze(Time.new(2011,1,27,16,0,0,"+08:00")) }
71
+ after(:each) { Timecop.return }
72
+ it "should be active" do
73
+ @ad.active?.should be_true
74
+ end
75
+ it "should have a pressure of 50/20" do
76
+ @ad.pressure.should == 50 / 20.0
77
+ end
78
+ end
79
+ it "should be inactive on February 1st, 2011 at 13:00" do
80
+ Timecop.freeze(Time.new(2011,2,1,13,0,0,"+08:00")) do
81
+ @ad.active?.should be_false
82
+ end
83
+ end
84
+ end
85
+
86
+ describe ".active?" do
87
+ it "should call calculate_active?" do
88
+ @ad = FakeAd.new
89
+ @ad.should_receive(:calculate_active?).and_return(:result)
90
+
91
+ @ad.active?.should == :result
92
+ end
93
+ end
94
+
95
+ describe ".pressure" do
96
+ it "should call calculate_pressure" do
97
+ @ad = FakeAd.new
98
+ @ad.should_receive(:calculate_pressure).and_return(:result)
99
+
100
+ @ad.pressure.should == :result
101
+ end
102
+ end
103
+
104
+ describe ".calculate_active?(time, total_impressed, today_impressed)" do
105
+ it "should have specs"
106
+ end
107
+
108
+ describe ".calculate_pressure(time, total_impressed, today_impressed, active=nil)" do
109
+ it "should have some specs related to when 'active' is passed in"
110
+ context "when daily_cap is present" do
111
+ context "and daily_cap precludes overall_cap from being met" do
112
+ it "should calculate the overall pressure" do
113
+ ad = FakeAd.new(:daily_cap => 10)
114
+ ad.stub(:daily_cap_precludes_overall_cap?).and_return(true)
115
+ ad.should_receive(:calculate_overall_pressure)
116
+ ad.should_not_receive(:calculate_today_pressure)
117
+
118
+ ad.calculate_pressure(Time.now, 50, 5)
119
+ end
120
+ end
121
+ context "and daily_cap does not preclude the overall_cap from being met" do
122
+ it "should calculate the pressure for just today" do
123
+ ad = FakeAd.new(:daily_cap => 10)
124
+ ad.stub(:daily_cap_precludes_overall_cap?).and_return(false)
125
+ ad.should_not_receive(:calculate_overall_pressure)
126
+ ad.should_receive(:calculate_today_pressure)
127
+
128
+ ad.calculate_pressure(Time.now, 50, 5)
129
+ end
130
+ end
131
+ end
132
+ context "when daily_cap is not present" do
133
+ context "when overall_cap is blank or end_datetime is blank" do
134
+ it "should use the ideal_impressions_per_hour" do
135
+ ad = FakeAd.new(:overall_cap => 100, :ideal_impressions_per_hour => 99.9)
136
+ ad.should_not_receive(:calculate_overall_pressure)
137
+ ad.should_not_receive(:calculate_today_pressure)
138
+
139
+ ad.calculate_pressure(Time.now, 50, 5).should == 99.9
140
+ end
141
+ end
142
+ context "when overall_cap is not blank and end_datetime is not blank" do
143
+ it "should calculate the overall pressure" do
144
+ ad = FakeAd.new(:overall_cap => 100, :end_datetime => Time.now + 1.month)
145
+ ad.should_receive(:calculate_overall_pressure)
146
+ ad.should_not_receive(:calculate_today_pressure)
147
+
148
+ ad.calculate_pressure(Time.now, 50, 5)
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+
155
+ describe ".start_in_time_zone" do
156
+ it "should return start_datetime, time zone adjusted" do
157
+ result = FakeAd.new(
158
+ :start_datetime => Time.utc(2011, 2, 1, 1, 59),
159
+ :time_zone => 'Eastern Time (US & Canada)'
160
+ ).send(:start_in_time_zone)
161
+ formatted_result = "#{result.time.strftime('%a, %d %b %Y %H:%M:%S')} #{result.zone} #{result.formatted_offset}"
162
+
163
+ formatted_result.should == "Mon, 31 Jan 2011 20:59:00 EST -05:00"
164
+ end
165
+ end
166
+
167
+ describe ".end_in_time_zone" do
168
+ context "when end_datetime is present" do
169
+ it "should return end_datetime, time zone adjusted" do
170
+ result = FakeAd.new(
171
+ :end_datetime => Time.utc(2011, 2, 1, 1, 59),
172
+ :time_zone => 'Eastern Time (US & Canada)'
173
+ ).send(:end_in_time_zone)
174
+ formatted_result = "#{result.time.strftime('%a, %d %b %Y %H:%M:%S')} #{result.zone} #{result.formatted_offset}"
175
+
176
+ formatted_result.should == "Mon, 31 Jan 2011 20:59:00 EST -05:00"
177
+ end
178
+ end
179
+ context "when end_datetime is not present" do
180
+ it "should return nil" do
181
+ FakeAd.new(:end_datetime => nil).send(:end_in_time_zone).should be_nil
182
+ end
183
+ end
184
+ end
185
+
186
+ describe ".daily_capped?" do
187
+ it "should be false when daily_cap is nil" do
188
+ FakeAd.new(:daily_cap => nil).send(:daily_capped?).should be_false
189
+ end
190
+ it "should be true when daily_cap is a positive integer > zero" do
191
+ FakeAd.new(:daily_cap => 42).send(:daily_capped?).should be_true
192
+ end
193
+ end
194
+
195
+ describe ".overall_capped?" do
196
+ it "should be false when overall_cap is nil" do
197
+ FakeAd.new(:overall_cap => nil).send(:overall_capped?).should be_false
198
+ end
199
+ it "should be true when overall_cap is a positive integer > zero" do
200
+ FakeAd.new(:overall_cap => 42).send(:overall_capped?).should be_true
201
+ end
202
+ end
203
+
204
+ describe ".partitioned?" do
205
+ it "should be false when day_partitions is nil" do
206
+ FakeAd.new(:day_partitions => nil).send(:partitioned?).should be_false
207
+ end
208
+ it "should be true when day_partitions is 168 character string of Ts and Fs." do
209
+ p = ("T"*100 + "F"*68).split('').shuffle.join
210
+ FakeAd.new(:day_partitions => p).send(:partitioned?).should be_true
211
+ end
212
+ end
213
+
214
+ def within_date_range
215
+ FakeAd.new(:start_datetime => @start, :end_datetime => @end).send(:within_date_range?, @now)
216
+ end
217
+
218
+ describe ".within_date_range?(date)" do
219
+ before(:all) do
220
+ @now = Time.now
221
+ end
222
+ context "when ad has no start date" do
223
+ context "when ad has no end date" do
224
+ it "should return true" do
225
+ @start = nil
226
+ @end = nil
227
+ within_date_range.should be_true
228
+ end
229
+ end
230
+ context "when before ad's end date" do
231
+ it "should return true" do
232
+ @start = nil
233
+ @end = @now + 1.day
234
+ within_date_range.should be_true
235
+ end
236
+ end
237
+ context "when on or beyond ad's end date" do
238
+ it "should return false" do
239
+ @start = nil
240
+ @end = @now
241
+ within_date_range.should be_false
242
+ @end = @now - 1.day
243
+ within_date_range.should be_false
244
+ end
245
+ end
246
+ end
247
+ context "when before ad's start date" do
248
+ context "when ad has no end date" do
249
+ it "should return false" do
250
+ @start = @now + 1.day
251
+ @end = nil
252
+ within_date_range.should be_false
253
+ end
254
+ end
255
+ context "when before ad's end date" do
256
+ it "should return false" do
257
+ @start = @now + 1.day
258
+ @end = @start + 1.day
259
+ within_date_range.should be_false
260
+ end
261
+ end
262
+ end
263
+ context "when on or beyond ad's start date" do
264
+ context "when ad has no end date" do
265
+ it "should return true" do
266
+ @start = @now
267
+ @end = nil
268
+ within_date_range.should be_true
269
+ @start = @now - 1.day
270
+ within_date_range.should be_true
271
+ end
272
+ end
273
+ context "when before ad's end date" do
274
+ it "should return true" do
275
+ @start = @now
276
+ @end = @now + 1.day
277
+ within_date_range.should be_true
278
+ @start = @now - 1.day
279
+ within_date_range.should be_true
280
+ end
281
+ end
282
+ context "when on or beyond ad's end date" do
283
+ it "should return false" do
284
+ @start = @now
285
+ @end = @now
286
+ within_date_range.should be_false
287
+ @start = @now - 2.days
288
+ @end = @now
289
+ within_date_range.should be_false
290
+ @end = @now - 1.day
291
+ within_date_range.should be_false
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ describe ".partitioned_hour?(time)" do
298
+ context "when day partitions exist" do
299
+ before(:all) do
300
+ @ad = FakeAd.new(:day_partitions => [
301
+ "TTTTTTTTTTFTTTTTTTTTTTTT", # Sunday
302
+ "TTTTTTTTTTTTTTTFTTTTTTTT", # Monday
303
+ "FTTTTTTTTTTTTTTTTTTTTTTT", # Tuesday
304
+ "TTTTTFTTTTTTTTTTTTTTTTTT", # Wednesday
305
+ "TTTTTTTTTTTTTTTTTTTTFTTT", # Thursday
306
+ "TTTTTTTTTTTTTTTTTTTTTTTF", # Friday
307
+ "TTTTTTTTTTTTFTTTTTTTTTTT" # Saturday
308
+ ].join)
309
+ end
310
+ context "when on Sunday" do
311
+ context "when on day partition" do
312
+ it "should return true" do
313
+ time = Chronic.parse('Sunday @ 9 am')
314
+ @ad.send(:partitioned_hour?, time).should be_true
315
+ end
316
+ end
317
+ context "when not on day partition" do
318
+ it "should return false" do
319
+ time = Chronic.parse('Sunday @ 10 am')
320
+ @ad.send(:partitioned_hour?, time).should be_false
321
+ end
322
+ end
323
+ end
324
+ context "when on Monday" do
325
+ context "when on day partition" do
326
+ it "should return true" do
327
+ time = Chronic.parse('Monday @ 2 pm')
328
+ @ad.send(:partitioned_hour?, time).should be_true
329
+ end
330
+ end
331
+ context "when not on day partition" do
332
+ it "should return false" do
333
+ time = Chronic.parse('Monday @ 3 pm')
334
+ @ad.send(:partitioned_hour?, time).should be_false
335
+ end
336
+ end
337
+ end
338
+ context "when on Friday" do
339
+ context "when on day partition" do
340
+ it "should return true" do
341
+ time = Chronic.parse('Friday @ 10 pm')
342
+ @ad.send(:partitioned_hour?, time).should be_true
343
+ end
344
+ end
345
+ context "when not on day partition" do
346
+ it "should return false" do
347
+ time = Chronic.parse('Friday @ 11 pm')
348
+ @ad.send(:partitioned_hour?, time).should be_false
349
+ end
350
+ end
351
+ end
352
+ context "when on Saturday" do
353
+ context "when on day partition" do
354
+ it "should return true" do
355
+ time = Chronic.parse('Saturday @ 1 pm')
356
+ @ad.send(:partitioned_hour?, time).should be_true
357
+ end
358
+ end
359
+ context "when not on day partition" do
360
+ it "should return false" do
361
+ time = Chronic.parse('Saturday @ 12 pm')
362
+ @ad.send(:partitioned_hour?, time).should be_false
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end
368
+
369
+ def partitioned_day?(time)
370
+ nine_to_five = "F"*9 + "T"*8 + "F"*7
371
+ no_hours = "F" * 24
372
+ ad = FakeAd.new(:day_partitions => [nine_to_five, nine_to_five, no_hours, nine_to_five, nine_to_five, nine_to_five, no_hours].join)
373
+ ad.send(:partitioned_day?, time)
374
+ end
375
+ describe ".partitioned_day?(time)" do
376
+ context "when ad is partitioned from 9-5, every day of the week except Tuesday and Saturday" do
377
+ it "should return true on Sunday @ 5 am" do
378
+ partitioned_day?( Chronic.parse('Sunday @ 5 am') ).should be_true
379
+ end
380
+ it "should return true on Sunday @ 10 am" do
381
+ partitioned_day?( Chronic.parse('Sunday @ 10 am') ).should be_true
382
+ end
383
+ it "should return true on Sunday @ 10 pm" do
384
+ partitioned_day?( Chronic.parse('Sunday @ 10 pm') ).should be_true
385
+ end
386
+ it "should return true on Monday @ 5 am" do
387
+ partitioned_day?( Chronic.parse('Monday @ 5 am') ).should be_true
388
+ end
389
+ it "should return false on Tuesday @ 5 am" do
390
+ partitioned_day?( Chronic.parse('Tuesday @ 5 am') ).should be_false
391
+ end
392
+ it "should return false on Tuesday @ 10 am" do
393
+ partitioned_day?( Chronic.parse('Tuesday @ 10 am') ).should be_false
394
+ end
395
+ it "should return true on Wednesday @ 5 am" do
396
+ partitioned_day?( Chronic.parse('Wednesday @ 5 am') ).should be_true
397
+ end
398
+ it "should return true on Thursday @ 5 am" do
399
+ partitioned_day?( Chronic.parse('Thursday @ 5 am') ).should be_true
400
+ end
401
+ it "should return true on Friday @ 5 am" do
402
+ partitioned_day?( Chronic.parse('Friday @ 5 am') ).should be_true
403
+ end
404
+ it "should return false on Saturday @ 5 am" do
405
+ partitioned_day?( Chronic.parse('Saturday @ 5 am') ).should be_false
406
+ end
407
+ end
408
+ end
409
+
410
+ describe ".cap_met?(total_impressed, today_impressed)" do
411
+ context "when not daily capped" do
412
+ context "and not overall capped" do
413
+ it "should return false" do
414
+ FakeAd.new(:daily_cap => nil, :overall_cap => nil).send(:cap_met?, 10, 10).should be_false
415
+ end
416
+ end
417
+ context "and overall cap is 10" do
418
+ before(:each) do
419
+ @ad = FakeAd.new(:daily_cap => nil, :overall_cap => 10)
420
+ end
421
+ context "and total_impressed = 9" do
422
+ it "should return false" do
423
+ @ad.send(:cap_met?, 9, 0).should be_false
424
+ end
425
+ end
426
+ context "and total_impressed = 10" do
427
+ it "should return true" do
428
+ @ad.send(:cap_met?, 10, 0).should be_true
429
+ end
430
+ end
431
+ context "and total_impressed = 11" do
432
+ it "should return true" do
433
+ @ad.send(:cap_met?, 11, 0).should be_true
434
+ end
435
+ end
436
+ end
437
+ end
438
+ context "when daily cap is 10" do
439
+ context "and not overall capped" do
440
+ before(:each) do
441
+ @ad = FakeAd.new(:daily_cap => 10, :overall_cap => nil)
442
+ end
443
+ context "and today_impressed = 9" do
444
+ it "should return false" do
445
+ @ad.send(:cap_met?, 0, 9).should be_false
446
+ end
447
+ end
448
+ context "and today_impressed = 10" do
449
+ it "should return true" do
450
+ @ad.send(:cap_met?, 0, 10).should be_true
451
+ end
452
+ end
453
+ context "and today_impressed = 11" do
454
+ it "should return true" do
455
+ @ad.send(:cap_met?, 0, 11).should be_true
456
+ end
457
+ end
458
+ end
459
+ context "and overall cap is 100" do
460
+ before(:each) do
461
+ @ad = FakeAd.new(:daily_cap => 10, :overall_cap => 100)
462
+ end
463
+ context "and total < 100, today < 10" do
464
+ it "should return false" do
465
+ @ad.send(:cap_met?, 99, 9).should be_false
466
+ end
467
+ end
468
+ context "and total < 100, today = 10" do
469
+ it "should return true" do
470
+ @ad.send(:cap_met?, 99, 10).should be_true
471
+ end
472
+ end
473
+ context "and total = 100, today < 10" do
474
+ it "should return true" do
475
+ @ad.send(:cap_met?, 100, 9).should be_true
476
+ end
477
+ end
478
+ context "and total = 100, today = 10" do
479
+ it "should return true" do
480
+ @ad.send(:cap_met?, 100, 10).should be_true
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end
486
+
487
+ def precluded
488
+ @time = Time.now
489
+ @total = 500
490
+ @today = 100
491
+ ad = FakeAd.new(:daily_cap => @daily_cap, :overall_cap => @overall_cap, :end_datetime => @end_datetime)
492
+ ad.stub(:remaining_impressions_via_daily_cap).with(@time, @today).and_return(@remaining_via_daily_cap)
493
+ ad.stub(:remaining_impressions_via_overall_cap).with(@total).and_return(@remaining_via_overall_cap)
494
+ ad.send(:daily_cap_precludes_overall_cap?, @time, @total, @today)
495
+ end
496
+ describe ".daily_cap_precludes_overall_cap?(time, total_impressed, today_impressed)" do
497
+ context "when ad has no daily cap" do
498
+ it "should return false" do
499
+ @daily_cap = nil
500
+ @overall_cap = 1
501
+ precluded.should be_false
502
+ end
503
+ end
504
+ context "when ad has no overall cap" do
505
+ it "should return false" do
506
+ @daily_cap = 1
507
+ @overall_cap = nil
508
+ precluded.should be_false
509
+ end
510
+ end
511
+ context "when ad has no end date/time" do
512
+ it "should return false" do
513
+ @daily_cap = 1
514
+ @overall_cap = 1
515
+ @end_datetime = nil
516
+ precluded.should be_false
517
+ end
518
+ end
519
+ context "when ad's daily cap is 10, overall cap is 100, and end datetime is present" do
520
+ before(:each) { @daily_cap = 10; @overall_cap = 100; @end_datetime = Time.now + 10.days }
521
+ context "when remaining_impressions_via_daily_cap < remaining_impressions_via_overall_cap" do
522
+ it "returns true" do
523
+ @remaining_via_daily_cap = 1
524
+ @remaining_via_overall_cap = 2
525
+ precluded.should be_true
526
+ end
527
+ end
528
+ context "when remaining_impressions_via_daily_cap = remaining_impressions_via_overall_cap" do
529
+ it "returns false" do
530
+ @remaining_via_daily_cap = 1
531
+ @remaining_via_overall_cap = 1
532
+ precluded.should be_false
533
+ end
534
+ end
535
+ context "when remaining_impressions_via_daily_cap > remaining_impressions_via_overall_cap" do
536
+ it "returns false" do
537
+ @remaining_via_daily_cap = 2
538
+ @remaining_via_overall_cap = 1
539
+ precluded.should be_false
540
+ end
541
+ end
542
+ end
543
+ end
544
+
545
+ describe ".remaining_impressions_via_overall_cap(total_impressed)" do
546
+ context "when overall_cap = 100" do
547
+ before(:each) { @ad = FakeAd.new(:overall_cap => 100) }
548
+ context "when total_impressed = 0" do
549
+ it "returns 100" do
550
+ @ad.send(:remaining_impressions_via_overall_cap, 0).should == 100
551
+ end
552
+ end
553
+ context "when total_impressed = 99" do
554
+ it "returns 1" do
555
+ @ad.send(:remaining_impressions_via_overall_cap, 99).should == 1
556
+ end
557
+ end
558
+ context "when total_impressed = 100" do
559
+ it "returns 0" do
560
+ @ad.send(:remaining_impressions_via_overall_cap, 100).should == 0
561
+ end
562
+ end
563
+ context "when total_impressed = 101" do
564
+ it "returns 0" do
565
+ @ad.send(:remaining_impressions_via_overall_cap, 101).should == 0
566
+ end
567
+ end
568
+ end
569
+ end
570
+
571
+ describe ".remaining_impressions_today(today_impressed)" do
572
+ context "when daily_cap = 100" do
573
+ before(:each) { @ad = FakeAd.new(:daily_cap => 100) }
574
+ context "when today_impressed = 0" do
575
+ it "returns 100" do
576
+ @ad.send(:remaining_impressions_today, 0).should == 100
577
+ end
578
+ end
579
+ context "when today_impressed = 99" do
580
+ it "returns 1" do
581
+ @ad.send(:remaining_impressions_today, 99).should == 1
582
+ end
583
+ end
584
+ context "when today_impressed = 100" do
585
+ it "returns 0" do
586
+ @ad.send(:remaining_impressions_today, 100).should == 0
587
+ end
588
+ end
589
+ context "when today_impressed = 101" do
590
+ it "returns 0" do
591
+ @ad.send(:remaining_impressions_today, 101).should == 0
592
+ end
593
+ end
594
+ end
595
+ end
596
+
597
+ describe ".remaining_impressions_via_daily_cap(time, today_impressed)" do
598
+ context "when daily_cap is 100, ad has 5 days remaining (including today), and 30 remaining impressions today" do
599
+ it "returns 430" do
600
+ ad = FakeAd.new(:daily_cap => 100)
601
+ ad.stub(:remaining_days).with(:time).and_return(5)
602
+ ad.stub(:remaining_impressions_today).with(:today_impressed).and_return(30)
603
+
604
+ ad.send(:remaining_impressions_via_daily_cap, :time, :today_impressed).should == 430
605
+ end
606
+ end
607
+ end
608
+
609
+ describe ".calculate_today_pressure(time, today_impressed)" do
610
+ context "when there are 3 hours and 12 impressions remaining today" do
611
+ it "should return a pressure of 8 (impressions/hr)" do
612
+ ad = FakeAd.new
613
+ ad.stub(:remaining_hours_today).with(:time).and_return(3)
614
+ ad.stub(:remaining_impressions_today).with(:today_impressed).and_return(12)
615
+
616
+ ad.send(:calculate_today_pressure, :time, :today_impressed).should == 8
617
+ end
618
+ end
619
+ context "when there are 0 hours and 12 impressions remaining today" do
620
+ it "should return a pressure of 0 (impressions/hr)" do
621
+ ad = FakeAd.new
622
+ ad.stub(:remaining_hours_today).with(:time).and_return(0)
623
+ ad.stub(:remaining_impressions_today).with(:today_impressed).and_return(12)
624
+
625
+ ad.send(:calculate_today_pressure, :time, :today_impressed).should == 0
626
+ end
627
+ end
628
+ end
629
+
630
+ describe ".calculate_overall_pressure(time, total_impressed)" do
631
+ context "when there are 3 hours and 12 impressions remaining overall" do
632
+ it "should return a pressure of 4 (impressions/hr)" do
633
+ ad = FakeAd.new
634
+ ad.stub(:remaining_hours).with(:time).and_return(3)
635
+ ad.stub(:remaining_impressions_via_overall_cap).with(:total_impressed).and_return(12)
636
+
637
+ ad.send(:calculate_overall_pressure, :time, :total_impressed).should == 4
638
+ end
639
+ end
640
+ context "when there are 0 hours and 12 impressions remaining overall" do
641
+ it "should return a pressure of 0 (impressions/hr)" do
642
+ ad = FakeAd.new
643
+ ad.stub(:remaining_hours).with(:time).and_return(0)
644
+ ad.stub(:remaining_impressions_via_overall_cap).with(:total_impressed).and_return(12)
645
+
646
+ ad.send(:calculate_overall_pressure, :time, :total_impressed).should == 0
647
+ end
648
+ end
649
+ end
650
+
651
+ def remaining_days(time)
652
+ FakeAd.new(
653
+ :start_datetime => @start,
654
+ :end_datetime => @end,
655
+ :day_partitions => @day_partitions).send(:remaining_days, time.in_time_zone("Beijing"))
656
+ end
657
+ describe ".remaining_days(time)" do
658
+ context "when time is on or after ad's end_datetime" do
659
+ it "should return zero" do
660
+ @start = nil
661
+ @end = Time.at(1234567890)
662
+ remaining_days(@end).should == 0
663
+ remaining_days(@end + 1.day).should == 0
664
+ end
665
+ end
666
+ context "when time is before ad's end_datetime" do
667
+ context "and ad is not day-partitioned" do
668
+ before(:each) { @day_partitions = nil }
669
+ context "and time is before ad's start_datetime" do
670
+ it "returns the number of days between the ad's start and end, including partial days on either end" do
671
+ @start = Time.parse("2011-11-11 11:11 +08:00")
672
+ @end = @start + 2.days
673
+ remaining_days(@start - 1.week).should == 3
674
+ end
675
+ end
676
+ context "and time is after the ad's start_datetime" do
677
+ it "should return the number of days between the given time and the ad's end" do
678
+ @start = Time.parse("2011-11-11 11:11 +08:00")
679
+ @time = @start + 1.day
680
+ @end = @time + 1.day
681
+ remaining_days(@time).should == 2
682
+ end
683
+ context "and the ad's end occurs at midnight" do
684
+ it "should return the number of days between the given time and the ad's end, not counting the morning of the final date" do
685
+ @start = Time.parse("2011-11-11 11:11 +08:00")
686
+ @time = @start + 1.day
687
+ @end = @time.end_of_day + 1.day
688
+ remaining_days(@time).should == 2
689
+ end
690
+ end
691
+ context "and the time is on the same day as the ad's end" do
692
+ it "should return one" do
693
+ @start = Time.parse("2011-11-11 11:11 +08:00")
694
+ @time = @start + 1.day
695
+ @end = @time + 1.hour
696
+ remaining_days(@time).should == 1
697
+ end
698
+ end
699
+ context "and the ad's end is at midnight the night after the given time" do
700
+ it "should return one" do
701
+ @start = Time.parse("2011-11-11 11:11 +08:00")
702
+ @time = @start + 1.day
703
+ @end = @time.end_of_day
704
+ remaining_days(@time).should == 1
705
+ end
706
+ end
707
+ end
708
+ end
709
+ context "when ad is partitioned with the first 12 hours of each weekday active, and the last 12 and the whole weekend inactive" do
710
+ before(:all) do
711
+ @time_zone = "Beijing"
712
+ Time.zone = @time_zone
713
+ Chronic.time_class = Time.zone
714
+ @day_partitions = "F"*24 + ("T"*12 + "F"*12) * 5 + "F"*24
715
+ end
716
+ context "when time is before ad's start_datetime, and date range is from Monday @ 9:30am to Friday @ 6pm" do
717
+ it "should return 5 days" do
718
+ @start = Chronic.parse("Monday @ 9:30am")
719
+ @end = Chronic.parse("Monday @ 6pm") + 4.days #Friday
720
+ remaining_days(Chronic.parse("1 month ago")).should == 5
721
+ end
722
+ end
723
+ context "when time is before ad's start_datetime, and date range is from Monday @ 6pm to Friday @ 9:30am" do
724
+ it "should return 4 days" do
725
+ @start = Chronic.parse("Monday @ 6pm")
726
+ @end = Chronic.parse("Monday @ 9:30am") + 4.days #Friday
727
+ remaining_days(Chronic.parse("1 month ago")).should == 4
728
+ end
729
+ end
730
+ context "when time is before ad's start_datetime, and date range is from Monday @ 6pm to Friday morning @ 12am" do
731
+ it "should return 4 days" do
732
+ @start = Chronic.parse("Monday @ 6pm")
733
+ @end = Chronic.parse("Monday @ 12am") + 4.days #Friday
734
+ remaining_days(Chronic.parse("1 month ago")).should == 3
735
+ end
736
+ end
737
+ context "when time is before ad's start_datetime, and date range is from Monday @ 6pm to Three Mondays later @ 6am" do
738
+ it "should return 15 hours" do
739
+ @start = Chronic.parse("Monday @ 6pm")
740
+ @end = Chronic.parse("Monday @ 6am") + 3.weeks
741
+ remaining_days(Chronic.parse("1 month ago")).should == 15
742
+ end
743
+ end
744
+ context "when the date range is really long" do
745
+ it "should not take a long time to compute" do
746
+ @start = Chronic.parse "2010-01-01 1am"
747
+ @end = Chronic.parse "2020-01-07 5am"
748
+ started = Time.now.to_f
749
+ remaining_days(Chronic.parse("2009-12-31 11pm")).should == 2613
750
+ finished = Time.now.to_f
751
+ (finished - started).should > 0.0
752
+ (finished - started).should < 0.1
753
+ end
754
+ end
755
+ end
756
+ end
757
+ end
758
+
759
+ describe ".remaining_hours(time)" do
760
+ context "when time is on or after ad's end_datetime" do
761
+ it "should return zero" do
762
+ @end = Time.at(1234567890)
763
+ a = FakeAd.new(:end_datetime => @end)
764
+ a.send(:remaining_hours, @end).should == 0
765
+ a.send(:remaining_hours, @end + 1.day).should == 0
766
+ end
767
+ end
768
+ context "when time is before ad's end_datetime" do
769
+ context "when ad is not day-partitioned" do
770
+ context "when time is before ad's start_datetime" do
771
+ it "returns the number of hours between the ad's start and end" do
772
+ @end = Time.at(1234567890)
773
+ a = FakeAd.new(:start_datetime => @end - 3.days, :end_datetime => @end, :day_partitions => nil)
774
+ a.send(:remaining_hours, @end - 5.days).should == 24*3
775
+ end
776
+ end
777
+ context "when time is after the ad's start_datetime" do
778
+ it "should return the number of hours between the given time and the ad's end" do
779
+ @end = Time.at(1234567890)
780
+ a = FakeAd.new(:start_datetime => @end - 5.days, :end_datetime => @end, :day_partitions => nil)
781
+ a.send(:remaining_hours, @end - 3.days).should == 24*3
782
+ end
783
+ end
784
+ end
785
+ context "when ad is partitioned with the first 12 hours of each day active, and the last 12 inactive" do
786
+ before(:all) do
787
+ @time_zone = "Beijing"
788
+ Time.zone = @time_zone
789
+ Chronic.time_class = Time.zone
790
+ @day_partitions = ("T"*12 + "F"*12) * 7
791
+ end
792
+ context "when time is before ad's start_datetime, and date range is from Monday @ 9:30am to Friday @ 6pm" do
793
+ it "should return 50.5 hours" do
794
+ a = FakeAd.new(:start_datetime => Chronic.parse("Monday @ 9:30am"),
795
+ :end_datetime => Chronic.parse("Monday @ 6pm") + 4.days,
796
+ :day_partitions => @day_partitions)
797
+ a.send(:remaining_hours, Chronic.parse("1 month ago")).should == 50.5
798
+ end
799
+ end
800
+ context "when time is before ad's start_datetime, and date range is from Monday @ 6pm to Friday @ 9:30am" do
801
+ it "should return 45.5 hours" do
802
+ a = FakeAd.new(:start_datetime => Chronic.parse("Monday @ 6pm"),
803
+ :end_datetime => Chronic.parse("Monday @ 9:30am") + 4.days,
804
+ :day_partitions => @day_partitions)
805
+ a.send(:remaining_hours, Chronic.parse("1 month ago")).should == 45.5
806
+ end
807
+ end
808
+ context "when time is before ad's start_datetime, and date range is from Monday @ 6pm to Three Mondays later @ 6am" do
809
+ it "should return 246 (12 * 7 * 3 - 6) hours" do
810
+ a = FakeAd.new(:start_datetime => Chronic.parse("Monday @ 6pm"),
811
+ :end_datetime => Chronic.parse("Monday @ 6am") + 3.weeks,
812
+ :day_partitions => @day_partitions)
813
+ a.send(:remaining_hours, Chronic.parse("1 month ago")).should == 12 * 7 * 3 - 6
814
+ end
815
+ end
816
+ context "when the date range is really long" do
817
+ it "should not take a long time to compute" do
818
+ a = FakeAd.new(:start_datetime => Chronic.parse("2010-01-01 1am"),
819
+ :end_datetime => Chronic.parse("2020-01-07 5am"),
820
+ :day_partitions => @day_partitions)
821
+ started = Time.now.to_f
822
+ a.send(:remaining_hours, Chronic.parse("2009-12-31 11pm")).should == 43900
823
+ finished = Time.now.to_f
824
+ (finished - started).should > 0.0
825
+ (finished - started).should < 0.1
826
+ end
827
+ end
828
+ end
829
+ end
830
+ end
831
+
832
+ def remaining_hours_today(time)
833
+ @ad = FakeAd.new(
834
+ :start_datetime => @start,
835
+ :end_datetime => @end,
836
+ :day_partitions => @day_partitions)
837
+ @ad.send(:remaining_hours_today, time.in_time_zone(@time_zone))
838
+ end
839
+ describe ".remaining_hours_today(time)" do
840
+ before(:all) do
841
+ @time_zone = "Beijing"
842
+ Time.zone = @time_zone
843
+ Chronic.time_class = Time.zone
844
+ end
845
+ context "when ad is not day-partitioned" do
846
+ before(:all) { @day_partitions = nil}
847
+ context "when ad's start and end date/time are the same day" do
848
+ before(:all) do
849
+ @start = Chronic.parse('tomorrow @ 9am')
850
+ @end = Chronic.parse('tomorrow @ 5pm')
851
+ end
852
+ context "when the given time is on a different day" do
853
+ it "should return zero" do
854
+ remaining_hours_today(@start - 2.days).should == 0
855
+ remaining_hours_today(@end - 2.days).should == 0
856
+ end
857
+ end
858
+ context "when the given time is on the same day, but before the start" do
859
+ it "should return the full number of hours of the ad's range" do
860
+ remaining_hours_today(@start - 5.hours).should == 8
861
+ end
862
+ end
863
+ context 'when the given time is on the same day, within the date range' do
864
+ it "should return the hours between the given time and the end" do
865
+ remaining_hours_today(Chronic.parse('tomorrow @ noon')).should == 5
866
+ end
867
+ end
868
+ context "when the given time is on the same day, but after the end" do
869
+ it "should return zero" do
870
+ remaining_hours_today(@end + 1.hour).should == 0
871
+ end
872
+ end
873
+ end
874
+ context "when the given time is at least 24 hours before the start time" do
875
+ it "should return zero" do
876
+ @start = Time.at(1234567890)
877
+ @end = @start + 1.month
878
+ remaining_hours_today(@start - 1.day).should == 0
879
+ end
880
+ end
881
+ context "when the given time is at least 24 hours after the end time" do
882
+ it "should return zero" do
883
+ @start = Time.at(1234567890)
884
+ @end = @start + 1.month
885
+ remaining_hours_today(@end + 1.day).should == 0
886
+ end
887
+ end
888
+ context "when the given time is after the the start_time" do
889
+ context "and at least 24 hours before the end time" do
890
+ it "should return the hours from the given time to midnight of that night" do
891
+ @start = Chronic.parse("yesterday @ 3am")
892
+ @time = Chronic.parse("today @ 8pm")
893
+ @end = @time + 1.day
894
+ remaining_hours_today(@time).should == 4
895
+ end
896
+ end
897
+ context "and the ad's end comes before midnight of the given time" do
898
+ context "and the given time is before the end time" do
899
+ it "should return the hours between the given time and the end time" do
900
+ @start = Chronic.parse("yesterday @ 3am")
901
+ @time = Chronic.parse("today @ 8pm")
902
+ @end = @time + 2.hours
903
+ remaining_hours_today(@time).should == 2
904
+ end
905
+ end
906
+ context "and the given time is after the end time" do
907
+ it "should return zero" do
908
+ @start = Chronic.parse("yesterday @ 3am")
909
+ @time = Chronic.parse("today @ 8pm")
910
+ @end = @time - 2.hours
911
+ remaining_hours_today(@time).should == 0
912
+ end
913
+ end
914
+ context "and the given time matches the end time" do
915
+ it "should return zero" do
916
+ @start = Chronic.parse("yesterday @ 3am")
917
+ @time = Chronic.parse("today @ 8pm")
918
+ @end = @time
919
+ remaining_hours_today(@time).should == 0
920
+ end
921
+ end
922
+ end
923
+ end
924
+ end
925
+ context "when ad is partitioned with the first 12 hours of each day active, and the last 12 inactive" do
926
+ before(:all) { @day_partitions = ("T"*12 + "F"*12) * 7}
927
+ context "when ad's start and end date/time are the same day" do
928
+ before(:all) do
929
+ @start = Chronic.parse('tomorrow @ 9am')
930
+ @end = Chronic.parse('tomorrow @ 5pm')
931
+ end
932
+ context "when the given time is on a different day" do
933
+ it "should return zero" do
934
+ remaining_hours_today(@start - 2.days).should == 0
935
+ remaining_hours_today(@end - 2.days).should == 0
936
+ end
937
+ end
938
+ context "when the given time is on the same day, but before the start" do
939
+ it "should return the full number of hours of the ad's range" do
940
+ remaining_hours_today(@start - 5.hours).should == 3
941
+ end
942
+ end
943
+ context 'when the given time is on the same day, within the date range' do
944
+ it "should return the hours between the given time and the end" do
945
+ remaining_hours_today(Chronic.parse('tomorrow @ 11am')).should == 1
946
+ end
947
+ end
948
+ context "when the given time is on the same day, but after the end" do
949
+ it "should return zero" do
950
+ remaining_hours_today(@end + 1.hour).should == 0
951
+ end
952
+ end
953
+ end
954
+ context "when the given time is at least 24 hours before the start time" do
955
+ it "should return zero" do
956
+ @start = Time.at(1234567890)
957
+ @end = @start + 1.month
958
+ remaining_hours_today(@start - 1.day).should == 0
959
+ end
960
+ end
961
+ context "when the given time is at least 24 hours after the end time" do
962
+ it "should return zero" do
963
+ @start = Time.at(1234567890)
964
+ @end = @start + 1.month
965
+ remaining_hours_today(@end + 1.day).should == 0
966
+ end
967
+ end
968
+ context "when the given time is after the the start_time" do
969
+ context "and at least 24 hours before the end time" do
970
+ it "should return the hours from the given time to midnight of that night" do
971
+ @start = Chronic.parse("yesterday @ 3am")
972
+ @time = Chronic.parse("today @ 7am")
973
+ @end = @time + 1.day
974
+ remaining_hours_today(@time).should == 5 #day part ends at noon
975
+ end
976
+ end
977
+ context "and the ad's end comes before midnight of the given time" do
978
+ context "and the given time is before the end time" do
979
+ it "should return the hours between the given time and the end time" do
980
+ @start = Chronic.parse("yesterday @ 3am")
981
+ @time = Chronic.parse("today @ 10:30am")
982
+ @end = @time + 2.hours
983
+ remaining_hours_today(@time).should == 1.5 #day part ends at noon
984
+ end
985
+ end
986
+ context "and the given time is after the end time" do
987
+ it "should return zero" do
988
+ @start = Chronic.parse("yesterday @ 3am")
989
+ @time = Chronic.parse("today @ 7am")
990
+ @end = @time - 2.hours
991
+ remaining_hours_today(@time).should == 0
992
+ end
993
+ end
994
+ context "and the given time matches the end time" do
995
+ it "should return zero" do
996
+ @start = Chronic.parse("yesterday @ 3am")
997
+ @time = Chronic.parse("today @ 7am")
998
+ @end = @time
999
+ remaining_hours_today(@time).should == 0
1000
+ end
1001
+ end
1002
+ end
1003
+ end
1004
+ end
1005
+ end
1006
+
1007
+ def beginning_of_week(time)
1008
+ FakeAd.new.send(:beginning_of_week, time)
1009
+ end
1010
+ describe ".beginning_of_week(time)" do
1011
+ context "time is on sunday morning at midnight" do
1012
+ it "should return an equivalent time" do
1013
+ time = Chronic.parse("Sunday @ 00:00")
1014
+ beginning_of_week(time).should == time
1015
+ end
1016
+ end
1017
+ context "time is on sunday at noon" do
1018
+ it "should return a time 12 hours before" do
1019
+ time = Chronic.parse("Sunday @ noon")
1020
+ beginning_of_week(time).should == time - 12.hours
1021
+ end
1022
+ end
1023
+ context "time is on monday at noon" do
1024
+ it "should return a time 12 hours and one day before" do
1025
+ time = Chronic.parse("Monday @ noon")
1026
+ beginning_of_week(time).should == time - (12.hours + 1.day)
1027
+ end
1028
+ end
1029
+ context "time is on tuesday at noon" do
1030
+ it "should return a time 12 hours and two days before" do
1031
+ time = Chronic.parse("Tuesday @ noon")
1032
+ beginning_of_week(time).should == time - (12.hours + 2.day)
1033
+ end
1034
+ end
1035
+ context "time is on wednesday at noon" do
1036
+ it "should return a time 12 hours and three days before" do
1037
+ time = Chronic.parse("Wednesday @ noon")
1038
+ beginning_of_week(time).should == time - (12.hours + 3.day)
1039
+ end
1040
+ end
1041
+ context "time is on thursday at noon" do
1042
+ it "should return a time 12 hours and four days before" do
1043
+ time = Chronic.parse("Thursday @ noon")
1044
+ beginning_of_week(time).should == time - (12.hours + 4.day)
1045
+ end
1046
+ end
1047
+ context "time is on friday at noon" do
1048
+ it "should return a time 12 hours and five days before" do
1049
+ time = Chronic.parse("Friday @ noon")
1050
+ beginning_of_week(time).should == time - (12.hours + 5.day)
1051
+ end
1052
+ end
1053
+ context "time is on saturday at noon" do
1054
+ it "should return a time 12 hours and six days before" do
1055
+ time = Chronic.parse("Saturday @ noon")
1056
+ beginning_of_week(time).should == time - (12.hours + 6.day)
1057
+ end
1058
+ end
1059
+ end
1060
+
1061
+ def beginning_of_hour(time)
1062
+ FakeAd.new.send(:beginning_of_hour, time)
1063
+ end
1064
+ describe ".beginning_of_hour(time)" do
1065
+ before(:all) { @one_pm = Chronic.parse('1:00pm')}
1066
+ context "at 1:00 pm" do
1067
+ it "should return 1:00 pm" do
1068
+ beginning_of_hour(@one_pm).should == @one_pm
1069
+ end
1070
+ end
1071
+ context "at 1:01 pm" do
1072
+ it "should return 1:00 pm" do
1073
+ beginning_of_hour(@one_pm + 1.minute).should == @one_pm
1074
+ end
1075
+ end
1076
+ context "at 1:59:59.9 pm" do
1077
+ it "should return 1:00 pm" do
1078
+ beginning_of_hour(@one_pm + 59.minutes + 59.9.seconds).should == @one_pm
1079
+ end
1080
+ end
1081
+ end
1082
+
1083
+ def partitioned_hours(beginning, ending)
1084
+ FakeAd.new(:day_partitions => @day_partitions).send(:partitioned_hours, beginning, ending)
1085
+ end
1086
+ describe ".partitioned_hours(beginning, ending)" do
1087
+ before(:all) do
1088
+ @time_zone = "Beijing"
1089
+ Time.zone = @time_zone
1090
+ Chronic.time_class = Time.zone
1091
+ end
1092
+ context "when ad is partitioned, active first 12 hours of every week day, inactive the last 12 hours and weekends" do
1093
+ before(:all) { @day_partitions = "F"*24 + ("T"*12 + "F"*12)*5 + "F"*24 }
1094
+ context "and beginning after ending" do
1095
+ it "should return zero" do
1096
+ partitioned_hours(Time.now, Time.now - 1.day).should == 0
1097
+ end
1098
+ end
1099
+ context "and beginning and ending are in the same hour" do
1100
+ context "and that hour is an active partition" do
1101
+ it "should return the time (in hours) from beginning to ending" do
1102
+ @start = Chronic.parse("Monday @ 9:10am")
1103
+ @end = Chronic.parse("Monday @ 9:50am")
1104
+ partitioned_hours(@start, @end).should == 40/60.0
1105
+ end
1106
+ end
1107
+ context "and that hour is an inactive partition" do
1108
+ it "should return zero" do
1109
+ @start = Chronic.parse("Monday @ 9:10pm")
1110
+ @end = Chronic.parse("Monday @ 9:50pm")
1111
+ partitioned_hours(@start, @end).should == 0
1112
+ end
1113
+ end
1114
+ end
1115
+ it "should have more specs"
1116
+ end
1117
+ end
1118
+
1119
+ describe ".partitioned_days(beginning, ending)" do
1120
+ it "should have specs"
1121
+ end
1122
+
1123
+ describe ".each_day(beginning, ending)" do
1124
+ it "should have specs"
1125
+ end
1126
+
1127
+ describe ".each_hour(beginning, ending)" do
1128
+ it "should have specs"
1129
+ end
1130
+
1131
+ describe ".actual_hours(beginning, ending)" do
1132
+ it "should have specs"
1133
+ end
1134
+
1135
+ describe ".days_per_week" do
1136
+ it "should have specs"
1137
+ end
1138
+
1139
+ describe ".hours_per_week" do
1140
+ it "should have specs"
1141
+ end
1142
+
1143
+ end
1144
+ end
1145
+ end
1146
+ end
1147
+ end
1148
+