cron-parser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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=
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+ Gemfile.lock
15
+ *.bk
16
+
17
+ # YARD artifacts
18
+ .yardoc
19
+ _yardoc
20
+ doc/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 2.0.0
4
+ - jruby-19mode
5
+ - rbx-19mode
6
+ notifications:
7
+ recipients:
8
+ - the@vishaltelangre.com
@@ -0,0 +1,7 @@
1
+ ### 0.1.1 (Next Release)
2
+
3
+ * Your contribution here.
4
+
5
+ ### 0.1.0 (10/20/2013)
6
+
7
+ * Initial public release - [@vishaltelangre](https://github.com/vishaltelangre).
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -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.
@@ -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).
@@ -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
@@ -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
@@ -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
@@ -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,5 @@
1
+ class Cron
2
+ class Parser
3
+ VERSION = '0.1.0'
4
+ end
5
+ 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
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'rubygems'
4
+ require 'rspec'
5
+ require 'cron'
6
+ require 'cron/parser'
7
+ require 'extras/custom_errors'
8
+ require 'extras/extensions'
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: []