parse-cron 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ *.swp
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in parse-cron.gemspec
4
+ gemspec
5
+
6
+ gem "ZenTest", "4.6.0"
data/README ADDED
@@ -0,0 +1,5 @@
1
+ # parse-cron - parse crontab syntax & determine next scheduled run
2
+
3
+ The goal of this gem is to parse a crontab timing specification and determine when the
4
+ job should be run. It is not a scheduler, it does not run the jobs.
5
+
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new
6
+ task :default => :spec
7
+
8
+ desc 'Start IRB with preloaded environment'
9
+ task :console do
10
+ exec 'irb', "-I#{File.join(File.dirname(__FILE__), 'lib')}", '-rparse-cron'
11
+ end
@@ -0,0 +1,184 @@
1
+ #
2
+ # Parses cron expressions and computes the next occurence of the "job"
3
+ #
4
+ class CronParser
5
+ # internal "mutable" time representation
6
+ class InternalTime
7
+ attr_accessor :year, :month, :day, :hour, :min
8
+
9
+ def initialize(time)
10
+ @year = time.year
11
+ @month = time.month
12
+ @day = time.day
13
+ @hour = time.hour
14
+ @min = time.min
15
+ end
16
+
17
+ def to_time
18
+ Time.utc(@year, @month, @day, @hour, @min, 0)
19
+ end
20
+ alias :inspect :to_time
21
+ end
22
+
23
+ SYMBOLS = {
24
+ "jan" => "0",
25
+ "feb" => "1",
26
+ "mar" => "2",
27
+ "apr" => "3",
28
+ "may" => "4",
29
+ "jun" => "5",
30
+ "jul" => "6",
31
+ "aug" => "7",
32
+ "sep" => "8",
33
+ "oct" => "9",
34
+ "nov" => "10",
35
+ "dec" => "11",
36
+
37
+ "sun" => "0",
38
+ "mon" => "1",
39
+ "tue" => "2",
40
+ "wed" => "3",
41
+ "thu" => "4",
42
+ "fri" => "5",
43
+ "sat" => "6"
44
+ }
45
+
46
+ def initialize(source)
47
+ @source = source
48
+ end
49
+
50
+
51
+ # returns the next occurence after the given date
52
+ def next(now = Time.now)
53
+ t = InternalTime.new(now)
54
+
55
+ unless time_specs[:month].include?(t.month)
56
+ nudge_month(t)
57
+ t.day = 0
58
+ end
59
+
60
+ unless t.day == 0 || interpolate_weekdays(t.year, t.month).include?(t.day)
61
+ nudge_date(t)
62
+ t.hour = -1
63
+ end
64
+
65
+ unless time_specs[:hour].include?(t.hour)
66
+ nudge_hour(t)
67
+ t.min = -1
68
+ end
69
+
70
+ # always nudge the minute
71
+ nudge_minute(t)
72
+ t.to_time
73
+ end
74
+
75
+ SUBELEMENT_REGEX = %r{^(\d+)(-(\d+)(/(\d+))?)?$}
76
+ def parse_element(elem, allowed_range)
77
+ elem.split(',').map do |subel|
78
+ if subel =~ /^\*/
79
+ step = subel.length > 1 ? subel[2..-1].to_i : 1
80
+ stepped_range(allowed_range, step)
81
+ else
82
+ if SUBELEMENT_REGEX === subel
83
+ if $5 # with range
84
+ stepped_range($1.to_i..($3.to_i + 1), $5.to_i)
85
+ elsif $3 # range without step
86
+ stepped_range($1.to_i..($3.to_i + 1), 1)
87
+ else # just a numeric
88
+ [$1.to_i]
89
+ end
90
+ else
91
+ raise "Bad Vixie-style specification #{subel}"
92
+ end
93
+ end
94
+ end.flatten.sort
95
+ end
96
+
97
+
98
+ protected
99
+
100
+ # returns a list of days which do both match time_spec[:dom] and time_spec[:dow]
101
+ def interpolate_weekdays(year, month)
102
+ t = Date.new(year, month, 1)
103
+ valid_mday = time_specs[:dom]
104
+ valid_wday = time_specs[:dow]
105
+
106
+ result = []
107
+ while t.month == month
108
+ result << t.mday if valid_mday.include?(t.mday) && valid_wday.include?(t.wday)
109
+ t = t.succ
110
+ end
111
+
112
+ result
113
+ end
114
+
115
+ def nudge_year(t)
116
+ t.year = t.year + 1
117
+ end
118
+
119
+ def nudge_month(t)
120
+ spec = time_specs[:month]
121
+ next_value = find_best_next(t.month, spec)
122
+ t.month = next_value || spec.first
123
+ next_value.nil? ? nudge_year(t) : t
124
+ end
125
+
126
+ def nudge_date(t)
127
+ spec = interpolate_weekdays(t.year, t.month)
128
+ next_value = find_best_next(t.day, spec)
129
+ t.day = next_value || spec.first
130
+ next_value.nil? ? nudge_month(t) : t
131
+ end
132
+
133
+ def nudge_hour(t)
134
+ spec = time_specs[:hour]
135
+ next_value = find_best_next(t.hour, spec)
136
+ t.hour = next_value || spec.first
137
+ next_value.nil? ? nudge_date(t) : t
138
+ end
139
+
140
+ def nudge_minute(t)
141
+ spec = time_specs[:minute]
142
+ next_value = find_best_next(t.min, spec)
143
+ t.min = next_value || spec.first
144
+ next_value.nil? ? nudge_hour(t) : t
145
+ end
146
+
147
+ def time_specs
148
+ @time_specs ||= begin
149
+ # tokens now contains the 5 fields
150
+ tokens = substitute_parse_symbols(@source).split(/\s+/)
151
+ {
152
+ :minute => parse_element(tokens[0], 0..59), #minute
153
+ :hour => parse_element(tokens[1], 0..23), #hour
154
+ :dom => parse_element(tokens[2], 1..31), #DOM
155
+ :month => parse_element(tokens[3], 1..12), #mon
156
+ :dow => parse_element(tokens[4], 0..6) #DOW
157
+ }
158
+ end
159
+ end
160
+
161
+ def substitute_parse_symbols(str)
162
+ SYMBOLS.inject(str) do |s, (symbol, replacement)|
163
+ s.gsub(symbol, replacement)
164
+ end
165
+ end
166
+
167
+
168
+ def stepped_range(rng, step = 1)
169
+ len = rng.last - rng.first
170
+
171
+ num = len.div(step)
172
+ result = (0..num).map { |i| rng.first + step * i }
173
+
174
+ result.pop if result[-1] == rng.last and rng.exclude_end?
175
+ result
176
+ end
177
+
178
+
179
+ # returns the smallest element from allowed which is greater than current
180
+ # returns nil if no matching value was found
181
+ def find_best_next(current, allowed)
182
+ allowed.sort.find { |val| val > current }
183
+ end
184
+ end
@@ -0,0 +1,5 @@
1
+ module Parse
2
+ module Cron
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "parse-cron/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "parse-cron"
7
+ s.version = Parse::Cron::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Michael Siebert"]
10
+ s.email = ["siebertm85@googlemail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Parses cron expressions and calculates the next occurence}
13
+ s.description = %q{Parses cron expressions and calculates the next occurence}
14
+
15
+ s.rubyforge_project = "parse-cron"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency 'rspec', '~>2.6.0'
23
+ end
@@ -0,0 +1,46 @@
1
+ require "time"
2
+ require "./spec/spec_helper"
3
+ require "cron_parser"
4
+
5
+ describe "CronParser#parse_element" do
6
+ [
7
+ ["*", 0..60, (0..60).to_a],
8
+ ["*/10", 0..60, [0, 10, 20, 30, 40, 50]],
9
+ ["10", 0..60, [10]],
10
+ ["10,30", 0..60, [10, 30]],
11
+ ["10-15", 0..60, [10, 11, 12, 13, 14, 15]],
12
+ ["10-40/10", 0..60, [10, 20, 30, 40]],
13
+ ].each do |element, range, expected|
14
+ it "should return #{expected} for '#{element}' when range is #{range}" do
15
+ parser = CronParser.new('')
16
+ parser.parse_element(element, range) == expected
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "CronParser#next" do
22
+ [
23
+ ["* * * * *", "2011-08-15T12:00", "2011-08-15T12:01"],
24
+ ["* * * * *", "2011-08-15T02:25", "2011-08-15T02:26"],
25
+ ["* * * * *", "2011-08-15T02:59", "2011-08-15T03:00"],
26
+ ["*/15 * * * *", "2011-08-15T02:02", "2011-08-15T02:15"],
27
+ ["*/15,25 * * * *", "2011-08-15T02:15", "2011-08-15T02:25"],
28
+ ["30 3,6,9 * * *", "2011-08-15T02:15", "2011-08-15T03:30"],
29
+ ["30 9 * * *", "2011-08-15T10:15", "2011-08-16T09:30"],
30
+ ["30 9 * * *", "2011-08-31T10:15", "2011-09-01T09:30"],
31
+ ["30 9 * * *", "2011-09-30T10:15", "2011-10-01T09:30"],
32
+ ["0 9 * * *", "2011-12-31T10:15", "2012-01-01T09:00"],
33
+ ["* * 12 * *", "2010-04-15T10:15", "2010-05-12T00:00"],
34
+ ["* * * * 1,3", "2010-04-15T10:15", "2010-04-19T00:00"],
35
+ ["0 0 1 1 *", "2010-04-15T10:15", "2011-01-01T00:00"],
36
+ ].each do |line, now, expected_next|
37
+ it "should return #{expected_next} for '#{line}' when now is #{now}" do
38
+ now = Time.xmlschema(now + ":00+00:00")
39
+ expected_next = Time.xmlschema(expected_next + ":00+00:00")
40
+
41
+ parser = CronParser.new(line)
42
+
43
+ parser.next(now).xmlschema.should == expected_next.xmlschema
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ spec_dir = File.dirname(__FILE__)
2
+ lib_dir = File.expand_path(File.join(spec_dir, '..', 'lib'))
3
+ $:.unshift(lib_dir)
4
+ $:.uniq!
5
+
6
+ RSpec.configure do |config|
7
+ end
8
+
9
+ require 'cron_parser'
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parse-cron
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Michael Siebert
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-17 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 23
30
+ segments:
31
+ - 2
32
+ - 6
33
+ - 0
34
+ version: 2.6.0
35
+ type: :development
36
+ version_requirements: *id001
37
+ description: Parses cron expressions and calculates the next occurence
38
+ email:
39
+ - siebertm85@googlemail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - .gitignore
48
+ - .rspec
49
+ - Gemfile
50
+ - README
51
+ - Rakefile
52
+ - lib/cron_parser.rb
53
+ - lib/parse-cron/version.rb
54
+ - parse-cron.gemspec
55
+ - spec/cron_parser_spec.rb
56
+ - spec/spec_helper.rb
57
+ has_rdoc: true
58
+ homepage: ""
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options: []
63
+
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project: parse-cron
87
+ rubygems_version: 1.4.2
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Parses cron expressions and calculates the next occurence
91
+ test_files:
92
+ - spec/cron_parser_spec.rb
93
+ - spec/spec_helper.rb