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.



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/schedulability/parser.rb', line 291

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
252
253
254
255
256
# 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

    if qualifier == 'am' && hour == 12
      hour = 0
    elsif qualifier == 'pm' && hour < 12
      hour += 12
    end

  else
    raise Schedulability::RangeError, "invalid hour value: %p" % [ time_value ] if
      hour < 0 || hour > 24
  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.



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

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.



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/schedulability/parser.rb', line 312

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 = String.new( encoding: 'utf-8' )
      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”.



331
332
333
# File 'lib/schedulability/parser.rb', line 331

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