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.
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
+