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.

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.


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

def coalesce_ranges( ints, scale )
	exclude_end = EXCLUSIVE_RANGED_SCALES.include?( scale )
	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
end

.extract_hour_ranges(ranges) ⇒ Object

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


208
209
210
211
212
# File 'lib/schedulability/parser.rb', line 208

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.


232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/schedulability/parser.rb', line 232

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.


192
193
194
195
196
# File 'lib/schedulability/parser.rb', line 192

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.


216
217
218
219
220
# File 'lib/schedulability/parser.rb', line 216

def extract_minute_ranges( ranges )
	return self.extract_ranges( :minute, ranges, 0, 60 ) 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.


168
169
170
171
172
# File 'lib/schedulability/parser.rb', line 168

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.


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/schedulability/parser.rb', line 107

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

	negative = scanner.skip( /\s*(!|not |except )\s*/ )

	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.


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/schedulability/parser.rb', line 89

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


257
258
259
260
261
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 257

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 )

		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 )

		if min > max
			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.


224
225
226
227
228
# File 'lib/schedulability/parser.rb', line 224

def extract_second_ranges( ranges )
	return self.extract_ranges( :second, ranges, 0, 60 ) 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.


200
201
202
203
204
# File 'lib/schedulability/parser.rb', line 200

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.


176
177
178
179
180
# File 'lib/schedulability/parser.rb', line 176

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.


184
185
186
187
188
# File 'lib/schedulability/parser.rb', line 184

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.


154
155
156
157
158
159
160
161
162
163
164
# File 'lib/schedulability/parser.rb', line 154

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.


307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/schedulability/parser.rb', line 307

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

.stringify(periods) ⇒ Object

Normalize an array of parsed periods into a human readable string.


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/schedulability/parser.rb', line 60

def stringify( periods )
	strings = []
	periods.each do |period|
		period_string = []
		period.sort_by{|k, v| k}.each do |scale, ranges|
			range_string = ""
			range_string << "%s { " % [ scale.to_s ]

			range_strings = ranges.each_with_object( [] ).each do |range, acc|
				if range.min == range.max
					acc << range.min
				elsif range.exclude_end?
					acc << "%d-%d" % [ range.min, range.max + 1 ]
				else
					acc << "%d-%d" % [ range.min, range.max ]
				end
			end

			range_string << range_strings.join( ' ' ) << " }"
			period_string << range_string
		end
		strings << period_string.join( ' ' )
	end

	return strings.join( ', ' )
end

.strip_leading_zeros(val) ⇒ Object

Return a copy of the specified val with any leading zeros stripped. If the resulting string is empty, return “0”.


326
327
328
# File 'lib/schedulability/parser.rb', line 326

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