schedulability 0.1.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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +1 -0
- data/.editorconfig +16 -0
- data/.simplecov +9 -0
- data/ChangeLog +87 -0
- data/History.md +4 -0
- data/Manifest.txt +15 -0
- data/README.md +236 -0
- data/Rakefile +88 -0
- data/lib/schedulability.rb +28 -0
- data/lib/schedulability/exceptions.rb +16 -0
- data/lib/schedulability/mixins.rb +126 -0
- data/lib/schedulability/parser.rb +309 -0
- data/lib/schedulability/schedule.rb +214 -0
- data/spec/helpers.rb +45 -0
- data/spec/schedulability/schedule_spec.rb +828 -0
- data/spec/schedulability_spec.rb +13 -0
- metadata +216 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
require 'loggability'
|
7
|
+
require 'schedulability' unless defined?( Schedulability )
|
8
|
+
require 'schedulability/exceptions'
|
9
|
+
|
10
|
+
|
11
|
+
# A schedule object representing one or more abstract ranges of times.
|
12
|
+
class Schedulability::Schedule
|
13
|
+
extend Loggability
|
14
|
+
|
15
|
+
|
16
|
+
# Schedulability API -- Log to the Schedulability logger
|
17
|
+
log_to :schedulability
|
18
|
+
|
19
|
+
|
20
|
+
### Parse one or more periods from the specified +expression+ and return a Schedule
|
21
|
+
### created with them.
|
22
|
+
def self::parse( expression )
|
23
|
+
positive, negative = Schedulability::Parser.extract_periods( expression )
|
24
|
+
return new( positive, negative )
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
### Create a new Schedule using the specified +periods+.
|
29
|
+
def initialize( positive_periods=[], negative_periods=[] )
|
30
|
+
positive_periods ||= []
|
31
|
+
negative_periods ||= []
|
32
|
+
|
33
|
+
@positive_periods = positive_periods.flatten.uniq
|
34
|
+
@positive_periods.freeze
|
35
|
+
@negative_periods = negative_periods.flatten.uniq
|
36
|
+
@negative_periods.freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# The periods that express which times are in the schedule
|
41
|
+
attr_reader :positive_periods
|
42
|
+
|
43
|
+
# The periods that express which times are *not* in the schedule
|
44
|
+
attr_reader :negative_periods
|
45
|
+
|
46
|
+
|
47
|
+
### Returns +true+ if the schedule doesn't have any time periods.
|
48
|
+
def empty?
|
49
|
+
return self.positive_periods.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
### Returns +true+ if the current time is within one of the Schedule's periods.
|
54
|
+
def now?
|
55
|
+
return self.include?( Time.now )
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
### Returns +true+ if the specified +time+ is in the schedule.
|
60
|
+
def include?( time )
|
61
|
+
time_obj = if time.respond_to?( :to_time )
|
62
|
+
time.to_time
|
63
|
+
else
|
64
|
+
time_obj = Time.parse( time.to_s )
|
65
|
+
self.log.debug "Parsed %p to time %p" % [ time, time_obj ]
|
66
|
+
time_obj
|
67
|
+
end
|
68
|
+
|
69
|
+
return ! self.negative_periods_include?( time_obj ) &&
|
70
|
+
self.positive_periods_include?( time_obj )
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
### Returns +true+ if any of the schedule's positive periods include the
|
75
|
+
### specified +time+.
|
76
|
+
def positive_periods_include?( time )
|
77
|
+
return self.positive_periods.empty? ||
|
78
|
+
find_matching_period_for( time, self.positive_periods )
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
### Returns +true+ if any of the schedule's negative periods include the
|
83
|
+
### specified +time+.
|
84
|
+
def negative_periods_include?( time )
|
85
|
+
return find_matching_period_for( time, self.negative_periods )
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
### Returns +true+ if the time periods for +other_schedule+ are the same as those for the
|
90
|
+
### receiver.
|
91
|
+
def ==( other_schedule )
|
92
|
+
other_schedule.is_a?( self.class ) &&
|
93
|
+
self.positive_periods.all? {|period| other_schedule.positive_periods.include?(period) } &&
|
94
|
+
other_schedule.positive_periods.all? {|period| self.positive_periods.include?(period) } &&
|
95
|
+
self.negative_periods.all? {|period| other_schedule.negative_periods.include?(period) } &&
|
96
|
+
other_schedule.negative_periods.all? {|period| self.negative_periods.include?(period) }
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
### Return a new Schedulability::Schedule object that is the union of the receiver and
|
101
|
+
### +other_schedule+.
|
102
|
+
def |( other_schedule )
|
103
|
+
positive = self.positive_periods + other_schedule.positive_periods
|
104
|
+
negative = intersect_periods( self.negative_periods, other_schedule.negative_periods )
|
105
|
+
|
106
|
+
return self.class.new( positive, negative )
|
107
|
+
end
|
108
|
+
alias_method :+, :|
|
109
|
+
|
110
|
+
|
111
|
+
### Return a new Schedulability::Schedule object that is the intersection of the receiver and
|
112
|
+
### +other_schedule+.
|
113
|
+
def &( other_schedule )
|
114
|
+
positive = intersect_periods( self.positive_periods, other_schedule.positive_periods )
|
115
|
+
negative = self.negative_periods + other_schedule.negative_periods
|
116
|
+
|
117
|
+
return self.class.new( positive, negative )
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
### Return a new Schedulability::Schedule object that inverts the positive and negative
|
122
|
+
### period criteria.
|
123
|
+
def ~@
|
124
|
+
return self.class.new( self.negative_periods, self.positive_periods )
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
#######
|
129
|
+
private
|
130
|
+
#######
|
131
|
+
|
132
|
+
### Returns true if any of the specified +periods+ contains the specified +time+.
|
133
|
+
def find_matching_period_for( time, periods )
|
134
|
+
periods.any? do |period|
|
135
|
+
period.all? do |scale, ranges|
|
136
|
+
val = value_for_scale( time, scale )
|
137
|
+
ranges.any? {|rng| rng.cover?(val) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
### Return the appropriate numeric value for the specified +scale+ from the
|
144
|
+
### given +time+.
|
145
|
+
def value_for_scale( time, scale )
|
146
|
+
case scale
|
147
|
+
when :mo
|
148
|
+
return time.mon
|
149
|
+
when :md
|
150
|
+
return time.day
|
151
|
+
when :wd
|
152
|
+
return time.wday
|
153
|
+
when :hr
|
154
|
+
return time.hour
|
155
|
+
when :min
|
156
|
+
return time.min
|
157
|
+
when :sec
|
158
|
+
return time.sec
|
159
|
+
when :yd
|
160
|
+
return time.yday
|
161
|
+
when :wk
|
162
|
+
return ( time.day / 7.0 ).ceil
|
163
|
+
when :yr
|
164
|
+
self.log.debug "Year match: %p" % [ time.year ]
|
165
|
+
return time.year
|
166
|
+
else
|
167
|
+
# If this happens, it's likely a bug in the parser.
|
168
|
+
raise ScriptError, "unknown scale %p" % [ scale ]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
### Return the specified +periods+ exploded into integer arrays instead of Ranges.
|
174
|
+
def explode( periods )
|
175
|
+
return periods.map do |per|
|
176
|
+
per.each_with_object({}) do |(scale,ranges), hash|
|
177
|
+
hash[ scale ] = ranges.flat_map( &:to_a )
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
### Return the intelligent merge of the +left+ and +right+ period hashes, only retaining
|
184
|
+
### values that exist on both sides.
|
185
|
+
def intersect_periods( left, right )
|
186
|
+
new_periods = []
|
187
|
+
explode( left ).product( explode(right) ) do |p1, p2|
|
188
|
+
new_period = {}
|
189
|
+
common_scales = p1.keys & p2.keys
|
190
|
+
|
191
|
+
# Keys exist on both sides, diff+merge identical values
|
192
|
+
common_scales.each do |scale|
|
193
|
+
vals = p1[ scale ] & p2[ scale ]
|
194
|
+
new_period[ scale ] = Schedulability::Parser.coalesce_ranges( vals, scale )
|
195
|
+
end
|
196
|
+
next if new_period.values.any?( &:empty? )
|
197
|
+
|
198
|
+
# Keys exist only on one side, sync between sides because
|
199
|
+
# the other side is implicitly infinite.
|
200
|
+
(p1.keys - common_scales).each do |scale|
|
201
|
+
new_period[ scale ] = Schedulability::Parser.coalesce_ranges( p1[scale], scale )
|
202
|
+
end
|
203
|
+
(p2.keys - common_scales).each do |scale|
|
204
|
+
new_period[ scale ] = Schedulability::Parser.coalesce_ranges( p2[scale], scale )
|
205
|
+
end
|
206
|
+
|
207
|
+
new_periods << new_period
|
208
|
+
end
|
209
|
+
|
210
|
+
return new_periods
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
end # class Schedulability::Schedule
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set nosta noet ts=4 sw=4:
|
3
|
+
# encoding: utf-8
|
4
|
+
|
5
|
+
BEGIN {
|
6
|
+
require 'pathname'
|
7
|
+
basedir = Pathname.new( __FILE__ ).dirname.parent
|
8
|
+
|
9
|
+
libdir = basedir + "lib"
|
10
|
+
|
11
|
+
$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
|
12
|
+
}
|
13
|
+
|
14
|
+
# SimpleCov test coverage reporting; enable this using the :coverage rake task
|
15
|
+
if ENV['COVERAGE']
|
16
|
+
$stderr.puts "\n\n>>> Enabling coverage report.\n\n"
|
17
|
+
require 'simplecov'
|
18
|
+
SimpleCov.start do
|
19
|
+
add_filter 'spec'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'schedulability'
|
24
|
+
require 'loggability/spechelpers'
|
25
|
+
|
26
|
+
|
27
|
+
# Helpers specific to Schedulability specs
|
28
|
+
module Schedulability::SpecHelpers
|
29
|
+
end # module Schedulability::SpecHelpers
|
30
|
+
|
31
|
+
|
32
|
+
### Mock with RSpec
|
33
|
+
RSpec.configure do |c|
|
34
|
+
c.run_all_when_everything_filtered = true
|
35
|
+
c.filter_run :focus
|
36
|
+
c.order = 'random'
|
37
|
+
|
38
|
+
c.mock_with( :rspec ) do |mock|
|
39
|
+
mock.syntax = :expect
|
40
|
+
end
|
41
|
+
|
42
|
+
c.include( Loggability::SpecHelpers )
|
43
|
+
c.include( Schedulability::SpecHelpers )
|
44
|
+
end
|
45
|
+
|
@@ -0,0 +1,828 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../helpers'
|
4
|
+
|
5
|
+
require 'time'
|
6
|
+
require 'timecop'
|
7
|
+
require 'schedulability/schedule'
|
8
|
+
require 'schedulability/mixins'
|
9
|
+
|
10
|
+
|
11
|
+
using Schedulability::TimeRefinements
|
12
|
+
|
13
|
+
describe Schedulability::Schedule do
|
14
|
+
|
15
|
+
before( :all ) do
|
16
|
+
@actual_zone = ENV['TZ']
|
17
|
+
ENV['TZ'] = 'GMT'
|
18
|
+
end
|
19
|
+
|
20
|
+
after( :all ) do
|
21
|
+
ENV['TZ'] = @actual_zone
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
let( :testing_time ) { Time.iso8601('2015-12-15T12:00:00-00:00') }
|
26
|
+
|
27
|
+
|
28
|
+
context "with no periods" do
|
29
|
+
|
30
|
+
let( :schedule ) { described_class.new }
|
31
|
+
|
32
|
+
|
33
|
+
it "is empty" do
|
34
|
+
expect( schedule ).to be_empty
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
it "is always 'now'" do
|
39
|
+
Timecop.freeze( testing_time ) do
|
40
|
+
expect( schedule ).to be_now
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
it "includes every time" do
|
46
|
+
expect( schedule ).to include( testing_time )
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
it "is equal to other empty schedules" do
|
51
|
+
expect( schedule ).to be == described_class.new
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
it "is not equal to any other non-empty schedules" do
|
56
|
+
expect( schedule ).to_not be == described_class.new( md: 10..10 )
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
context "with one simple time period" do
|
63
|
+
|
64
|
+
let( :schedule ) { described_class.parse("wd {Mon-Fri}") }
|
65
|
+
|
66
|
+
|
67
|
+
it "isn't empty" do
|
68
|
+
expect( schedule ).to_not be_empty
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
it "includes the current time if it's within the period" do
|
73
|
+
Timecop.freeze( testing_time ) do
|
74
|
+
expect( schedule ).to be_now
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
it "includes a particular time within its period" do
|
80
|
+
expect( schedule ).to include( testing_time )
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
it "doesn't include a time outside of its period" do
|
85
|
+
expect( schedule ).to_not include( 'Tue Dec 13 12:00:00 2015' )
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
it "is equal to another schedule with the same period" do
|
90
|
+
expect( schedule ).to be == described_class.parse( 'wd {Mon Tue Wed Thu Fri}' )
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
it "is not equal to another schedule if it doesn't have the same time periods" do
|
95
|
+
expect( schedule ).to_not be == described_class.parse( 'wd {Mon-Sat}' )
|
96
|
+
expect( schedule ).to_not be == described_class.parse( 'wd {Mon-Thu}' )
|
97
|
+
expect( schedule ).to_not be == described_class.parse( 'wd {Mon-Fri} hour {6am-8am}' )
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
context "with one time period with multiple scales" do
|
104
|
+
|
105
|
+
let( :schedule ) { described_class.parse("wd {Sun Tue} hr {8am-4pm}") }
|
106
|
+
|
107
|
+
|
108
|
+
it "isn't empty" do
|
109
|
+
expect( schedule ).to_not be_empty
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
it "includes the current time if both scales match" do
|
114
|
+
Timecop.freeze( testing_time ) do
|
115
|
+
expect( schedule ).to be_now
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
it "includes a particular time within its period" do
|
121
|
+
expect( schedule ).to include( 'Tue Dec 15 12:00:00 2015' )
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
it "doesn't include a time outside of its period" do
|
126
|
+
expect( schedule ).to_not include( 'Tue Dec 15 17:00:00 2015' )
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
context "with multiple time periods" do
|
133
|
+
|
134
|
+
let( :schedule ) do
|
135
|
+
described_class.parse( "wd {Mon Wed Fri} hr {8am-4pm}, wd {Tue Thu} hr {9am-5pm}" )
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
it "isn't empty" do
|
140
|
+
expect( schedule ).to_not be_empty
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
it "includes the current time if all scales of one of its periods match" do
|
145
|
+
expect( schedule ).to include( 'Tue Dec 15 12:00:00 2015' )
|
146
|
+
expect( schedule ).to include( 'Wed Dec 16 12:00:00 2015' )
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
it "doesn't include a time outside of all of its periods" do
|
151
|
+
expect( schedule ).to_not include( 'Tue Dec 15 8:00:00 2015' )
|
152
|
+
expect( schedule ).to_not include( 'Wed Dec 16 17:00:00 2015' )
|
153
|
+
expect( schedule ).to_not include( 'Sat Dec 19 12:00:00 2015' )
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
it "respects negations" do
|
158
|
+
schedule = described_class.
|
159
|
+
parse( "wd {Mon Wed Fri} hr {8am-4pm}, wd {Tue Thu} hr {9am-5pm}, not hour { 3pm }" )
|
160
|
+
expect( schedule ).to include( 'Tue Dec 15 12:00:00 2015' )
|
161
|
+
expect( schedule ).to include( 'Wed Dec 16 12:00:00 2015' )
|
162
|
+
expect( schedule ).to_not include( 'Wed Dec 16 15:05:00 2015' )
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
describe "period parsing" do
|
168
|
+
|
169
|
+
it "matches single second values only during that second of every minute" do
|
170
|
+
schedule = described_class.parse( "sec {18}" )
|
171
|
+
time = Time.iso8601( '2015-12-15T12:00:18-00:00' )
|
172
|
+
|
173
|
+
expect( schedule ).to_not include( time - 2.seconds )
|
174
|
+
expect( schedule ).to_not include( time - 1.second )
|
175
|
+
expect( schedule ).to include( time )
|
176
|
+
expect( schedule ).to_not include( time + 1.second )
|
177
|
+
expect( schedule ).to_not include( time + 2.seconds )
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
it "matches negated single second values during every other second of every minute" do
|
182
|
+
schedule = described_class.parse( "except sec {18}" )
|
183
|
+
time = Time.iso8601( '2015-12-15T12:00:18-00:00' )
|
184
|
+
|
185
|
+
expect( schedule ).to include( time - 2.seconds )
|
186
|
+
expect( schedule ).to include( time - 1.second )
|
187
|
+
expect( schedule ).to_not include( time )
|
188
|
+
expect( schedule ).to include( time + 1.second )
|
189
|
+
expect( schedule ).to include( time + 2.seconds )
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
it "matches second range values as multi-second exclusive ranges" do
|
194
|
+
schedule = described_class.parse( "sec {10-20}" )
|
195
|
+
time = Time.iso8601( '2015-12-15T12:00:10-00:00' )
|
196
|
+
|
197
|
+
expect( schedule ).to_not include( time - 2.seconds )
|
198
|
+
expect( schedule ).to_not include( time - 1.second )
|
199
|
+
expect( schedule ).to include( time )
|
200
|
+
expect( schedule ).to include( time + 1.second )
|
201
|
+
expect( schedule ).to include( time + 9.seconds )
|
202
|
+
expect( schedule ).to_not include( time + 10.seconds )
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
it "matches wrapped second range values as two ranges covering the upper and lower parts" do
|
207
|
+
schedule = described_class.parse( "sec {45-15}" )
|
208
|
+
time = Time.iso8601( '2015-12-15T12:00:45-00:00' )
|
209
|
+
|
210
|
+
expect( schedule ).to_not include( time - 2.seconds )
|
211
|
+
expect( schedule ).to_not include( time - 1.second )
|
212
|
+
expect( schedule ).to include( time )
|
213
|
+
expect( schedule ).to include( time + 1.second )
|
214
|
+
expect( schedule ).to include( time + 14.seconds )
|
215
|
+
expect( schedule ).to include( time + 15.seconds )
|
216
|
+
expect( schedule ).to include( time + 20.seconds )
|
217
|
+
expect( schedule ).to include( time + 29.seconds )
|
218
|
+
expect( schedule ).to_not include( time + 30.seconds )
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
it "matches single minute values as a 60-second exclusive range" do
|
223
|
+
schedule = described_class.parse( "min {28}" )
|
224
|
+
time = Time.iso8601( '2015-12-15T12:28:00-00:00' )
|
225
|
+
|
226
|
+
expect( schedule ).to_not include( time - 15.seconds )
|
227
|
+
expect( schedule ).to_not include( time - 1.second )
|
228
|
+
expect( schedule ).to include( time )
|
229
|
+
expect( schedule ).to include( time + 38.seconds )
|
230
|
+
expect( schedule ).to include( time + 59.seconds )
|
231
|
+
expect( schedule ).to_not include( time + 1.minute )
|
232
|
+
expect( schedule ).to_not include( time + 2.minutes )
|
233
|
+
end
|
234
|
+
|
235
|
+
|
236
|
+
it "matches minute range values as multi-minute exclusive ranges" do
|
237
|
+
schedule = described_class.parse( "min {25-35}" )
|
238
|
+
time = Time.iso8601( '2015-12-15T12:25:00-00:00' )
|
239
|
+
|
240
|
+
expect( schedule ).to_not include( time - 2.minutes )
|
241
|
+
expect( schedule ).to_not include( time - 1.second )
|
242
|
+
expect( schedule ).to include( time )
|
243
|
+
expect( schedule ).to include( time + 1.minute )
|
244
|
+
expect( schedule ).to include( time + 9.minutes )
|
245
|
+
expect( schedule ).to include( time + 9.minutes + 59.seconds )
|
246
|
+
expect( schedule ).to_not include( time + 10.minutes )
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
it "matches wrapped minute range values as two ranges covering the upper and lower parts" do
|
251
|
+
schedule = described_class.parse( "min {50-15}" )
|
252
|
+
time = Time.iso8601( '2015-12-15T12:50:00-00:00' )
|
253
|
+
|
254
|
+
expect( schedule ).to_not include( time - 1.minute )
|
255
|
+
expect( schedule ).to_not include( time - 1.second )
|
256
|
+
expect( schedule ).to include( time )
|
257
|
+
expect( schedule ).to include( time + 1.second )
|
258
|
+
expect( schedule ).to include( time + 1.minute )
|
259
|
+
expect( schedule ).to include( time + 9.minutes )
|
260
|
+
expect( schedule ).to include( time + 9.minutes + 59.seconds )
|
261
|
+
expect( schedule ).to include( time + 10.minutes )
|
262
|
+
expect( schedule ).to include( time + 20.minutes )
|
263
|
+
expect( schedule ).to include( time + 24.minutes )
|
264
|
+
expect( schedule ).to include( time + 24.minutes + 59.seconds )
|
265
|
+
expect( schedule ).to_not include( time + 25.minutes )
|
266
|
+
end
|
267
|
+
|
268
|
+
|
269
|
+
it "matches single hour values as a 3600-second exclusive range" do
|
270
|
+
schedule = described_class.parse( "hr {8}" )
|
271
|
+
time = Time.iso8601( '2015-12-15T08:00:00-00:00' )
|
272
|
+
|
273
|
+
expect( schedule ).to_not include( time - 1 )
|
274
|
+
expect( schedule ).to include( time )
|
275
|
+
expect( schedule ).to include( time + 1.minute )
|
276
|
+
expect( schedule ).to include( time + 20.minutes )
|
277
|
+
expect( schedule ).to include( time + (1.hour - 1.minute) )
|
278
|
+
expect( schedule ).to_not include( time + 1.hour )
|
279
|
+
expect( schedule ).to_not include( time + 3.hours )
|
280
|
+
end
|
281
|
+
|
282
|
+
|
283
|
+
it "matches hour range values as multi-hour exclusive ranges" do
|
284
|
+
schedule = described_class.parse( "hr {9am-5pm}" )
|
285
|
+
time = Time.iso8601( '2015-12-15T09:00:00-00:00' )
|
286
|
+
|
287
|
+
expect( schedule ).to_not include( time - 12.hours )
|
288
|
+
expect( schedule ).to_not include( time - 10.minutes )
|
289
|
+
expect( schedule ).to_not include( time - 1.second )
|
290
|
+
expect( schedule ).to include( time )
|
291
|
+
expect( schedule ).to include( time + 1.minute )
|
292
|
+
expect( schedule ).to include( time + 1.hour )
|
293
|
+
expect( schedule ).to include( time + 7.hours )
|
294
|
+
expect( schedule ).to include( time + (8.hours - 1.second) )
|
295
|
+
expect( schedule ).to_not include( time + 8.hours )
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
it "matches wrapped hour range values as two ranges covering the upper and lower parts" do
|
300
|
+
schedule = described_class.parse( "hr {5pm-9am}" )
|
301
|
+
time = Time.iso8601( '2015-12-15T17:00:00-00:00' )
|
302
|
+
|
303
|
+
expect( schedule ).to_not include( time - 1.hour )
|
304
|
+
expect( schedule ).to_not include( time - 1.second )
|
305
|
+
expect( schedule ).to include( time )
|
306
|
+
expect( schedule ).to include( time + 1.second )
|
307
|
+
expect( schedule ).to include( time + 1.minute )
|
308
|
+
expect( schedule ).to include( time + 10.minutes )
|
309
|
+
expect( schedule ).to include( time + 2.hours )
|
310
|
+
expect( schedule ).to include( time + (7.hours - 1.second) )
|
311
|
+
expect( schedule ).to include( time + 7.hours )
|
312
|
+
expect( schedule ).to include( time + 12.hours )
|
313
|
+
expect( schedule ).to include( time + (16.hours - 1.second) )
|
314
|
+
expect( schedule ).to include( time + 24.hours )
|
315
|
+
expect( schedule ).to_not include( time + (24.hours - 1.second) )
|
316
|
+
expect( schedule ).to_not include( time + 16.hours )
|
317
|
+
expect( schedule ).to_not include( time + 18.hours )
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
it "handles 12pm correctly" do
|
322
|
+
schedule = described_class.parse( "hr {12pm}" )
|
323
|
+
time = Time.iso8601( '2015-12-15T12:00:00-00:00' )
|
324
|
+
|
325
|
+
expect( schedule ).to_not include( time - 1 )
|
326
|
+
expect( schedule ).to include( time )
|
327
|
+
expect( schedule ).to include( time + 1.minute )
|
328
|
+
expect( schedule ).to include( time + 20.minutes )
|
329
|
+
expect( schedule ).to include( time + (1.hour - 1.minute) )
|
330
|
+
expect( schedule ).to_not include( time + 1.hour )
|
331
|
+
expect( schedule ).to_not include( time + 3.hours )
|
332
|
+
end
|
333
|
+
|
334
|
+
|
335
|
+
it "matches single day number values as a 86400-second exclusive range" do
|
336
|
+
schedule = described_class.parse( "md {11}" )
|
337
|
+
|
338
|
+
expect( schedule ).to_not include( '2014-06-10T23:00:00-00:00' )
|
339
|
+
expect( schedule ).to_not include( '2014-06-10T23:59:59-00:00' )
|
340
|
+
expect( schedule ).to include( '2014-06-11T00:00:00-00:00' )
|
341
|
+
expect( schedule ).to include( '2014-06-11T00:01:00-00:00' )
|
342
|
+
expect( schedule ).to include( '2014-06-11T01:00:00-00:00' )
|
343
|
+
expect( schedule ).to include( '2014-06-11T12:00:00-00:00' )
|
344
|
+
expect( schedule ).to include( '2014-06-11T23:59:59-00:00' )
|
345
|
+
expect( schedule ).to_not include( '2014-06-12T00:00:00-00:00' )
|
346
|
+
expect( schedule ).to_not include( '2014-06-12T02:00:00-00:00' )
|
347
|
+
end
|
348
|
+
|
349
|
+
|
350
|
+
it "matches day number range values as multi-day inclusive ranges" do
|
351
|
+
schedule = described_class.parse( "md {13-15}" )
|
352
|
+
|
353
|
+
expect( schedule ).to_not include( '2015-12-12T23:59:59:00:00-00:00' )
|
354
|
+
expect( schedule ).to include( '2015-12-13T00:00:00-00:00' )
|
355
|
+
expect( schedule ).to include( '2015-12-14T00:00:00-00:00' )
|
356
|
+
expect( schedule ).to include( '2015-12-15T00:00:00-00:00' )
|
357
|
+
expect( schedule ).to include( '2015-12-15T23:59:59-00:00' )
|
358
|
+
expect( schedule ).to_not include( '2015-12-16T00:00:00-00:00' )
|
359
|
+
end
|
360
|
+
|
361
|
+
|
362
|
+
it "matches wrapped day number range values as two ranges covering the upper and lower parts" do
|
363
|
+
schedule = described_class.parse( "md {28-2}" )
|
364
|
+
|
365
|
+
expect( schedule ).to_not include( '2015-12-27T23:59:59-00:00' )
|
366
|
+
expect( schedule ).to include( '2015-12-28T00:00:00-00:00' )
|
367
|
+
expect( schedule ).to include( '2015-12-30T00:00:00-00:00' )
|
368
|
+
expect( schedule ).to include( '2015-12-31T00:00:00-00:00' )
|
369
|
+
expect( schedule ).to include( '2016-01-01T00:00:00-00:00' )
|
370
|
+
expect( schedule ).to include( '2016-01-02T23:59:59-00:00' )
|
371
|
+
expect( schedule ).to include( '2016-02-29T00:00:00-00:00' )
|
372
|
+
expect( schedule ).to_not include( '2016-01-03T00:00:00-00:00' )
|
373
|
+
end
|
374
|
+
|
375
|
+
|
376
|
+
it "matches single week number values against the counted week of the month" do
|
377
|
+
schedule = described_class.parse( "wk {2}" )
|
378
|
+
|
379
|
+
expect( schedule ).to_not include( '2016-04-01T00:00:00-00:00' )
|
380
|
+
expect( schedule ).to_not include( '2016-04-02T00:00:00-00:00' )
|
381
|
+
expect( schedule ).to_not include( '2016-04-02T23:59:59-00:00' )
|
382
|
+
expect( schedule ).to_not include( '2016-04-03T00:00:00-00:00' )
|
383
|
+
expect( schedule ).to_not include( '2016-04-04T03:00:00-00:00' )
|
384
|
+
expect( schedule ).to_not include( '2016-04-05T06:00:00-00:00' )
|
385
|
+
expect( schedule ).to_not include( '2016-04-06T09:00:00-00:00' )
|
386
|
+
expect( schedule ).to_not include( '2016-04-07T23:59:59-00:00' )
|
387
|
+
|
388
|
+
expect( schedule ).to include( '2016-04-08T15:00:00-00:00' )
|
389
|
+
expect( schedule ).to include( '2016-04-09T19:00:00-00:00' )
|
390
|
+
expect( schedule ).to include( '2016-04-10T00:00:00-00:00' )
|
391
|
+
expect( schedule ).to include( '2016-04-11T00:00:00-00:00' )
|
392
|
+
expect( schedule ).to include( '2016-04-12T00:00:00-00:00' )
|
393
|
+
expect( schedule ).to include( '2016-04-13T00:00:00-00:00' )
|
394
|
+
expect( schedule ).to include( '2016-04-14T23:59:59-00:00' )
|
395
|
+
|
396
|
+
expect( schedule ).to_not include( '2016-04-15T00:00:00-00:00' )
|
397
|
+
expect( schedule ).to_not include( '2016-04-28T00:00:00-00:00' )
|
398
|
+
end
|
399
|
+
|
400
|
+
|
401
|
+
it "matches week number range values as multi-day inclusive ranges" do
|
402
|
+
schedule = described_class.parse( "wk {2-4}" )
|
403
|
+
|
404
|
+
expect( schedule ).to_not include( '2016-04-01T00:00:00-00:00' )
|
405
|
+
expect( schedule ).to_not include( '2016-04-02T00:00:00-00:00' )
|
406
|
+
expect( schedule ).to_not include( '2016-04-07T23:59:59-00:00' )
|
407
|
+
|
408
|
+
expect( schedule ).to include( '2016-04-08T00:00:00-00:00' )
|
409
|
+
expect( schedule ).to include( '2016-04-10T00:00:00-00:00' )
|
410
|
+
expect( schedule ).to include( '2016-04-17T00:00:00-00:00' )
|
411
|
+
expect( schedule ).to include( '2016-04-28T23:59:59-00:00' )
|
412
|
+
|
413
|
+
expect( schedule ).to_not include( '2016-04-29T00:00:00-00:00' )
|
414
|
+
expect( schedule ).to_not include( '2016-04-30T00:00:00-00:00' )
|
415
|
+
end
|
416
|
+
|
417
|
+
|
418
|
+
it "matches wrapped week number range values as two ranges covering the upper and lower parts" do
|
419
|
+
schedule = described_class.parse( "wk {4-1}" )
|
420
|
+
|
421
|
+
expect( schedule ).to include( '2016-04-01T00:00:00-00:00' )
|
422
|
+
expect( schedule ).to include( '2016-04-02T00:00:00-00:00' )
|
423
|
+
expect( schedule ).to include( '2016-04-07T23:59:59-00:00' )
|
424
|
+
|
425
|
+
expect( schedule ).to_not include( '2016-04-08T00:00:00-00:00' )
|
426
|
+
expect( schedule ).to_not include( '2016-04-10T00:00:00-00:00' )
|
427
|
+
expect( schedule ).to_not include( '2016-04-20T23:59:59-00:00' )
|
428
|
+
|
429
|
+
expect( schedule ).to include( '2016-04-28T00:00:00-00:00' )
|
430
|
+
expect( schedule ).to include( '2016-04-29T00:00:00-00:00' )
|
431
|
+
expect( schedule ).to include( '2016-04-30T23:59:59-00:00' )
|
432
|
+
end
|
433
|
+
|
434
|
+
|
435
|
+
it "matches single day of week values as a 86400-second exclusive range" do
|
436
|
+
schedule = described_class.parse( "wd {Wed}" )
|
437
|
+
|
438
|
+
expect( schedule ).to_not include( 'Tue, 01 Dec 2015 00:00:00 GMT' )
|
439
|
+
expect( schedule ).to_not include( 'Tue, 01 Dec 2015 23:59:59 GMT' )
|
440
|
+
expect( schedule ).to include( 'Wed, 02 Dec 2015 00:00:00 GMT' )
|
441
|
+
expect( schedule ).to include( 'Wed, 02 Dec 2015 12:00:00 GMT' )
|
442
|
+
expect( schedule ).to include( 'Wed, 02 Dec 2015 23:59:59 GMT' )
|
443
|
+
expect( schedule ).to_not include( 'Thu, 03 Dec 2015 00:00:00 GMT' )
|
444
|
+
end
|
445
|
+
|
446
|
+
|
447
|
+
it "matches single numeric day of week value as a 86400-second exclusive range" do
|
448
|
+
schedule = described_class.parse( "wd {6}" )
|
449
|
+
|
450
|
+
expect( schedule ).to_not include( 'Fri, 01 Jan 2016 00:00:00 GMT' )
|
451
|
+
expect( schedule ).to_not include( 'Fri, 01 Jan 2016 23:59:59 GMT' )
|
452
|
+
expect( schedule ).to include( 'Sat, 02 Jan 2016 00:00:00 GMT' )
|
453
|
+
expect( schedule ).to include( 'Sat, 02 Jan 2016 12:00:00 GMT' )
|
454
|
+
expect( schedule ).to include( 'Sat, 02 Jan 2016 23:59:59 GMT' )
|
455
|
+
expect( schedule ).to_not include( 'Sun, 03 Jan 2016 00:00:00 GMT' )
|
456
|
+
end
|
457
|
+
|
458
|
+
|
459
|
+
it "matches day of week name ranges as an inclusive range" do
|
460
|
+
schedule = described_class.parse( "wd {Mon-Fri}" )
|
461
|
+
|
462
|
+
expect( schedule ).to_not include( 'Sun, 03 Jan 2016 23:59:59 GMT' )
|
463
|
+
expect( schedule ).to include( 'Mon, 04 Jan 2016 00:00:00 GMT' )
|
464
|
+
expect( schedule ).to include( 'Wed, 06 Jan 2016 00:00:00 GMT' )
|
465
|
+
expect( schedule ).to include( 'Fri, 08 Jan 2016 23:59:59 GMT' )
|
466
|
+
expect( schedule ).to_not include( 'Sat, 09 Jan 2016 00:00:00 GMT' )
|
467
|
+
expect( schedule ).to_not include( 'Sat, 09 Jan 2016 23:59:59 GMT' )
|
468
|
+
end
|
469
|
+
|
470
|
+
|
471
|
+
it "matches day of week wrapped name ranges as a set of two ranges of the included days" do
|
472
|
+
schedule = described_class.parse( "wd {Fri-Sun}" )
|
473
|
+
|
474
|
+
expect( schedule ).to_not include( 'Mon, 04 Jan 2016 00:00:00 GMT' )
|
475
|
+
expect( schedule ).to_not include( 'Thu, 07 Jan 2016 23:59:59 GMT' )
|
476
|
+
expect( schedule ).to include( 'Fri, 08 Jan 2016 00:00:00 GMT' )
|
477
|
+
expect( schedule ).to include( 'Sat, 09 Jan 2016 12:00:00 GMT' )
|
478
|
+
expect( schedule ).to include( 'Sun, 10 Jan 2016 23:59:59 GMT' )
|
479
|
+
expect( schedule ).to_not include( 'Mon, 11 Jan 2016 00:00:00 GMT' )
|
480
|
+
expect( schedule ).to_not include( 'Mon, 11 Jan 2016 23:59:59 GMT' )
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
it "matches single month name values as a single-month exclusive range" do
|
485
|
+
schedule = described_class.parse( "mo {Dec}" )
|
486
|
+
|
487
|
+
expect( schedule ).to_not include( 'Mon, 30 Nov 2015 23:59:59 GMT' )
|
488
|
+
expect( schedule ).to include( 'Tue, 01 Dec 2015 00:00:00 GMT' )
|
489
|
+
expect( schedule ).to include( 'Tue, 15 Dec 2015 00:00:00 GMT' )
|
490
|
+
expect( schedule ).to include( 'Thu, 31 Dec 2015 23:59:59 GMT' )
|
491
|
+
expect( schedule ).to_not include( 'Fri, 01 Jan 2016 00:00:00 GMT' )
|
492
|
+
end
|
493
|
+
|
494
|
+
|
495
|
+
it "matches single month number values as a single-month exclusive range" do
|
496
|
+
schedule = described_class.parse( "mo {7}" )
|
497
|
+
|
498
|
+
expect( schedule ).to_not include( '2015-06-30T23:59:59-00:00' )
|
499
|
+
expect( schedule ).to include( '2015-07-01T00:00:00-00:00' )
|
500
|
+
expect( schedule ).to include( '2015-07-15T23:59:59-00:00' )
|
501
|
+
expect( schedule ).to include( '2015-07-31T23:59:59-00:00' )
|
502
|
+
expect( schedule ).to_not include( '2015-08-01T00:00:00-00:00' )
|
503
|
+
end
|
504
|
+
|
505
|
+
|
506
|
+
it "matches a range of month name values as a inclusive range" do
|
507
|
+
schedule = described_class.parse( "mo {Aug-Nov}" )
|
508
|
+
|
509
|
+
expect( schedule ).to_not include( 'Fri, 31 Jul 2015 23:59:59 GMT' )
|
510
|
+
expect( schedule ).to include( 'Sat, 01 Aug 2015 00:00:00 GMT' )
|
511
|
+
expect( schedule ).to include( 'Thu, 15 Oct 2015 00:00:00 GMT' )
|
512
|
+
expect( schedule ).to include( 'Mon, 30 Nov 2015 23:59:59 GMT' )
|
513
|
+
expect( schedule ).to_not include( 'Tue, 01 Dec 2015 00:00:00 GMT' )
|
514
|
+
end
|
515
|
+
|
516
|
+
|
517
|
+
it "matches every month other than those in a negated range of month names" do
|
518
|
+
schedule = described_class.parse( "not mo {Aug-Nov}" )
|
519
|
+
|
520
|
+
expect( schedule ).to include( 'Fri, 31 Jul 2015 23:59:59 GMT' )
|
521
|
+
expect( schedule ).to_not include( 'Sat, 01 Aug 2015 00:00:00 GMT' )
|
522
|
+
expect( schedule ).to_not include( 'Thu, 15 Oct 2015 00:00:00 GMT' )
|
523
|
+
expect( schedule ).to_not include( 'Mon, 30 Nov 2015 23:59:59 GMT' )
|
524
|
+
expect( schedule ).to include( 'Tue, 01 Dec 2015 00:00:00 GMT' )
|
525
|
+
end
|
526
|
+
|
527
|
+
|
528
|
+
it "matches a wrapped range of month name values as two inclusive ranges" do
|
529
|
+
schedule = described_class.parse( "mo {Sep-Mar}" )
|
530
|
+
|
531
|
+
expect( schedule ).to_not include( 'Mon, 31 Aug 2015 23:59:59 GMT' )
|
532
|
+
expect( schedule ).to include( 'Tue, 01 Sep 2015 00:00:00 GMT' )
|
533
|
+
expect( schedule ).to include( 'Thu, 15 Oct 2015 00:00:00 GMT' )
|
534
|
+
expect( schedule ).to include( 'Mon, 30 Nov 2015 23:59:59 GMT' )
|
535
|
+
expect( schedule ).to include( 'Thu, 31 Dec 2015 23:59:59 GMT' )
|
536
|
+
expect( schedule ).to include( 'Fri, 01 Jan 2016 00:00:00 GMT' )
|
537
|
+
expect( schedule ).to include( 'Thu, 31 Mar 2016 23:59:59 GMT' )
|
538
|
+
expect( schedule ).to_not include( 'Fri, 01 Apr 2016 00:00:00 GMT' )
|
539
|
+
end
|
540
|
+
|
541
|
+
|
542
|
+
it "matches single day-of-year values as a single 24-hour period" do
|
543
|
+
schedule = described_class.parse( "yd {362}" )
|
544
|
+
|
545
|
+
expect( schedule ).to_not include( '2015-12-27T23:59:59-00:00' )
|
546
|
+
expect( schedule ).to include( '2015-12-28T00:00:00-00:00' )
|
547
|
+
expect( schedule ).to include( '2015-12-28T12:00:00-00:00' )
|
548
|
+
expect( schedule ).to include( '2015-12-28T23:59:59-00:00' )
|
549
|
+
expect( schedule ).to_not include( '2015-12-29T00:00:00-00:00' )
|
550
|
+
end
|
551
|
+
|
552
|
+
|
553
|
+
it "matches a range of day-of-year values as an inclusive range" do
|
554
|
+
schedule = described_class.parse( "yd {362-365}" )
|
555
|
+
|
556
|
+
expect( schedule ).to_not include( '2015-12-27T23:59:59-00:00' )
|
557
|
+
expect( schedule ).to include( '2015-12-28T00:00:00-00:00' )
|
558
|
+
expect( schedule ).to include( '2015-12-29T12:00:00-00:00' )
|
559
|
+
expect( schedule ).to include( '2015-12-30T18:00:00-00:00' )
|
560
|
+
expect( schedule ).to include( '2015-12-31T23:59:59-00:00' )
|
561
|
+
expect( schedule ).to_not include( '2016-01-01T00:00:00-00:00' )
|
562
|
+
end
|
563
|
+
|
564
|
+
|
565
|
+
it "matches a wrapped range of day-of-year values as two inclusive ranges" do
|
566
|
+
schedule = described_class.parse( "yd {362-15}" )
|
567
|
+
|
568
|
+
expect( schedule ).to_not include( '2015-12-27T23:59:59-00:00' )
|
569
|
+
expect( schedule ).to include( '2015-12-28T00:00:00-00:00' )
|
570
|
+
expect( schedule ).to include( '2015-12-29T12:00:00-00:00' )
|
571
|
+
expect( schedule ).to include( '2015-12-30T18:00:00-00:00' )
|
572
|
+
expect( schedule ).to include( '2015-12-31T23:59:59-00:00' )
|
573
|
+
expect( schedule ).to include( '2016-01-01T00:00:00-00:00' )
|
574
|
+
expect( schedule ).to include( '2016-01-11T00:00:00-00:00' )
|
575
|
+
expect( schedule ).to include( '2016-01-15T23:59:59-00:00' )
|
576
|
+
expect( schedule ).to_not include( '2016-01-16T00:00:00-00:00' )
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
it "handles day-of-year values during a leap-year correctly" do
|
581
|
+
schedule = described_class.parse( "yd {366}" )
|
582
|
+
|
583
|
+
expect( schedule ).to_not include( '2016-12-30T23:59:59-00:00' )
|
584
|
+
expect( schedule ).to include( '2016-12-31T00:00:00-00:00' )
|
585
|
+
expect( schedule ).to include( '2016-12-31T23:59:59-00:00' )
|
586
|
+
expect( schedule ).to_not include( '2017-01-01T00:00:00-00:00' )
|
587
|
+
end
|
588
|
+
|
589
|
+
|
590
|
+
it "matches single year values as a single-year exclusive range" do
|
591
|
+
schedule = described_class.parse( "yr {2019}" )
|
592
|
+
|
593
|
+
expect( schedule ).to_not include( '2018-12-31T23:59:59-00:00' )
|
594
|
+
expect( schedule ).to include( '2019-01-01T00:00:00-00:00' )
|
595
|
+
expect( schedule ).to include( '2019-06-15T00:00:00-00:00' )
|
596
|
+
expect( schedule ).to include( '2019-12-31T23:59:59-00:00' )
|
597
|
+
expect( schedule ).to_not include( '2020-01-01T00:00:00-00:00' )
|
598
|
+
end
|
599
|
+
|
600
|
+
|
601
|
+
it "matches year range values as multi-year inclusive ranges" do
|
602
|
+
schedule = described_class.parse( "yr {2009-2015}" )
|
603
|
+
|
604
|
+
expect( schedule ).to_not include( '2008-12-31T23:59:59-00:00' )
|
605
|
+
expect( schedule ).to include( '2009-01-01T00:00:00-00:00' )
|
606
|
+
expect( schedule ).to include( '2011-06-15T00:00:00-00:00' )
|
607
|
+
expect( schedule ).to include( '2015-12-31T23:59:59-00:00' )
|
608
|
+
expect( schedule ).to_not include( '2016-01-01T00:00:00-00:00' )
|
609
|
+
end
|
610
|
+
|
611
|
+
|
612
|
+
it "matches negative year range values as multi-year inclusive ranges" do
|
613
|
+
schedule = described_class.parse( "! yr {2009-2015}" )
|
614
|
+
|
615
|
+
expect( schedule ).to include( '2008-12-31T23:59:59-00:00' )
|
616
|
+
expect( schedule ).to_not include( '2009-01-01T00:00:00-00:00' )
|
617
|
+
expect( schedule ).to_not include( '2011-06-15T00:00:00-00:00' )
|
618
|
+
expect( schedule ).to_not include( '2015-12-31T23:59:59-00:00' )
|
619
|
+
expect( schedule ).to include( '2016-01-01T00:00:00-00:00' )
|
620
|
+
end
|
621
|
+
|
622
|
+
|
623
|
+
it "raises an error for wrapped year ranges" do
|
624
|
+
expect {
|
625
|
+
described_class.parse( "yr {2015-2013}" )
|
626
|
+
}.to raise_error( Schedulability::ParseError, /wrapped year range/i )
|
627
|
+
end
|
628
|
+
|
629
|
+
|
630
|
+
it "raises an error for invalid years" do
|
631
|
+
expect {
|
632
|
+
described_class.parse( "yr {76}" )
|
633
|
+
}.to raise_error( Schedulability::ParseError, /invalid year value: 76/i )
|
634
|
+
end
|
635
|
+
|
636
|
+
|
637
|
+
it "raises a parse error for invalid scales" do
|
638
|
+
expect {
|
639
|
+
described_class.parse( 'mil {2}' )
|
640
|
+
}.to raise_error( Schedulability::ParseError, /malformed schedule/i )
|
641
|
+
end
|
642
|
+
|
643
|
+
|
644
|
+
it "raises a parse error for invalid hour periods" do
|
645
|
+
expect {
|
646
|
+
described_class.parse( 'hr {2yt}' )
|
647
|
+
}.to raise_error( Schedulability::ParseError, /invalid hour range: "2yt"/i )
|
648
|
+
expect {
|
649
|
+
described_class.parse( 'hr {14pm}' )
|
650
|
+
}.to raise_error( Schedulability::ParseError, /invalid hour value: "14pm"/i )
|
651
|
+
expect {
|
652
|
+
described_class.parse( 'hr {14am}' )
|
653
|
+
}.to raise_error( Schedulability::ParseError, /invalid hour value: "14am"/i )
|
654
|
+
expect {
|
655
|
+
described_class.parse( 'hr {28}' )
|
656
|
+
}.to raise_error( Schedulability::ParseError, /invalid hour value: "28"/i )
|
657
|
+
end
|
658
|
+
|
659
|
+
|
660
|
+
it "raises a parse error for day of month values greater than 31" do
|
661
|
+
expect {
|
662
|
+
described_class.parse( 'md {11 21 88}' )
|
663
|
+
}.to raise_error( Schedulability::ParseError, /invalid mday value: 88/i )
|
664
|
+
end
|
665
|
+
|
666
|
+
|
667
|
+
it "raises a parse error for day of month ranges greater than 31" do
|
668
|
+
expect {
|
669
|
+
described_class.parse( 'md {28-35}' )
|
670
|
+
}.to raise_error( Schedulability::ParseError, /invalid mday value: 35/i )
|
671
|
+
end
|
672
|
+
|
673
|
+
|
674
|
+
it "raises a parse error for day of week numbers greater than 6" do
|
675
|
+
expect {
|
676
|
+
described_class.parse( 'wd {2 5 7}' )
|
677
|
+
}.to raise_error( Schedulability::ParseError, /invalid wday value: 7/i )
|
678
|
+
end
|
679
|
+
|
680
|
+
|
681
|
+
it "raises a parse error for day of week ranges which include a value greater than 6" do
|
682
|
+
expect {
|
683
|
+
described_class.parse( 'wd {8-2}' )
|
684
|
+
}.to raise_error( Schedulability::ParseError, /invalid wday value: 8/i )
|
685
|
+
end
|
686
|
+
|
687
|
+
|
688
|
+
it "raises a parse error for non-existant month names" do
|
689
|
+
expect {
|
690
|
+
described_class.parse( 'mo {Fit}' )
|
691
|
+
}.to raise_error( Schedulability::ParseError, /invalid month value: "Fit"/i )
|
692
|
+
end
|
693
|
+
|
694
|
+
|
695
|
+
it "raises a parse error for second values greater than 59" do
|
696
|
+
expect {
|
697
|
+
described_class.parse( 'sec {74 18}' )
|
698
|
+
}.to raise_error( Schedulability::ParseError, /invalid second value: 74/i )
|
699
|
+
expect {
|
700
|
+
described_class.parse( 'sec {60}' )
|
701
|
+
}.to raise_error( Schedulability::ParseError, /invalid second value: 60/i )
|
702
|
+
end
|
703
|
+
|
704
|
+
|
705
|
+
it "raises a parse error for second ranges with invalid values" do
|
706
|
+
expect {
|
707
|
+
described_class.parse( 'sec {55-60}' )
|
708
|
+
}.to raise_error( Schedulability::ParseError, /invalid second value: 60/i )
|
709
|
+
end
|
710
|
+
|
711
|
+
|
712
|
+
it "raises a parse error for minute values greater than 59" do
|
713
|
+
expect {
|
714
|
+
described_class.parse( 'min {09 28 68}' )
|
715
|
+
}.to raise_error( Schedulability::ParseError, /invalid minute value: 68/i )
|
716
|
+
expect {
|
717
|
+
described_class.parse( 'min {60}' )
|
718
|
+
}.to raise_error( Schedulability::ParseError, /invalid minute value: 60/i )
|
719
|
+
end
|
720
|
+
|
721
|
+
|
722
|
+
it "raises a parse error for minute ranges with invalid values" do
|
723
|
+
expect {
|
724
|
+
described_class.parse( 'min {120-15}' )
|
725
|
+
}.to raise_error( Schedulability::ParseError, /invalid minute value: 120/i )
|
726
|
+
end
|
727
|
+
|
728
|
+
|
729
|
+
it "raises a parse error for week values greater than 5" do
|
730
|
+
expect {
|
731
|
+
described_class.parse( 'wk {7}' )
|
732
|
+
}.to raise_error( Schedulability::ParseError, /invalid week value: 7/i )
|
733
|
+
end
|
734
|
+
|
735
|
+
|
736
|
+
it "raises a parse error for week ranges that have a value greater than 5" do
|
737
|
+
expect {
|
738
|
+
described_class.parse( 'wk {2-11}' )
|
739
|
+
}.to raise_error( Schedulability::ParseError, /invalid week value: 11/i )
|
740
|
+
end
|
741
|
+
|
742
|
+
|
743
|
+
it "supports pluralization syntactic sugar" do
|
744
|
+
expect(
|
745
|
+
described_class.parse("years {2001 2011 2021} months {Jul Sep}")
|
746
|
+
).to be_a( described_class )
|
747
|
+
end
|
748
|
+
|
749
|
+
it "ignores whitespace in range values" do
|
750
|
+
schedule = described_class.parse( "sec { 18 - 55 }" )
|
751
|
+
expect( schedule ).to be_a( described_class )
|
752
|
+
end
|
753
|
+
|
754
|
+
end
|
755
|
+
|
756
|
+
|
757
|
+
describe "mutators" do
|
758
|
+
|
759
|
+
|
760
|
+
it "can calculate the union of two schedules" do
|
761
|
+
schedule1 = described_class.parse( 'md {1-15}' )
|
762
|
+
schedule2 = described_class.parse( 'month {Feb-Jul}' )
|
763
|
+
schedule3 = schedule1 | schedule2
|
764
|
+
|
765
|
+
expect( schedule3 ).to be == described_class.parse( 'md {1-15}, month {Feb-Jul}' )
|
766
|
+
end
|
767
|
+
|
768
|
+
|
769
|
+
it "can calculate the intersection of two schedules" do
|
770
|
+
schedule1 = described_class.parse( 'md {1-15} month {Mar-Jun}' )
|
771
|
+
schedule2 = described_class.parse( 'md {10-20} month {Feb-Jul}' )
|
772
|
+
schedule3 = schedule1 & schedule2
|
773
|
+
|
774
|
+
expect( schedule3 ).to be == described_class.parse( 'md {10-15} month {Mar-Jun}' )
|
775
|
+
end
|
776
|
+
|
777
|
+
|
778
|
+
it "returns an empty schedule as the intersection of two non-overlapping schedules" do
|
779
|
+
schedule1 = described_class.parse( 'hr {6am-8am} wday {Mon Wed Fri}' )
|
780
|
+
schedule2 = described_class.parse( 'wday {Thu Sat}' )
|
781
|
+
schedule3 = schedule1 & schedule2
|
782
|
+
|
783
|
+
expect( schedule3 ).to be_empty
|
784
|
+
end
|
785
|
+
|
786
|
+
|
787
|
+
it "can calculate unions of schedules with negated periods" do
|
788
|
+
schedule1 = described_class.parse( '! wday { Mon-Fri }' )
|
789
|
+
schedule2 = described_class.parse( '! wday { Thu }' )
|
790
|
+
schedule3 = schedule1 | schedule2
|
791
|
+
|
792
|
+
expect( schedule3 ).to be == schedule2
|
793
|
+
end
|
794
|
+
|
795
|
+
|
796
|
+
it "can calculate unions of schedules with negated periods that don't overlap" do
|
797
|
+
schedule1 = described_class.parse( '! wday { Wed }' )
|
798
|
+
schedule2 = described_class.parse( '! wday { Thu }' )
|
799
|
+
schedule3 = schedule1 | schedule2
|
800
|
+
|
801
|
+
expect( schedule3 ).to be_empty
|
802
|
+
end
|
803
|
+
|
804
|
+
|
805
|
+
it "can calculate intersections of schedules with negated periods" do
|
806
|
+
schedule1 = described_class.parse( '! wday { Wed }' )
|
807
|
+
schedule2 = described_class.parse( '! wday { Thu }' )
|
808
|
+
schedule3 = schedule1 & schedule2
|
809
|
+
|
810
|
+
expect( schedule3 ).to be == described_class.parse( '! wday {Wed}, ! wday {Thu}' )
|
811
|
+
end
|
812
|
+
|
813
|
+
|
814
|
+
it "can calculate the inverse of a schedule" do
|
815
|
+
schedule1 = described_class.parse( 'hr {8am-4pm} md {10-15}' )
|
816
|
+
schedule2 = ~schedule1
|
817
|
+
|
818
|
+
expect( schedule2 ).to include( '2015-01-09T08:00:00-00:00' )
|
819
|
+
expect( schedule2 ).to include( '2015-01-10T07:59:59-00:00' )
|
820
|
+
expect( schedule2 ).to_not include( '2015-01-10T08:00:00-00:00' )
|
821
|
+
expect( schedule2 ).to_not include( '2015-01-15T15:59:59-00:00' )
|
822
|
+
expect( schedule2 ).to include( '2015-01-15T16:00:00-00:00' )
|
823
|
+
end
|
824
|
+
|
825
|
+
end
|
826
|
+
|
827
|
+
end
|
828
|
+
|