Module: Schedulability::Parser

Extended by:
Loggability
Defined in:
lib/schedulability/parser.rb

Overview

A collection of parsing functions for Schedulability schedule syntax.

Constant Summary collapse

VALID_SCALES =

A Regexp that will match valid period scale codes

Regexp.union(%w[
	year   yr
	month  mo
	week   wk
	yday   yd
	mday   md
	wday   wd
	hour   hr
	minute min
	second sec
])
EXCLUSIVE_RANGED_SCALES =

Scales that are parsed with exclusive end values.

%i[ hour hr minute min second sec ]
PERIOD_PATTERN =

The Regexp for matching value periods

%r:
	(\A|\G\s+) # beginning of the string or the end of the last match
	(?<scale> #{VALID_SCALES} )
	s? # Optional plural sugar
	\s*
	\{
		(?<ranges>.*?)
	\}
:ix
TIME_VALUE_PATTERN =

Pattern for matching hour-scale values

/\A(?<hour>\d+)(?<qualifier>am|pm|noon)?\z/i
ABBR_DAYNAMES =

Downcased day-name Arrays

Date::ABBR_DAYNAMES.map( &:downcase )
DAYNAMES =
Date::DAYNAMES.map( &:downcase )
ABBR_MONTHNAMES =

Downcased month-name Arrays

Date::ABBR_MONTHNAMES.map {|val| val && val.downcase }
MONTHNAMES =
Date::MONTHNAMES.map {|val| val && val.downcase }

Class Method Summary collapse

Class Method Details

.coalesce_ranges(ints, scale) ⇒ Object

Coalese an Array of non-contiguous Range objects from the specified ints for scale.



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/schedulability/parser.rb', line 262

def coalesce_ranges( ints, scale )
	exclude_end = EXCLUSIVE_RANGED_SCALES.include?( scale )
	self.log.debug "Coalescing %d ints to Ranges (%p, %s)" %
		[ ints.size, ints, exclude_end ? "exclusive" : "inclusive" ]
	ints.flatten!
	return [] if ints.empty?

	prev = ints[0]
	range_ints = ints.sort.slice_before do |v|
		prev, prev2 = v, prev
		prev2.succ != v
	end

	return range_ints.map do |values|
		last_val = values.last
		last_val += 1 if exclude_end
		Range.new( values.first, last_val, exclude_end )
	end.tap do |ranges|
		self.log.debug "Coalesced range integers to Ranges: %p" % [ ranges ]
	end
end

.extract_hour_ranges(ranges) ⇒ Object

Return an Array of 24-hour Integer Ranges for the specified ranges expression.



181
182
183
184
185
# File 'lib/schedulability/parser.rb', line 181

def extract_hour_ranges( ranges )
	return self.extract_ranges( :hour, ranges, 0, 24 ) do |val|
		self.extract_hour_value( val )
	end
end

.extract_hour_value(time_value) ⇒ Object

Return the integer equivalent of the specified time_value.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/schedulability/parser.rb', line 205

def extract_hour_value( time_value )
	unless match = TIME_VALUE_PATTERN.match( time_value )
		raise Schedulability::ParseError, "invalid hour range: %p" % [ time_value ]
	end

	hour, qualifier = match[:hour], match[:qualifier]
	hour = hour.to_i

	if qualifier
		raise Schedulability::RangeError, "invalid hour value: %p" % [ time_value ] if
			hour > 12
		hour += 12 if qualifier == 'pm' && hour < 12
	else
		raise Schedulability::RangeError, "invalid hour value: %p" % [ time_value ] if
			hour > 24
		hour = 24 if hour.zero?
	end

	return hour
end

.extract_mday_ranges(ranges) ⇒ Object

Return an Array of day-of-month Integer Ranges for the specified ranges expression.



165
166
167
168
169
# File 'lib/schedulability/parser.rb', line 165

def extract_mday_ranges( ranges )
	return self.extract_ranges( :mday, ranges, 0, 31 ) do |val|
		Integer( strip_leading_zeros(val) )
	end
end

.extract_minute_ranges(ranges) ⇒ Object

Return an Array of Integer minute Ranges for the specified ranges expression.



189
190
191
192
193
# File 'lib/schedulability/parser.rb', line 189

def extract_minute_ranges( ranges )
	return self.extract_ranges( :minute, ranges, 0, 59 ) do |val|
		Integer( strip_leading_zeros(val) )
	end
end

.extract_month_ranges(ranges) ⇒ Object

Return an Array of month Integer Ranges for the specified ranges expression.



141
142
143
144
145
# File 'lib/schedulability/parser.rb', line 141

def extract_month_ranges( ranges )
	return self.extract_ranges( :month, ranges, 0, MONTHNAMES.size - 1 ) do |val|
		self.map_integer_value( :month, val, [ABBR_MONTHNAMES, MONTHNAMES] )
	end
end

.extract_period(expression) ⇒ Object

Return the specified period expression as a Hash of Ranges keyed by scale.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/schedulability/parser.rb', line 79

def extract_period( expression )
	hash = {}
	scanner = StringScanner.new( expression )

	negative = scanner.skip( /\s*(!|not |except )\s*/ )
	self.log.debug "Period %p is %snegative!" % [ expression, negative ? "" : "not " ]

	while scanner.scan( PERIOD_PATTERN )
		ranges = scanner[:ranges].strip
		scale = scanner[:scale]

		case scale
		when 'year',   'yr'
			hash[:yr] = self.extract_year_ranges( ranges )
		when 'month',  'mo'
			hash[:mo] = self.extract_month_ranges( ranges )
		when 'week',   'wk'
			hash[:wk] = self.extract_week_ranges( ranges )
		when 'yday',   'yd'
			hash[:yd] = self.extract_yday_ranges( ranges )
		when 'mday',   'md'
			hash[:md] = self.extract_mday_ranges( ranges )
		when 'wday',   'wd'
			hash[:wd] = self.extract_wday_ranges( ranges )
		when 'hour',   'hr'
			hash[:hr] = self.extract_hour_ranges( ranges )
		when 'minute', 'min'
			hash[:min] = self.extract_minute_ranges( ranges )
		when 'second', 'sec'
			hash[:sec] = self.extract_second_ranges( ranges )
		else
			# This should never happen
			raise ArgumentError, "Unhandled scale %p!" % [ scale ]
		end
	end

	unless scanner.eos?
		raise Schedulability::ParseError,
			"malformed schedule (at %d: %p)" % [ scanner.pos, scanner.rest ]
	end

	return hash, negative
ensure
	scanner.terminate if scanner
end

.extract_periods(expression) ⇒ Object

Scan expression for periods and return them in an Array.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/schedulability/parser.rb', line 60

def extract_periods( expression )
	positive_periods = []
	negative_periods = []

	expression.strip.downcase.split( /\s*,\s*/ ).each do |subexpr|
		hash, negative = self.extract_period( subexpr )
		if negative
			self.log.debug "Adding %p to the negative "
			negative_periods << hash
		else
			positive_periods << hash
		end
	end

	return positive_periods, negative_periods
end

.extract_ranges(scale, ranges, minval, maxval) ⇒ Object

Extract an Array of Ranges from the specified ranges string using the given index_arrays for non-numeric values. Construct the Ranges with the given minval/maxval range boundaries.



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/schedulability/parser.rb', line 230

def extract_ranges( scale, ranges, minval, maxval )
	exclude_end = EXCLUSIVE_RANGED_SCALES.include?( scale )
	valid_range = Range.new( minval, maxval, exclude_end )

	ints = ranges.split( /(?<!-)\s+(?!-)/ ).flat_map do |range|
		min, max = range.split( /\s*-\s*/, 2 )
		self.log.debug "Min = %p, max = %p" % [ min, max ]

		min = yield( min )
		raise Schedulability::ParseError, "invalid %s value: %p" % [ scale, min ] unless
			valid_range.cover?( min )
		next [ min ] unless max

		max = yield( max )
		raise Schedulability::ParseError, "invalid %s value: %p" % [ scale, max ] unless
			valid_range.cover?( max )
		self.log.debug "Parsed min = %p, max = %p" % [ min, max ]

		if min > max
			self.log.debug "wrapped: %d-%d and %d-%d" % [ minval, max, min, maxval ]
			Range.new( minval, max, exclude_end ).to_a +
				Range.new( min, maxval, false ).to_a
		else
			Range.new( min, max, exclude_end ).to_a
		end
	end

	return self.coalesce_ranges( ints, scale )
end

.extract_second_ranges(ranges) ⇒ Object

Return an Array of Integer second Ranges for the specified ranges expression.



197
198
199
200
201
# File 'lib/schedulability/parser.rb', line 197

def extract_second_ranges( ranges )
	return self.extract_ranges( :second, ranges, 0, 59 ) do |val|
		Integer( strip_leading_zeros(val) )
	end
end

.extract_wday_ranges(ranges) ⇒ Object

Return an Array of weekday Integer Ranges for the specified ranges expression.



173
174
175
176
177
# File 'lib/schedulability/parser.rb', line 173

def extract_wday_ranges( ranges )
	return self.extract_ranges( :wday, ranges, 0, DAYNAMES.size - 1 ) do |val|
		self.map_integer_value( :wday, val, [ABBR_DAYNAMES, DAYNAMES] )
	end
end

.extract_week_ranges(ranges) ⇒ Object

Return an Array of week-of-month Integer Ranges for the specified ranges expression.



149
150
151
152
153
# File 'lib/schedulability/parser.rb', line 149

def extract_week_ranges( ranges )
	return self.extract_ranges( :week, ranges, 1, 5 ) do |val|
		Integer( strip_leading_zeros(val) )
	end
end

.extract_yday_ranges(ranges) ⇒ Object

Return an Array of day-of-year Integer Ranges for the specified ranges expression.



157
158
159
160
161
# File 'lib/schedulability/parser.rb', line 157

def extract_yday_ranges( ranges )
	return self.extract_ranges( :yday, ranges, 1, 366 ) do |val|
		Integer( strip_leading_zeros(val) )
	end
end

.extract_year_ranges(ranges) ⇒ Object

Return an Array of year integer Ranges for the specified ranges expression.



127
128
129
130
131
132
133
134
135
136
137
# File 'lib/schedulability/parser.rb', line 127

def extract_year_ranges( ranges )
	ranges = self.extract_ranges( :year, ranges, 2000, 9999 ) do |val|
		Integer( val )
	end

	if ranges.any? {|rng| rng.end == 9999 }
		raise Schedulability::ParseError, "no support for wrapped year ranges"
	end

	return ranges
end

.map_integer_value(scale, value, index_arrays) ⇒ Object

Map a value from a period’s range to an Integer, using the specified index_arrays if it doesn’t look like an integer string.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/schedulability/parser.rb', line 287

def map_integer_value( scale, value, index_arrays )
	return Integer( value ) if value =~ /\A\d+\z/

	unless index = index_arrays.inject( nil ) {|res, ary| res || ary.index(value) }
		expected = "expected one of: %s, %d-%d" % [
			index_arrays.flatten.compact.flatten.join( ', ' ),
			index_arrays.first.index {|val| val },
			index_arrays.first.size - 1
		]
		raise Schedulability::ParseError, "invalid %s value: %p (%s)" %
			[ scale, value, expected ]
	end

	return index
end

.strip_leading_zeros(val) ⇒ Object

Return a copy of the specified val with any leading zeros stripped.



305
306
307
# File 'lib/schedulability/parser.rb', line 305

def strip_leading_zeros( val )
	return val.sub( /\A0+/, '' )
end