cron-parser 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +3 -0
- data/LICENSE.md +9 -0
- data/README.md +125 -0
- data/Rakefile +13 -0
- data/cron-parser.gemspec +23 -0
- data/lib/cron.rb +12 -0
- data/lib/cron/parser.rb +102 -0
- data/lib/cron/parser/day_of_month_field.rb +20 -0
- data/lib/cron/parser/day_of_week_field.rb +51 -0
- data/lib/cron/parser/field.rb +213 -0
- data/lib/cron/parser/hour_field.rb +56 -0
- data/lib/cron/parser/minute_field.rb +22 -0
- data/lib/cron/parser/month_field.rb +61 -0
- data/lib/cron/parser/version.rb +5 -0
- data/lib/extras/custom_errors.rb +7 -0
- data/lib/extras/extensions.rb +35 -0
- data/spec/cron/parser_spec.rb +132 -0
- data/spec/spec_helper.rb +8 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MmU1MjNiNWI4Mzc3MjQ0NjZmNGEyODljM2QzMDkyZWExY2M4NDYxNA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YmUzOGY2MzkwYjYxMWVlM2FjMGVmNjMxNDU5NjNhZDlhMjc0YmViYg==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ODQxOWMwNDdlOTQxNTM5OWQyNTBkODE0ZjRlZTliNmE1MDJmNzRhYzVkMDYz
|
10
|
+
MGVkYWU1MjU2MTk5MjFkODk3ZDk4N2E0OTlhY2Q3OTZiYzdlYjcxOTBjNWE4
|
11
|
+
MDY1MWNlZDA0YTQxNjJjNjZjMjY4ZjIyZTBhZjYzYzljZDgyNjI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZjcyZjVmN2ZmZmE3MjA4MzMxNzEwMGJiODI1Yjc4ODVkMmM3N2MyNzg3YTk4
|
14
|
+
NTM5YjU3MTcyNWZlODlkOGQ3MDM4ZjU4OGUyMDNjNTMwY2M4OTljNzEzOWMy
|
15
|
+
YWI3NmExMTk2ZTI1MmQxNDM0NWNjZTllY2E2MmM1MWZhM2VhYWU=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2013 Vishal Telangre.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
Cron::Parser
|
2
|
+
============
|
3
|
+
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/cron-parser.png)](http://badge.fury.io/rb/cron-parser)
|
5
|
+
[![Build Status](https://travis-ci.org/vishaltelangre/cron-parser.png?branch=master)](https://travis-ci.org/vishaltelangre/cron-parser)
|
6
|
+
|
7
|
+
Dissect your Cron pattern!
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Install gem by using following command:
|
12
|
+
|
13
|
+
gem install cron-parser
|
14
|
+
|
15
|
+
or add it to your Gemfile as:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'cron-parser', require: 'cron'
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Well, using `Cron::Parser` is really easy, take a look at following example:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require 'cron'
|
27
|
+
|
28
|
+
# so you have got a pattern, which you want to dissect:
|
29
|
+
dirty_cron_pattern = "38,37/40,39 */6,01,2 21-30/3,30,31 * MON,2-4"
|
30
|
+
|
31
|
+
# ask `Cron::Parser` to dissect it, and it will do that nasty job for you:
|
32
|
+
|
33
|
+
>> parsed_pattern = Cron::Parser.new(dirty_cron_pattern)
|
34
|
+
# => #<Cron::Parser:0x973860c> { :pattern => "38,37/40,39 */6,01,2 21-30/3,30,31 * MON,2-4", :fields => {:minute=>#<Cron::Parser::MinuteField:0x9736ab4> { :pattern => "38,37/40,39", :warning => '37/40' is valid but confusing pattern, :meaning => "at 37th, 38th minute" }, :hour=>#<Cron::Parser::HourField:0x9743bd8> { :pattern => "*/6,01,2", :meaning => "on 12am, 1am, 2am, 6am, 12pm, 6pm" }, :day_of_month=>#<Cron::Parser::DayOfMonthField:0x9537768> { :pattern => "21-30/3,30,31", :meaning => "on days: 21st, 24th, 27th, 30th, 30th, 31st" }, :month=>#<Cron::Parser::MonthField:0x9586110> { :pattern => "*", :meaning => "every month" }, :day_of_week=>#<Cron::Parser::DayOfWeekField:0x958f224> { :pattern => "MON,2-4", :meaning => "on Tuesday, Wednsday, Thursday, Monday" }} }
|
35
|
+
|
36
|
+
# you can do it other way too, if you like:
|
37
|
+
|
38
|
+
>> Cron::Parser.parse(dirty_cron_pattern)
|
39
|
+
# => "at 37th, 38th minute; on 12am, 1am, 2am, 6am, 12pm, 6pm; on days: 21st, 24th, 27th, 30th, 30th, 31st; every month; on Tuesday, Wednsday, Thursday, Monday"
|
40
|
+
|
41
|
+
# this is same as below:
|
42
|
+
|
43
|
+
>> parsed_pattern.meaning
|
44
|
+
# => "at 37th, 38th minute; on 12am, 1am, 2am, 6am, 12pm, 6pm; on days: 21st, 24th, 27th, 30th, 30th, 31st; every month; on Tuesday, Wednsday, Thursday, Monday"
|
45
|
+
|
46
|
+
# also, `parsed_pattern.humanize` produces the same result as above.
|
47
|
+
|
48
|
+
# further exploration methods you can use:
|
49
|
+
|
50
|
+
>> parsed_pattern.fields
|
51
|
+
# => {:minute=>#<Cron::Parser::MinuteField:0x9736ab4> { :pattern => "38,37/40,39", :warning => '37/40' is valid but confusing pattern, :meaning => "at 37th, 38th minute" }, :hour=>#<Cron::Parser::HourField:0x9743bd8> { :pattern => "*/6,01,2", :meaning => "on 12am, 1am, 2am, 6am, 12pm, 6pm" }, :day_of_month=>#<Cron::Parser::DayOfMonthField:0x9537768> { :pattern => "21-30/3,30,31", :meaning => "on days: 21st, 24th, 27th, 30th, 30th, 31st" }, :month=>#<Cron::Parser::MonthField:0x9586110> { :pattern => "*", :meaning => "every month" }, :day_of_week=>#<Cron::Parser::DayOfWeekField:0x958f224> { :pattern => "MON,2-4", :meaning => "on Tuesday, Wednsday, Thursday, Monday" }}
|
52
|
+
|
53
|
+
>> parsed_pattern.minute_field
|
54
|
+
# => #<Cron::Parser::MinuteField:0x9736ab4> { :pattern => "38,37/40,39", :warning => '37/40' is valid but confusing pattern, :meaning => "at 37th, 38th minute" }
|
55
|
+
|
56
|
+
# similarly, see: `parsed_pattern.hour_field`, `parsed_pattern.day_of_month_field`, `parsed_pattern.month_field`, `parsed_pattern.day_of_week_field`
|
57
|
+
|
58
|
+
>> parsed_pattern.minute_field.pattern
|
59
|
+
# => "38,37/40,39"
|
60
|
+
|
61
|
+
>> parsed_pattern.minute_field.meaning
|
62
|
+
# => "at 37th, 38th minute"
|
63
|
+
|
64
|
+
>> parsed_pattern.minute_field.warning
|
65
|
+
# => "'37/40' is valid but confusing pattern"
|
66
|
+
|
67
|
+
>> parsed_pattern.warnings
|
68
|
+
# => ["for 'minute' field: '37/40' is valid but confusing pattern"]
|
69
|
+
```
|
70
|
+
|
71
|
+
What about wrong or invalid patterns, huh?
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
>> Cron::Parser.new(1)
|
75
|
+
# raises InvalidCronPatternError: cron pattern must be a string
|
76
|
+
|
77
|
+
>> Cron::Parser.new("* * *")
|
78
|
+
# raises InvalidCronPatternError: cron pattern must contain exact 5 fields seperated by whitespaces
|
79
|
+
|
80
|
+
>> Cron::Parser.new("* * * * 1-10")
|
81
|
+
# raises InvalidDayOfWeekFieldError: value: '10' not allowed for 'day_of_week' field, run: 'Cron::Parser::DayOfWeekField.allowed_values' to know valid values
|
82
|
+
|
83
|
+
>> Cron::Parser::DayOfWeekField.allowed_values
|
84
|
+
# => ["0", "1", "2", "3", "4", "5", "6", "7", "00", "01", "02", "03", "04", "05", "06", "07", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]
|
85
|
+
|
86
|
+
>> Cron::Parser::DayOfWeekField.allowed_special_characters
|
87
|
+
# => ["*", "/", ",", "-"]
|
88
|
+
```
|
89
|
+
|
90
|
+
## Important Notes
|
91
|
+
* This parser is based upon the specifications of `crontab(5)`.
|
92
|
+
* Following are the pattern fields, and respective allowed values:
|
93
|
+
|
94
|
+
* * * * *
|
95
|
+
┬ ┬ ┬ ┬ ┬
|
96
|
+
│ │ │ │ │
|
97
|
+
│ │ │ │ │
|
98
|
+
│ │ │ │ └───── day_of_week (0-7) (0 or 7 is Sun, or use 3-letter names)
|
99
|
+
│ │ │ └────────── month (1-12, or use 3-letter names)
|
100
|
+
│ │ └─────────────── day_of_month (1-31)
|
101
|
+
│ └──────────────────── hour (0-23)
|
102
|
+
└───────────────────────── minute (0-59)
|
103
|
+
|
104
|
+
* When specifying day of week, both day 0 and day 7 will be considered Sunday.
|
105
|
+
* Ranges & Lists of numbers are allowed.
|
106
|
+
* Ranges or lists of names are not allowed.
|
107
|
+
* Ranges can include 'steps', so `1-9/2` is the same as `1,3,5,7,9`.
|
108
|
+
* Months or days of the week can be specified by name.
|
109
|
+
* Use the first three letters of the particular day or month (case doesn't matter).
|
110
|
+
|
111
|
+
## Contributing
|
112
|
+
|
113
|
+
You're encouraged to contribute to this gem.
|
114
|
+
|
115
|
+
* Fork this project.
|
116
|
+
* Make changes, write tests (run `rake` to check existing test coverage).
|
117
|
+
* Report bugs, comment on and close open issues.
|
118
|
+
* Update [CHANGELOG](CHANGELOG.md).
|
119
|
+
* Make a pull request, bonus points for topic branches.
|
120
|
+
|
121
|
+
## Copyright and License
|
122
|
+
|
123
|
+
Copyright (c) 2013, Vishal Telangre and [Contributors](CHANGELOG.md). All Rights Reserved.
|
124
|
+
|
125
|
+
This project is licenced under the [MIT License](LICENSE.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
|
6
|
+
require 'rspec/core'
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
10
|
+
spec.pattern = FileList["spec/**/*_spec.rb"]
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => :spec
|
data/cron-parser.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
$:.push File.expand_path("../lib/extras", __FILE__)
|
3
|
+
$:.push File.expand_path("../lib/cron", __FILE__)
|
4
|
+
$:.push File.expand_path("../lib/cron/parser", __FILE__)
|
5
|
+
|
6
|
+
require "cron/parser/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = "cron-parser"
|
10
|
+
s.version = Cron::Parser::VERSION
|
11
|
+
s.authors = [ "Vishal Telangre" ]
|
12
|
+
s.email = "the@vishaltelangre.com"
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
s.required_rubygems_version = '>= 1.3.6'
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.require_paths = [ "lib", "lib/extras", "lib/cron", "lib/cron/parser" ]
|
17
|
+
s.homepage = "http://github.com/vishaltelangre/cron-parser"
|
18
|
+
s.licenses = [ "MIT" ]
|
19
|
+
s.summary = "Dissect your Cron patterns!"
|
20
|
+
s.add_development_dependency "rake"
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_dependency "activesupport"
|
23
|
+
end
|
data/lib/cron.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "active_support/inflector"
|
2
|
+
require_relative "extras/custom_errors"
|
3
|
+
require_relative "extras/extensions"
|
4
|
+
require_relative "cron/parser"
|
5
|
+
require_relative "cron/parser/field"
|
6
|
+
require_relative "cron/parser/minute_field"
|
7
|
+
require_relative "cron/parser/hour_field"
|
8
|
+
require_relative "cron/parser/day_of_month_field"
|
9
|
+
require_relative "cron/parser/month_field"
|
10
|
+
require_relative "cron/parser/day_of_week_field"
|
11
|
+
|
12
|
+
class Cron; end
|
data/lib/cron/parser.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
class Cron
|
2
|
+
class Parser
|
3
|
+
attr_reader :pattern,
|
4
|
+
:fields,
|
5
|
+
:meaning, # umm, let me think about it
|
6
|
+
:warnings
|
7
|
+
|
8
|
+
FIELDS = %w{ minute hour day_of_month month day_of_week }
|
9
|
+
|
10
|
+
def initialize(pattern = nil)
|
11
|
+
@pattern = pattern
|
12
|
+
@fields = {}; FIELDS.map { |field| @fields[field.to_sym] = nil }
|
13
|
+
validate! # and don't ask to dissect it!
|
14
|
+
@meaning = humanize # being human, wtf!
|
15
|
+
end
|
16
|
+
|
17
|
+
# Inspects the parsed pattern, displays fields in pattern along with their
|
18
|
+
# meanings.
|
19
|
+
def inspect
|
20
|
+
%Q{
|
21
|
+
#<#{self.class.name}:#{Object::o_hexy_id(self)}>
|
22
|
+
{
|
23
|
+
:pattern => "#@pattern",
|
24
|
+
:fields => #{@fields.inspect}
|
25
|
+
}
|
26
|
+
}.squish
|
27
|
+
end
|
28
|
+
|
29
|
+
# Alias of `meaning` getter method.
|
30
|
+
# Returns human readable meaning for cron pattern after parsing it.
|
31
|
+
def humanize
|
32
|
+
@fields.collect do |_, field|
|
33
|
+
field.meaning
|
34
|
+
end.join("; ").chomp("; ")
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns warnings in pattern fields if any, occured while parsing those
|
38
|
+
# fields.
|
39
|
+
def warnings
|
40
|
+
warnings = @fields.collect do |_, field|
|
41
|
+
"for '#{field.field_name}' field: #{field.warning}" if field.warning
|
42
|
+
end.compact.join(", ").chomp(", ").split(", ")
|
43
|
+
end
|
44
|
+
|
45
|
+
# Alias of `new` method.
|
46
|
+
def self.parse(pattern = nil) # oh, that sounds ridiculous!
|
47
|
+
self.new(pattern).humanize
|
48
|
+
end
|
49
|
+
|
50
|
+
def method_missing(method, *args, &block)
|
51
|
+
super unless field_methods.include? method
|
52
|
+
self.class.send(:define_method, method) do
|
53
|
+
@fields[method.to_s.sub('_field', '').to_sym]
|
54
|
+
end and self.send(method, *args)
|
55
|
+
end
|
56
|
+
|
57
|
+
def respond_to_missing?(method, include_private = false)
|
58
|
+
field_methods.include? method || super
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Internal method to validate the cron pattern. Raises errors if pattern is
|
64
|
+
# invalid.
|
65
|
+
def validate!
|
66
|
+
unless @pattern.kind_of? String
|
67
|
+
raise InvalidCronPatternError.new("cron pattern must be a string".squish)
|
68
|
+
end
|
69
|
+
fix_common_typos! # how nasty!
|
70
|
+
# do you know that cron has some pretty good fields, huh?
|
71
|
+
# go and,
|
72
|
+
validate_fields!
|
73
|
+
end
|
74
|
+
|
75
|
+
# Internal method to validate each field in the pattern.
|
76
|
+
def validate_fields!
|
77
|
+
pattern_fields = @pattern.split
|
78
|
+
if pattern_fields.size != FIELDS.size
|
79
|
+
raise InvalidCronPatternError.new("cron pattern must contain exact
|
80
|
+
#{FIELDS.size} fields seperated by whitespaces".squish)
|
81
|
+
end
|
82
|
+
FIELDS.map.with_index do |field, index|
|
83
|
+
field_class = self.class.name + "::" + field.split("_"). \
|
84
|
+
map(&:capitalize).join + "Field".squish. \
|
85
|
+
classify
|
86
|
+
@fields[field.to_sym] = field_class.safe_constantize. \
|
87
|
+
new(pattern_fields[index])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Internal method to clean up the frequently made typos and mistakes in
|
92
|
+
# the cron pattern.
|
93
|
+
def fix_common_typos!
|
94
|
+
# well, I don't know what kind of typos people do!
|
95
|
+
@pattern.squish!
|
96
|
+
end
|
97
|
+
|
98
|
+
def field_methods
|
99
|
+
self.fields.keys.map{|f| (f.to_s + "_field").to_sym }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Cron::Parser
|
2
|
+
class DayOfMonthField < Field
|
3
|
+
def self.allowed_values; ("1".."9").to_a + ("01".."31").to_a end
|
4
|
+
def self.upper_bound; self.allowed_values.last end
|
5
|
+
def self.lower_bound; self.allowed_values.first end
|
6
|
+
def self.allowed_special_characters; %w{ * / , - } end
|
7
|
+
def self.specifications
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
# Creates partial meaning (sentence) for the day of month field's pattern.
|
12
|
+
def self.generate_meaning(list, unit)
|
13
|
+
meaning = ""
|
14
|
+
meaning += self.field_preposition(unit)
|
15
|
+
meaning += " days: "
|
16
|
+
meaning += list.map(&:to_s).map(&:ordinalize).join(", ")
|
17
|
+
meaning
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class Cron::Parser
|
2
|
+
class DayOfWeekField < Field
|
3
|
+
def self.allowed_values
|
4
|
+
("0".."7").to_a + ("00".."07").to_a + %w{ sun mon tue wed thu fri
|
5
|
+
sat }.map(&:upcase)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.allowed_special_characters; %w{ * / , - } end
|
9
|
+
def self.upper_bound; "0" end
|
10
|
+
def self.lower_bound; "7" end
|
11
|
+
|
12
|
+
# Adds some day of week field-specific extra regular expressions to super
|
13
|
+
# class's `specifications` method.
|
14
|
+
def self.specifications
|
15
|
+
extra_specs = [
|
16
|
+
{
|
17
|
+
rule: /\A(?<day>(sun|mon|tue|wed|thu|fri|sat))\Z/i,
|
18
|
+
yields: ->(day, options) do
|
19
|
+
return [day] if options[:exclude_preposition]
|
20
|
+
return self.generate_meaning([day], options[:unit])
|
21
|
+
end,
|
22
|
+
for_fields: %w{ day_of_week }
|
23
|
+
},
|
24
|
+
]
|
25
|
+
super + extra_specs
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates partial meaning (sentence) for the day of week field's pattern.
|
29
|
+
def self.generate_meaning(list, unit)
|
30
|
+
meaning = ""
|
31
|
+
meaning += self.field_preposition(unit)
|
32
|
+
meaning += " "
|
33
|
+
meaning += list.map{ |d| self.ascii_weekday(d) }.uniq.join(", ")
|
34
|
+
meaning
|
35
|
+
end
|
36
|
+
|
37
|
+
# Converts a numerical day of week value or 3-letter day of week value to
|
38
|
+
# human-readable week-day value.
|
39
|
+
def self.ascii_weekday(day)
|
40
|
+
case day.to_s.downcase
|
41
|
+
when "0", "00", "7", "07", "sun"; "Sunday"
|
42
|
+
when "1", "01", "mon"; "Monday"
|
43
|
+
when "2", "02", "tue"; "Tuesday"
|
44
|
+
when "3", "03", "wed"; "Wednsday"
|
45
|
+
when "4", "04", "thu"; "Thursday"
|
46
|
+
when "5", "05", "fri"; "Friday"
|
47
|
+
when "6", "06", "sat"; "Saturday"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
class Cron::Parser
|
2
|
+
class Field
|
3
|
+
attr_reader :pattern,
|
4
|
+
:meaning,
|
5
|
+
:warning
|
6
|
+
|
7
|
+
def initialize(pattern)
|
8
|
+
raise NotImplementedError.new("'Cron::Parser::Field' can't be initialized
|
9
|
+
directly".squish) if self.class == Cron::Parser::Field
|
10
|
+
@pattern = pattern # for current field only
|
11
|
+
@warning = nil
|
12
|
+
@meaning = validate! # current field
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
%Q{
|
17
|
+
#<#{self.class.name}:#{Object::o_hexy_id(self)}>
|
18
|
+
{
|
19
|
+
:pattern => "#@pattern",
|
20
|
+
#{":warning => #@warning," if @warning}
|
21
|
+
:meaning => "#@meaning"
|
22
|
+
}
|
23
|
+
}.squish
|
24
|
+
end
|
25
|
+
|
26
|
+
# Recognizes and validates the current field in cron pattern.
|
27
|
+
# FYI, all magic happens over here!
|
28
|
+
def validate!
|
29
|
+
investigate_invalid_values!
|
30
|
+
|
31
|
+
meaning = ""
|
32
|
+
halt_loop = false
|
33
|
+
match_data = nil
|
34
|
+
list = []
|
35
|
+
values = @pattern.split(",")
|
36
|
+
options = {
|
37
|
+
unit: self.field_name,
|
38
|
+
exclude_preposition: true
|
39
|
+
}
|
40
|
+
|
41
|
+
# iterate over all comma seperated values for current field
|
42
|
+
values.map.with_index do |value, index|
|
43
|
+
# look whether the value matches any of regex rule
|
44
|
+
self.class.specifications.map do |spec|
|
45
|
+
next unless spec[:for_fields].include? self.field_name
|
46
|
+
next if (match_data = spec[:rule].match(value)).nil?
|
47
|
+
|
48
|
+
# some special kind of values can dominate all other values for
|
49
|
+
# current field, e.g. value such as '*'
|
50
|
+
if spec[:can_dominate_all]
|
51
|
+
meaning = spec[:yields].(*match_data.captures, options)
|
52
|
+
halt_loop = true and break
|
53
|
+
end
|
54
|
+
|
55
|
+
list << spec[:yields].(*match_data.captures, options)
|
56
|
+
|
57
|
+
# some values are confusing or ambiguous, e.g. '8/2' -- but crontab
|
58
|
+
# allows such values, but they can halt next comma seperated values
|
59
|
+
# from being parsed for the current field of cron pattern
|
60
|
+
if spec[:can_halt]
|
61
|
+
@warning = "'#{value}' is valid but confusing pattern"
|
62
|
+
halt_loop = true
|
63
|
+
break
|
64
|
+
end
|
65
|
+
# stop looking for other regex rules if values is matched here
|
66
|
+
break if match_data
|
67
|
+
end
|
68
|
+
# stop iterating over comma seperated values if special-purpose flag:
|
69
|
+
# `halt_loop` is set to `true` somewhere
|
70
|
+
break if halt_loop
|
71
|
+
end
|
72
|
+
|
73
|
+
list = list.flatten.uniq.map(&:to_s).sort
|
74
|
+
list = list.map(&:to_i).sort if !!(/minute|hour|day_of_month/ =~ field_name)
|
75
|
+
if meaning.==("") and list.empty?
|
76
|
+
raise invalid_field_error_class.new("\"#{self.field_name}\" field's
|
77
|
+
pattern is invalid".squish)
|
78
|
+
end
|
79
|
+
meaning = self.class.generate_meaning(list, field_name) if meaning.==("")
|
80
|
+
meaning
|
81
|
+
end
|
82
|
+
|
83
|
+
# List of regular expressions to match the different kind of values in cron
|
84
|
+
# pattern fields, also produces partial meaning for the matched values
|
85
|
+
def self.specifications
|
86
|
+
[
|
87
|
+
{
|
88
|
+
# e.g.: "*"
|
89
|
+
rule: /\A\*\Z/,
|
90
|
+
yields: ->(options) do
|
91
|
+
"every " + options[:unit].split("_").join(" ")
|
92
|
+
end,
|
93
|
+
for_fields: %w{ minute hour day_of_month month day_of_week },
|
94
|
+
can_dominate_all: true
|
95
|
+
},
|
96
|
+
{
|
97
|
+
# e.g.: "*/2"
|
98
|
+
rule: /\A\*\/(?<step>\d+)\Z/,
|
99
|
+
yields: ->(step, options) do
|
100
|
+
range = []
|
101
|
+
(self.lower_bound.to_i..self.upper_bound.to_i).map do |value|
|
102
|
+
range << value if value.modulo(step.to_i).zero?
|
103
|
+
end
|
104
|
+
list = range.any? ? range : [step]
|
105
|
+
return list if options[:exclude_preposition]
|
106
|
+
return self.generate_meaning(list, options[:unit])
|
107
|
+
end,
|
108
|
+
for_fields: %w{ minute hour day_of_month month day_of_week }
|
109
|
+
},
|
110
|
+
{
|
111
|
+
# e.g.: "4"
|
112
|
+
rule: /\A(?<value>\d+)\Z/,
|
113
|
+
yields: ->(value, options) do
|
114
|
+
return [value] if options[:exclude_preposition]
|
115
|
+
return self.generate_meaning([value], options[:unit])
|
116
|
+
end,
|
117
|
+
for_fields: %w{ minute hour day_of_month month day_of_week }
|
118
|
+
},
|
119
|
+
{
|
120
|
+
# e.g.: "3-26"
|
121
|
+
rule: /\A(?<from>\d+)\-(?<to>\d+)\Z/,
|
122
|
+
yields: ->(from, to, options) do
|
123
|
+
range = (from..to).to_a
|
124
|
+
list = range.any? ? range : [from]
|
125
|
+
return list.map(&:to_i) if options[:exclude_preposition]
|
126
|
+
return self.generate_meaning(list, options[:unit])
|
127
|
+
end,
|
128
|
+
for_fields: %w{ minute hour day_of_month month day_of_week }
|
129
|
+
},
|
130
|
+
{
|
131
|
+
# e.g.: "3-26/2"
|
132
|
+
rule: /\A(?<from>\d+)\-(?<to>\d+)\/(?<step>\d+)\Z/,
|
133
|
+
yields: ->(from, to, step, options) do
|
134
|
+
range = self.range_step_values(from.to_i, to.to_i, step.to_i)
|
135
|
+
return range if options[:exclude_preposition]
|
136
|
+
return self.generate_meaning(range, options[:unit])
|
137
|
+
end,
|
138
|
+
for_fields: %w{ minute hour day_of_month month day_of_week }
|
139
|
+
},
|
140
|
+
{
|
141
|
+
# e.g.: "12/3"
|
142
|
+
rule: /\A(?<value>\d+)\/\d+\Z/,
|
143
|
+
yields: ->(value, options) do
|
144
|
+
return [value] if options[:exclude_preposition]
|
145
|
+
return self.generate_meaning([value], options[:unit])
|
146
|
+
end,
|
147
|
+
for_fields: %w{ minute hour day_of_month month day_of_week },
|
148
|
+
can_halt: true
|
149
|
+
}
|
150
|
+
]
|
151
|
+
end
|
152
|
+
|
153
|
+
# Checks for invalid characters and values for the current field.
|
154
|
+
def investigate_invalid_values!
|
155
|
+
invalids = @pattern.split(/,|\/|\-/).uniq.collect do |value|
|
156
|
+
value unless self.class.allowed_values.to_a.include?(value.upcase)
|
157
|
+
end.compact
|
158
|
+
invalids.delete("*")
|
159
|
+
|
160
|
+
err = nil
|
161
|
+
if invalids.include?('') || invalids.include?(' ')
|
162
|
+
err = "#{field_name} field's pattern is invalid, please run:
|
163
|
+
'#{self.class}.allowed_values' to know valid values".squish
|
164
|
+
elsif invalids.any?
|
165
|
+
err = "value: '#{invalids.join(', ')}' not allowed for '#{field_name}'
|
166
|
+
field, run: '#{self.class}.allowed_values' to know valid values".squish
|
167
|
+
end
|
168
|
+
raise self.invalid_field_error_class.new(err) if err
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns current field's name, for e.g. 'minute', 'hour', and likewise.
|
172
|
+
def field_name
|
173
|
+
self.class.name.split("::").last.downcase.sub("of", "_of_"). \
|
174
|
+
sub("field", "").downcase
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.field_name; self.new.field_name end
|
178
|
+
|
179
|
+
# Returns preposition for the current field to be prepended while generating
|
180
|
+
# meaning (partial sentence).
|
181
|
+
def self.field_preposition(field)
|
182
|
+
case field
|
183
|
+
when "minute"
|
184
|
+
"at"
|
185
|
+
when "hour", "day_of_month", "day_of_week"
|
186
|
+
"on"
|
187
|
+
when "month"
|
188
|
+
"in"
|
189
|
+
else
|
190
|
+
"at"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# An algorithm calculates and returns values exist for the expression
|
195
|
+
# <from>-<to>/<step>.
|
196
|
+
def self.range_step_values(from, to, step)
|
197
|
+
values = [from]
|
198
|
+
(from..to).map do |value|
|
199
|
+
value += step
|
200
|
+
if (value == values.last + step) and value <= to
|
201
|
+
values.push value
|
202
|
+
end
|
203
|
+
end
|
204
|
+
values
|
205
|
+
end
|
206
|
+
|
207
|
+
# Generates error class for the current cron field.
|
208
|
+
def invalid_field_error_class
|
209
|
+
("Invalid" + self.field_name.split("_").map(&:capitalize).join + \
|
210
|
+
"FieldError").classify.safe_constantize
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class Cron::Parser
|
2
|
+
class HourField < Field
|
3
|
+
def self.allowed_values; ("0".."9").to_a + ("00".."23").to_a end
|
4
|
+
def self.upper_bound; self.allowed_values.last end
|
5
|
+
def self.lower_bound; self.allowed_values.first end
|
6
|
+
def self.allowed_special_characters; %w{ * / , - } end
|
7
|
+
def self.specifications
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
# Converts 24-hour value to 12-hour am/pm value.
|
12
|
+
def self.to_12h(hour)
|
13
|
+
case hour.to_i
|
14
|
+
when 0
|
15
|
+
"12am"
|
16
|
+
when 1..11
|
17
|
+
hour.to_s + "am"
|
18
|
+
when 12
|
19
|
+
hour.to_s + "pm"
|
20
|
+
when 13..23
|
21
|
+
(hour - 12).to_s + "pm"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sorts a given array of 12-hour am/pm values in a clockwise order starting
|
26
|
+
# from midnight (like: 12am, 1am, 2am, ..., 11am, 12pm, 1pm, 2pm, ..., 11pm)
|
27
|
+
def self.sort_by_12h(hours_array)
|
28
|
+
hour_priority_map = {
|
29
|
+
"12am" => 1, "1am" => 2, "2am" => 3, "3am" => 4, "4am" => 5, "5am" => 6,
|
30
|
+
"6am" => 7, "7am" => 8, "8am" => 9, "9am" => 10, "10am" => 11,
|
31
|
+
"11am" => 12, "12pm" => 13, "1pm" => 14, "2pm" => 15, "3pm" => 16,
|
32
|
+
"4pm" => 17, "5pm" => 18, "6pm" => 19, "7pm" => 20, "8pm" => 21,
|
33
|
+
"9pm" => 22, "10pm" => 23, "11pm" => 24
|
34
|
+
}
|
35
|
+
arr_with_priorities = hours_array.collect do |hour|
|
36
|
+
hour_priority_map[hour]
|
37
|
+
end.sort
|
38
|
+
return_arr = []
|
39
|
+
arr_with_priorities.map do |priority|
|
40
|
+
hour_priority_map.each_pair do |hr, pr|
|
41
|
+
return_arr << hr if pr == priority
|
42
|
+
end
|
43
|
+
end.flatten
|
44
|
+
return_arr
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates partial meaning (sentence) for the hour field's pattern.
|
48
|
+
def self.generate_meaning(list, unit)
|
49
|
+
meaning = ""
|
50
|
+
meaning += self.field_preposition(unit)
|
51
|
+
meaning += " "
|
52
|
+
meaning += list.map(&:to_i).map{ |h| self.to_12h(h) }.join(", ")
|
53
|
+
meaning
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Cron::Parser
|
2
|
+
class MinuteField < Field
|
3
|
+
def self.allowed_values; ("0".."9").to_a + ("00".."59").to_a end
|
4
|
+
def self.upper_bound; self.allowed_values.last end
|
5
|
+
def self.lower_bound; self.allowed_values.first end
|
6
|
+
def self.allowed_special_characters; %w{ * / , - } end
|
7
|
+
def self.specifications
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
# Creates partial meaning (sentence) for the minute field's pattern.
|
12
|
+
def self.generate_meaning(list, unit)
|
13
|
+
meaning = ""
|
14
|
+
meaning += self.field_preposition(unit)
|
15
|
+
meaning += " "
|
16
|
+
meaning += list.map(&:to_s).map(&:ordinalize).join(", ")
|
17
|
+
meaning += " "
|
18
|
+
meaning += unit
|
19
|
+
meaning
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Cron::Parser
|
2
|
+
class MonthField < Field
|
3
|
+
def self.allowed_values
|
4
|
+
("1".."9").to_a + ("01".."12").to_a + %w{ jan feb mar apr may jun
|
5
|
+
jul aug sep oct nov dec
|
6
|
+
}.map(&:upcase)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.allowed_special_characters; %w{ * / , - } end
|
10
|
+
def self.upper_bound; "1" end
|
11
|
+
def self.lower_bound; "12" end
|
12
|
+
|
13
|
+
# Adds some month field-specific extra regular expressions to super class's
|
14
|
+
# `specifications` method.
|
15
|
+
def self.specifications
|
16
|
+
extra_specs = [
|
17
|
+
{
|
18
|
+
rule: /\A
|
19
|
+
(?<month>
|
20
|
+
(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)
|
21
|
+
)
|
22
|
+
\Z/ix,
|
23
|
+
yields: ->(month, options) do
|
24
|
+
return [month] if options[:exclude_preposition]
|
25
|
+
return self.generate_meaning([month], options[:unit])
|
26
|
+
end,
|
27
|
+
for_fields: %w{ month }
|
28
|
+
},
|
29
|
+
]
|
30
|
+
super + extra_specs
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates partial meaning (sentence) for the month field's pattern.
|
34
|
+
def self.generate_meaning(list, unit)
|
35
|
+
meaning = ""
|
36
|
+
meaning += self.field_preposition(unit)
|
37
|
+
meaning += " "
|
38
|
+
meaning += list.map{ |m| self.ascii_month(m) }.join(", ")
|
39
|
+
meaning
|
40
|
+
end
|
41
|
+
|
42
|
+
# Converts a numerical month value or 3-letter month value to human-readable
|
43
|
+
# month value.
|
44
|
+
def self.ascii_month(month)
|
45
|
+
case month.to_s.downcase
|
46
|
+
when "1", "01", "jan"; "January"
|
47
|
+
when "2", "02", "feb"; "February"
|
48
|
+
when "3", "03", "mar"; "March"
|
49
|
+
when "4", "04", "apr"; "April"
|
50
|
+
when "5", "05", "may"; "May"
|
51
|
+
when "6", "06", "jun"; "June"
|
52
|
+
when "7", "07", "jul"; "July"
|
53
|
+
when "8", "08", "aug"; "August"
|
54
|
+
when "9", "09", "sep"; "September"
|
55
|
+
when "10", "oct"; "October"
|
56
|
+
when "11", "nov"; "November"
|
57
|
+
when "12", "dec"; "December"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
InvalidCronPatternError = Class.new(ArgumentError)
|
2
|
+
|
3
|
+
InvalidMinuteFieldError = Class.new(ArgumentError)
|
4
|
+
InvalidHourFieldError = Class.new(ArgumentError)
|
5
|
+
InvalidDayOfMonthFieldError = Class.new(ArgumentError)
|
6
|
+
InvalidMonthFieldError = Class.new(ArgumentError)
|
7
|
+
InvalidDayOfWeekFieldError = Class.new(ArgumentError)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module StringExtensions
|
2
|
+
# Adds 'th', 'nd', 'st' like ordinal to numerical (string) values.
|
3
|
+
# e.g. 22nd, 40th, 1st etc.
|
4
|
+
def ordinalize
|
5
|
+
match = /\A(?<int>\d+)\Z|\A(?<real>\d+\.\d+)\Z/.match(self)
|
6
|
+
raise NameError.new("cannot ordinalize non-numeric value") unless match
|
7
|
+
num = match[:int] ? self.to_i : self.to_f
|
8
|
+
"#{num}#{num.to_s.ordinal}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def ordinal
|
12
|
+
ActiveSupport::Inflector.ordinal(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Removes extra whitespaces from the string.
|
16
|
+
def squish
|
17
|
+
dup.squish!
|
18
|
+
end
|
19
|
+
|
20
|
+
# Destructive version of `squish` method.
|
21
|
+
def squish!
|
22
|
+
strip!
|
23
|
+
gsub!(/\s+/, ' ')
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class String; self.send :include, StringExtensions end
|
29
|
+
|
30
|
+
class Object
|
31
|
+
# Generate hex value for requester object in argument.
|
32
|
+
def self.o_hexy_id(requester) # that's not "oh sexy lady", or is it?!
|
33
|
+
"0x" + (requester.object_id << 1).to_s(16)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Cron
|
4
|
+
describe Parser do
|
5
|
+
|
6
|
+
let(:parser_klass) { Cron::Parser }
|
7
|
+
|
8
|
+
spec_fields = %w{ minute hour day_of_month month day_of_week }
|
9
|
+
|
10
|
+
it "should check invalid pattern" do
|
11
|
+
expect { parser_klass.new }.to raise_error(InvalidCronPatternError, /cron pattern must be a string/)
|
12
|
+
expect { parser_klass.new("") }.to raise_error(InvalidCronPatternError, /cron pattern must contain exact/)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "initialized with correct pattern" do
|
16
|
+
subject { parser_klass.new("* * * * *") }
|
17
|
+
|
18
|
+
specify { expect(subject).to be_an_instance_of(parser_klass) }
|
19
|
+
|
20
|
+
specify { should have_exactly(5).fields }
|
21
|
+
|
22
|
+
it "should respond to all public instance methods" do
|
23
|
+
methods = [:pattern, :fields, :meaning, :warnings, :humanize, :inspect] + spec_fields.map(&:to_sym).map { |f| f = "#{f}_field" }
|
24
|
+
methods.each do |method|
|
25
|
+
subject.should respond_to(method)
|
26
|
+
end
|
27
|
+
end # respond to public instance methods
|
28
|
+
|
29
|
+
context "checks each instance field" do
|
30
|
+
it "check minute_field instance" do
|
31
|
+
expect(subject.minute_field).to eq(subject.fields[:minute])
|
32
|
+
end
|
33
|
+
it "check hour_field instance" do
|
34
|
+
expect(subject.hour_field).to eq(subject.fields[:hour])
|
35
|
+
end
|
36
|
+
it "check day_of_month_field instance" do
|
37
|
+
expect(subject.day_of_month_field).to eq(subject.fields[:day_of_month])
|
38
|
+
end
|
39
|
+
it "check month_field instance" do
|
40
|
+
expect(subject.month_field).to eq(subject.fields[:month])
|
41
|
+
end
|
42
|
+
it "check day_of_week_field instance" do
|
43
|
+
expect(subject.day_of_week_field).to eq(subject.fields[:day_of_week])
|
44
|
+
end
|
45
|
+
end # context each instance field
|
46
|
+
end # context correct pattern
|
47
|
+
|
48
|
+
context "well, start with kind of patterns..." do
|
49
|
+
context "wrong patterns tests" do
|
50
|
+
|
51
|
+
describe "minute" do
|
52
|
+
wrong_min_patterns = ["-1 * * * *", "3- * * * *", "60 * * * *", "1,2,b * * * *", "1-03,3/,6 * * * *", "5--6, * * * *", "(2,3),5 * * * *"]
|
53
|
+
wrong_min_patterns.each do |pattern|
|
54
|
+
it "for PATTERN: \"#{pattern}\" it raises error" do
|
55
|
+
expect { parser_klass.new(pattern) }.to raise_error(InvalidMinuteFieldError)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end # context wrong minute pattern
|
59
|
+
|
60
|
+
describe "hour" do
|
61
|
+
wrong_hr_patterns = ["* -1 * * *", "* 3- * * *", "* 24 * * *", "* 1pm * * *", "* 3/,4 * * *", "* 5--10 * * *", "* 20%5 * * *"]
|
62
|
+
wrong_hr_patterns.each do |pattern|
|
63
|
+
it "for PATTERN: \"#{pattern}\" it raises error" do
|
64
|
+
expect { parser_klass.new(pattern) }.to raise_error(InvalidHourFieldError)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end # context wrong hour pattern
|
68
|
+
|
69
|
+
describe "day_of_month" do
|
70
|
+
wrong_dom_patterns = ["* * 0 * *", "* * -1 * *", "* * 3- * *", "* * 32 * *", "* * 1st * *", "* * 3/,5,6 * *", "* * 5--10 * *", "* * 2^2 * *"]
|
71
|
+
wrong_dom_patterns.each do |pattern|
|
72
|
+
it "for PATTERN: \"#{pattern}\" it raises error" do
|
73
|
+
expect { parser_klass.new(pattern) }.to raise_error(InvalidDayOfMonthFieldError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end # context wrong day_of_month pattern
|
77
|
+
|
78
|
+
describe "month" do
|
79
|
+
wrong_mnth_patterns = ["* * * 0 *", "* * * -1 *", "* * * 3- *", "* * * 13 *", "* * * MARC *", "* * * 1st *", "* * * 3/,5,6 *", "* * * 5--10 *", "* * * @ *"]
|
80
|
+
wrong_mnth_patterns.each do |pattern|
|
81
|
+
it "for PATTERN: \"#{pattern}\" it raises error" do
|
82
|
+
expect { parser_klass.new(pattern) }.to raise_error(InvalidMonthFieldError)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end # context wrong month pattern
|
86
|
+
|
87
|
+
describe "day_of_week" do
|
88
|
+
wrong_dow_patterns = ["* * * * -1", "* * * * 3-", "* * * * 8", "* * * * sunday", "* * * * 2nd", "* * * * 3/", "* * * * 1--6", "* * * * %"]
|
89
|
+
wrong_dow_patterns.each do |pattern|
|
90
|
+
it "for PATTERN: \"#{pattern}\" it raises error" do
|
91
|
+
expect { parser_klass.new(pattern) }.to raise_error(InvalidDayOfWeekFieldError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end # context wrong day_of_week pattern
|
95
|
+
end # context invalid patterns' tests
|
96
|
+
|
97
|
+
context "valid patterns tests" do
|
98
|
+
|
99
|
+
def test_valid_pattern(pattern = "", meaning = "", warnings = [])
|
100
|
+
parsed_obj = parser_klass.new(pattern)
|
101
|
+
parsed_obj.meaning.should == meaning
|
102
|
+
parsed_obj.warnings.should == warnings
|
103
|
+
end
|
104
|
+
|
105
|
+
valid_data = [
|
106
|
+
["* * * * *", "every minute; every hour; every day of month; every month; every day of week", []],
|
107
|
+
["04 * * * *", "at 4th minute; every hour; every day of month; every month; every day of week", []],
|
108
|
+
["5 0 * * *", "at 5th minute; on 12am; every day of month; every month; every day of week", []],
|
109
|
+
["15 14 1 * *", "at 15th minute; on 2pm; on days: 1st; every month; every day of week", []],
|
110
|
+
["0 22 * * 1-5", "at 0th minute; on 10pm; every day of month; every month; on Monday, Tuesday, Wednsday, Thursday, Friday", []],
|
111
|
+
["23 0-23/2 * * *", "at 23rd minute; on 12am, 2am, 4am, 6am, 8am, 10am, 12pm, 2pm, 4pm, 6pm, 8pm, 10pm; every day of month; every month; every day of week", []],
|
112
|
+
["5 4 * * sun", "at 5th minute; on 4am; every day of month; every month; on Sunday", []],
|
113
|
+
["0 4 8-14 * *", "at 0th minute; on 4am; on days: 8th, 9th, 10th, 11th, 12th, 13th, 14th; every month; every day of week", []],
|
114
|
+
["5-12,1,59 4-10/2,11 */6,28,29 1-3,Nov,Dec *", "at 1st, 5th, 6th, 7th, 8th, 9th, 10th, 11th, 12th, 59th minute; on 4am, 6am, 8am, 10am, 11am; on days: 6th, 12th, 18th, 24th, 28th, 29th, 30th; in January, February, March, December, November; every day of week", []],
|
115
|
+
["38,37/40,39 1/2,3 26,27/2,30,31 * MON", "at 37th, 38th minute; on 1am; on days: 26th, 27th; every month; on Monday", ["for 'minute' field: '37/40' is valid but confusing pattern", "for 'hour' field: '1/2' is valid but confusing pattern", "for 'day_of_month' field: '27/2' is valid but confusing pattern"]],
|
116
|
+
]
|
117
|
+
|
118
|
+
valid_data.map do |item|
|
119
|
+
pattern, meaning, warnings = item
|
120
|
+
display_msg = "for PATTERN: \"#{pattern}\""
|
121
|
+
display_msg += "\n\tmeaning: \"#{meaning}\""
|
122
|
+
display_msg += "\n\twarnings: #{warnings}" if warnings.any?
|
123
|
+
|
124
|
+
it display_msg do
|
125
|
+
test_valid_pattern(pattern, meaning, warnings)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end # context valid patterns' tests
|
130
|
+
end # context start with kind of patterns
|
131
|
+
end # describe Parser
|
132
|
+
end # class Cron
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cron-parser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vishal Telangre
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email: the@vishaltelangre.com
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- .gitignore
|
62
|
+
- .rspec
|
63
|
+
- .travis.yml
|
64
|
+
- CHANGELOG.md
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.md
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- cron-parser.gemspec
|
70
|
+
- lib/cron.rb
|
71
|
+
- lib/cron/parser.rb
|
72
|
+
- lib/cron/parser/day_of_month_field.rb
|
73
|
+
- lib/cron/parser/day_of_week_field.rb
|
74
|
+
- lib/cron/parser/field.rb
|
75
|
+
- lib/cron/parser/hour_field.rb
|
76
|
+
- lib/cron/parser/minute_field.rb
|
77
|
+
- lib/cron/parser/month_field.rb
|
78
|
+
- lib/cron/parser/version.rb
|
79
|
+
- lib/extras/custom_errors.rb
|
80
|
+
- lib/extras/extensions.rb
|
81
|
+
- spec/cron/parser_spec.rb
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
homepage: http://github.com/vishaltelangre/cron-parser
|
84
|
+
licenses:
|
85
|
+
- MIT
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
- lib/extras
|
92
|
+
- lib/cron
|
93
|
+
- lib/cron/parser
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.3.6
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.0.2
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Dissect your Cron patterns!
|
110
|
+
test_files: []
|