cron2english 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 34283e98c0f59e56d15d8c57f7d2132af6a9d128
4
+ data.tar.gz: 0bcb3f53e715baf34bb2fd215870958c41790d35
5
+ SHA512:
6
+ metadata.gz: 29ffe6c30702396e09fcf3b4bdfe8db1c6f89f3b259e88c3aee3e613dceb5b7319dbac7f04a8ee3b3f051a25f77ca3c1f0a4754f677054adfcfe9ac0dc1cd007
7
+ data.tar.gz: 293f79abcea3c3c9fd3dc5168bef0b56730bd7546fc92c51c6c9cf8fc358b75ea7e49fd5e886a335c2af4e88a27aaf9d3616d3dbdb5678f25cc488c0eeb6024b
data/.gitignore ADDED
@@ -0,0 +1,52 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # jeweler generated
15
+ pkg
16
+
17
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
18
+ #
19
+ # * Create a file at ~/.gitignore
20
+ # * Include files you want ignored
21
+ # * Run: git config --global core.excludesfile ~/.gitignore
22
+ #
23
+ # After doing this, these files will be ignored in all your git projects,
24
+ # saving you from having to 'pollute' every project you touch with them
25
+ #
26
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
27
+ #
28
+ # For MacOS:
29
+ #
30
+ #.DS_Store
31
+
32
+ # For TextMate
33
+ #*.tmproj
34
+ #tmtags
35
+
36
+ # For emacs:
37
+ #*~
38
+ #\#*
39
+ #.\#*
40
+
41
+ # For vim:
42
+ .*.swp
43
+ .*.swo
44
+
45
+ # For redcar:
46
+ #.redcar
47
+
48
+ # For rubinius:
49
+ #*.rbc
50
+
51
+ tmp
52
+ README.html
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm_gemset_create_on_use_flag=1
2
+ rvm gemset use ruby-2.0.0-p247@cron2english
data/Gemfile ADDED
@@ -0,0 +1,23 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'rails', '>= 3.0.0'
7
+ # gem 'rake'
8
+ # gem 'activemodel', '= 3.0.11'
9
+ # gem 'activesupport', '= 3.0.11'
10
+
11
+ # Add dependencies to develop your gem here.
12
+ # Include everything needed to run rake, tests, features, etc.
13
+ group :development do
14
+ gem "rspec", "~> 2.4.0"
15
+ gem "bundler"
16
+ end
17
+
18
+ group :test do
19
+ gem 'activerecord-postgresql-adapter', :platforms => :ruby
20
+ gem 'activerecord-mysql2-adapter', :platforms => :ruby
21
+ gem 'activerecord-jdbcpostgresql-adapter', :platforms => :jruby
22
+ gem 'activerecord-jdbcmysql-adapter', :platforms => :jruby
23
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,101 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ actionmailer (4.0.2)
5
+ actionpack (= 4.0.2)
6
+ mail (~> 2.5.4)
7
+ actionpack (4.0.2)
8
+ activesupport (= 4.0.2)
9
+ builder (~> 3.1.0)
10
+ erubis (~> 2.7.0)
11
+ rack (~> 1.5.2)
12
+ rack-test (~> 0.6.2)
13
+ activemodel (4.0.2)
14
+ activesupport (= 4.0.2)
15
+ builder (~> 3.1.0)
16
+ activerecord (4.0.2)
17
+ activemodel (= 4.0.2)
18
+ activerecord-deprecated_finders (~> 1.0.2)
19
+ activesupport (= 4.0.2)
20
+ arel (~> 4.0.0)
21
+ activerecord-deprecated_finders (1.0.3)
22
+ activerecord-mysql2-adapter (0.0.3)
23
+ mysql2
24
+ activerecord-postgresql-adapter (0.0.1)
25
+ pg
26
+ activesupport (4.0.2)
27
+ i18n (~> 0.6, >= 0.6.4)
28
+ minitest (~> 4.2)
29
+ multi_json (~> 1.3)
30
+ thread_safe (~> 0.1)
31
+ tzinfo (~> 0.3.37)
32
+ arel (4.0.1)
33
+ atomic (1.1.14)
34
+ builder (3.1.4)
35
+ diff-lcs (1.1.3)
36
+ erubis (2.7.0)
37
+ hike (1.2.3)
38
+ i18n (0.6.9)
39
+ mail (2.5.4)
40
+ mime-types (~> 1.16)
41
+ treetop (~> 1.4.8)
42
+ mime-types (1.25.1)
43
+ minitest (4.7.5)
44
+ multi_json (1.8.4)
45
+ mysql2 (0.3.14)
46
+ pg (0.17.1)
47
+ polyglot (0.3.3)
48
+ rack (1.5.2)
49
+ rack-test (0.6.2)
50
+ rack (>= 1.0)
51
+ rails (4.0.2)
52
+ actionmailer (= 4.0.2)
53
+ actionpack (= 4.0.2)
54
+ activerecord (= 4.0.2)
55
+ activesupport (= 4.0.2)
56
+ bundler (>= 1.3.0, < 2.0)
57
+ railties (= 4.0.2)
58
+ sprockets-rails (~> 2.0.0)
59
+ railties (4.0.2)
60
+ actionpack (= 4.0.2)
61
+ activesupport (= 4.0.2)
62
+ rake (>= 0.8.7)
63
+ thor (>= 0.18.1, < 2.0)
64
+ rake (10.1.1)
65
+ rspec (2.4.0)
66
+ rspec-core (~> 2.4.0)
67
+ rspec-expectations (~> 2.4.0)
68
+ rspec-mocks (~> 2.4.0)
69
+ rspec-core (2.4.0)
70
+ rspec-expectations (2.4.0)
71
+ diff-lcs (~> 1.1.2)
72
+ rspec-mocks (2.4.0)
73
+ sprockets (2.10.1)
74
+ hike (~> 1.2)
75
+ multi_json (~> 1.0)
76
+ rack (~> 1.0)
77
+ tilt (~> 1.1, != 1.3.0)
78
+ sprockets-rails (2.0.1)
79
+ actionpack (>= 3.0)
80
+ activesupport (>= 3.0)
81
+ sprockets (~> 2.8)
82
+ thor (0.18.1)
83
+ thread_safe (0.1.3)
84
+ atomic
85
+ tilt (1.4.1)
86
+ treetop (1.4.15)
87
+ polyglot
88
+ polyglot (>= 0.3.1)
89
+ tzinfo (0.3.38)
90
+
91
+ PLATFORMS
92
+ ruby
93
+
94
+ DEPENDENCIES
95
+ activerecord-jdbcmysql-adapter
96
+ activerecord-jdbcpostgresql-adapter
97
+ activerecord-mysql2-adapter
98
+ activerecord-postgresql-adapter
99
+ bundler
100
+ rails (>= 3.0.0)
101
+ rspec (~> 2.4.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Paul A. Jungwirth
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ cron2english
2
+ ============
3
+
4
+ Cron2English is a Ruby library for turning crontab schedules into English text.
5
+
6
+ It is roughly a Ruby port of Sean Burke's [crontab2english](http://interglacial.com/~sburke/pub/crontab2english.html) Perl script, except its interface is a Ruby method rather than an executable, and it parses only time specs, not all the other things you might find in a crontab (comments, variable definitions, and the commands to run).
7
+
8
+ Usage
9
+ -----
10
+
11
+ You can convert a time spec to English like so:
12
+
13
+ english = Cron2English.parse("40 5 * * *")
14
+
15
+ This will yield an array of strings, in this case the following:
16
+
17
+ ["5:40am", "every day"]
18
+
19
+ These strings are chosen so as to sound vaguely human if you say:
20
+
21
+ Cron2English.parse("40 5 * * *").join(" ")
22
+
23
+ Cron2English understands just about anything you'll find in a crontab, including non-POSIX extensions. It can parse:
24
+
25
+ * `1-20/3 * * * *`
26
+ * `1,2,3 * * * *`
27
+ * `1-9,15-30 * * * *`
28
+ * `1-9/3,15-30/4 * * * *`
29
+ * `1 2 3 4 mON`
30
+ * `1 2 3 jan 5`
31
+ * `@reboot`
32
+ * `@yearly`
33
+ * `@annually`
34
+ * `@monthly`
35
+ * `@weekly`
36
+ * `@daily`
37
+ * `@midnight`
38
+ * `@hourly`
39
+ * `*/3 * * * *`
40
+
41
+ Known Issues
42
+ ------------
43
+
44
+ None yet!
45
+
46
+
47
+ Contributing to Cron2English
48
+ -----------------------------
49
+
50
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
51
+ * Check out the issue tracker to make sure someone hasn't already requested and/or contributed it.
52
+ * Fork the project.
53
+ * Start a feature/bugfix branch.
54
+ * Commit and push until you are happy with your contribution.
55
+ * Make be sure to add tests for it. This is important so I don't break it in a future version unintentionally.
56
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, that is fine, but please isolate that change to its own commit so I can cherry-pick around it.
57
+
58
+ Commands for building/releasing/installing:
59
+
60
+ * `rake build`
61
+ * `rake install`
62
+ * `rake release`
63
+
64
+ Copyright
65
+ ---------
66
+
67
+ Copyright (c) 2014 Paul A. Jungwirth.
68
+ See LICENSE.txt for further details.
69
+
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+
13
+
14
+ require 'rspec/core'
15
+ require 'rspec/core/rake_task'
16
+ RSpec::Core::RakeTask.new(:spec) do |spec|
17
+ spec.pattern = FileList['spec/**/*_spec.rb']
18
+ end
19
+ desc 'Default: run specs'
20
+ task :default => :spec
21
+
22
+
23
+ Bundler::GemHelper.install_tasks
24
+
25
+
26
+ task :readme => [] do |task|
27
+ `markdown README.md >README.html`
28
+ end
@@ -0,0 +1,27 @@
1
+ $:.push File.dirname(__FILE__) + '/lib'
2
+ require 'cron2english/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "cron2english"
6
+ s.version = Cron2English::VERSION
7
+ s.date = "2014-01-20"
8
+
9
+ s.summary = "Converts a crontab schedule into English text."
10
+
11
+ s.authors = ["Paul A. Jungwirth"]
12
+ s.homepage = "http://github.com/pjungwir/cron2english"
13
+ s.email = "pj@illuminatedcomputing.com"
14
+
15
+ s.licenses = ["MIT"]
16
+
17
+ s.require_paths = ["lib"]
18
+ s.executables = []
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,fixtures}/*`.split("\n")
21
+
22
+ s.add_runtime_dependency 'rails', '>= 3.0.0'
23
+ s.add_development_dependency 'rspec', '~> 2.4.0'
24
+ s.add_development_dependency 'bundler', '>= 0'
25
+
26
+ end
27
+
@@ -0,0 +1,437 @@
1
+ module Cron2English
2
+
3
+ DAYS_OF_WEEK = %w{Sun Mon Tue Wed Thu Fri Sat}
4
+ MONTHS = %w{Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec}
5
+ MIL2AMPM = ['midnight', *(1..11).map{|i| "#{i}am"}, 'noon', *(1..11).map{|i| "#{i}pm"}]
6
+ DOW2NUM = Hash[DAYS_OF_WEEK.map(&:downcase).zip(0..6)]
7
+ NUM2DOW = Hash[(0..6).zip(DAYS_OF_WEEK) + [[7, 'Sun']]]
8
+ MONTH2NUM = Hash[MONTHS.map(&:downcase).zip(1..12)]
9
+ NUM2MONTH = Hash[(1..12).zip(MONTHS)]
10
+ # unshift @months, ''; # What is this about??
11
+ DOW_REGEX = %r{^(#{DAYS_OF_WEEK.join("|")})$}i
12
+ MONTH_REGEX = %r{^(#{MONTHS.join("|")}|)$}i
13
+ NUM2MONTH_LONG = Hash[(1..12).zip(%w{January February March April May June July August September October November December})]
14
+ NUM2DOW_LONG = %w{Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday}
15
+ ATOM = '\d+|(?:\d+-\d+(?:/\d+)?)'
16
+ ATOMS_REGEX = %r{^(?:#{ATOM})(?:,#{ATOM})*$}i
17
+ AT_WORDS = {
18
+ 'reboot' => 'At reboot',
19
+ 'yearly' => 'Yearly (midnight on January 1st)',
20
+ 'annually' => 'Yearly (midnight on January 1st)',
21
+ 'monthly' => 'Monthly (midnight on the first of every month)',
22
+ 'weekly' => 'Weekly (midnight every Sunday)',
23
+ 'daily' => 'Daily, at midnight',
24
+ 'midnight' => 'Daily, at midnight',
25
+ 'hourly' => 'At the top of every hour'
26
+ }
27
+
28
+ def self.parse(str)
29
+ parser = Cron2English::Parser.new
30
+ parser.parse(str)
31
+ end
32
+
33
+ class Parser
34
+
35
+ def initialize
36
+ @dow = nil
37
+ @month = nil
38
+ @dow2num = {}
39
+ @month2num = {}
40
+ @num2dow = {}
41
+ @num2month = {}
42
+ end
43
+
44
+ def parse(str)
45
+ str = str.strip
46
+
47
+ if str =~ /^@(\w+)$/ and AT_WORDS[$1.downcase]
48
+ process_vixie($1)
49
+ else
50
+ bits = str.split(/[ \t]+/)
51
+ if bits.size == 5
52
+ process_trad(*bits)
53
+ else
54
+ give_up(str)
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def process_vixie(str)
62
+ [str]
63
+ end
64
+
65
+ def process_trad(m, h, day_of_month, month, dow)
66
+ month = MONTH2NUM[$1.downcase] if month =~ MONTH_REGEX
67
+ month = month.to_s if month
68
+ dow = DOW2NUM[$1.downcase] if dow =~ DOW_REGEX
69
+ dow = dow.to_s if dow
70
+ bits = [m, h, day_of_month, month, dow]
71
+ unparseable = []
72
+ bits_segmented = []
73
+ bits.each_with_index do |bit, i|
74
+ segments = []
75
+ if bit == '*'
76
+ segments << ['*']
77
+
78
+ elsif bit =~ %r<^\*/(\d+)$>
79
+ # a hack for "*/3" etc
80
+ segments << ['*', $1.to_i]
81
+
82
+ elsif bit =~ ATOMS_REGEX
83
+ bit.split(',').each do |thang|
84
+ if thang =~ %r<^(?:(\d+)|(?:(\d+)-(\d+)(?:/(\d+))?))$>
85
+ if $1
86
+ segments << [$1.to_i]
87
+ elsif $4
88
+ segments << [$2.to_i, $3.to_i, $4.to_i]
89
+ else
90
+ segments << [$2.to_i, $3.to_i]
91
+ end
92
+ else
93
+ unparseable << ("field %s: \"%s\"" % [i + 1, bit])
94
+ end
95
+ end
96
+ else
97
+ unparseable << ("field %s: \"%s\"" % [i + 1, bit])
98
+ end
99
+ bits_segmented << segments
100
+ end
101
+
102
+ give_up(unparseable.join("; ")) if unparseable.size > 0
103
+ bits_to_english(bits_segmented)
104
+ end
105
+
106
+ def bits_to_english(bits)
107
+ # This is the deep ugly scary guts of this program.
108
+ # The older and eldritch among you might recognize this as sort of a
109
+ # parody of bad old Lisp style of data-structure handling.
110
+ time_lines = []
111
+
112
+
113
+ #######################################################################
114
+ # Render the minutes and hours
115
+ if bits[0].size == 1 and bits[1].size == 1 and
116
+ bits[0][0].size == 1 and bits[1][0].size == 1 and
117
+ bits[0][0][0] != '*' and bits[1][0][0] != '*'
118
+ # It's a highly simplifiable time expression!
119
+ # This is a very common case. Like "46 13" -> 1:46pm
120
+ # Formally: when minute and hour are each a single number.
121
+
122
+ h = bits[1][0][0]
123
+ if bits[0][0][0] == 0
124
+ # Simply at the top of the hour, so just call it by the hour name.
125
+ time_lines << MIL2AMPM[h]
126
+
127
+ else
128
+ # Can't say "noon:02", so use an always-numeric time format:
129
+ time_lines << "%s:%02d:%s" % [
130
+ (h > 12) ? (h - 12) : h,
131
+ bits[0][0][0],
132
+ (h >= 12) ? 'pm' : 'am'
133
+ ]
134
+ end
135
+ time_lines[time_lines.size - 1] += ' on'
136
+
137
+ else
138
+ # It's not a highly simplifiable time expression
139
+
140
+ # First, minutes:
141
+ if bits[0][0][0] == '*'
142
+ if bits[0][0].size == 1 or bits[0][0][1] == 1
143
+ time_lines << 'every minute of'
144
+ else
145
+ time_lines << "every #{freq(bits[0][0][1])} minute of"
146
+ end
147
+
148
+ elsif bits[0].size == 1 and bits[0][0][0] == 0
149
+ # It's just a '0'. Ignore it -- instead of bothering
150
+ # to add a "0 minutes past"
151
+
152
+ elsif bits[0].none?{|x| x.size > 1}
153
+ # It's all like 7,10,15. Conjoinable
154
+ time_lines << conj_and(bits[0].map{|x| x[0]}) + (bits[0][-1][0] == 1 ? ' minute past' : ' minutes past')
155
+
156
+ else
157
+ # It's just gonna be long.
158
+ hunks = []
159
+ bits[0].each do |bit|
160
+ if bit.size == 1 # "7"
161
+ hunks << (bit[0] == 1 ? '1 minute' : "#{bit[0]} minutes")
162
+
163
+ elsif bit.size == 2 # "7-9"
164
+ hunks << ("from %d to %d %s" % [*bit, bit[1] == 1 ? 'minute' : 'minutes'])
165
+
166
+ elsif bit.size == 3 # "7-20/2"
167
+ hunks << ("every %d %s from %d to %d" % [bit[2],
168
+ bit[2] == 1 ? 'minute' : 'minutes',
169
+ bit[0],
170
+ bit[1]])
171
+ end
172
+ end
173
+ time_lines << (conj_and(hunks) + " past")
174
+ end
175
+
176
+ # Now hours
177
+ if bits[1][0][0] == '*'
178
+ if bits[1][0].size == 1 or bits[1][0][1] == 1
179
+ time_lines << 'every hour of'
180
+ else
181
+ time_lines << "every #{freq(bits[1][0][1])} hour of"
182
+ end
183
+
184
+ else
185
+ hunks = []
186
+ bits[1].each do |bit|
187
+ if bit.size == 1 # "7"
188
+ hunks << (MIL2AMPM[bit[0]] || "HOUR_#{bit[0]}??")
189
+
190
+ elsif bit.size == 2 # "7-9"
191
+ hunks << ("from %s to %s" % [MIL2AMPM[bit[0]] || "HOUR_#{bit[0]}??",
192
+ MIL2AMPM[bit[1]] || "HOUR_#{bit[1]}??"])
193
+
194
+ elsif bit.size == 3 # "7-20/2"
195
+ hunks << ("every %d %s from %s to %s" % [bit[2],
196
+ bit[2] == 1 ? 'hour' : 'hours',
197
+ MIL2AMPM[bit[0]] || "HOUR_#{bit[0]}??",
198
+ MIL2AMPM[bit[1]] || "HOUR_#{bit[2]}??"])
199
+ end
200
+ end
201
+ time_lines << (conj_and(hunks) + " of")
202
+ end
203
+ end
204
+ # End of hours and minutes
205
+
206
+ #######################################################################
207
+ # Day-of-month
208
+
209
+ if bits[2][0][0] == '*'
210
+ time_lines[-1].gsub!(/ on$/, '')
211
+ if bits[2][0].size == 1 or bits[2][0][1] == 1
212
+ time_lines << 'every day of'
213
+ else
214
+ time_lines << "every #{freq(bits[2][0][1])} day of"
215
+ end
216
+ else
217
+ hunks = []
218
+ bits[2].each do |bit|
219
+ if bit.size == 1 # "7"
220
+ hunks << "the #{ordinate(bit[0])}"
221
+
222
+ elsif bit.size == 2 # "7-9"
223
+ hunks << ("from the %s to the %s" % [ordinate(bit[0]), ordinate(bit[1])])
224
+
225
+ elsif bit.size == 3 # "7-20/2"
226
+ hunks << ("every %d %s from the %s to the %s" % [bit[2],
227
+ bit[2] == 1 ? 'day' : 'days',
228
+ ordinate(bit[0]),
229
+ ordinate(bit[1])])
230
+ end
231
+ end
232
+
233
+ # collapse the "the"s, if all the elements have one
234
+ if hunks.size > 1 and hunks.none?{|h| h !~ /^the /}
235
+ hunks = hunks.map{|h| h.gsub(/^the /, '')}
236
+ hunks[0] = "the #{hunks[0]}"
237
+ end
238
+
239
+ time_lines << "#{conj_and(hunks)} of"
240
+ end
241
+
242
+ #######################################################################
243
+ # Month
244
+
245
+ if bits[3][0][0] == '*'
246
+ if bits[3][0].size == 1 or bits[3][0][1] == 1
247
+ time_lines << 'every month'
248
+ else
249
+ time_lines << "every #{freq(bits[3][0][1])} month"
250
+ end
251
+ else
252
+ hunks = []
253
+ bits[3].each do |bit|
254
+ if bit.size == 1 # "7"
255
+ hunks << (NUM2MONTH_LONG[bit[0]] || "MONTH_#{bit[0]}??")
256
+
257
+ elsif bit.size == 2 # "7-9"
258
+ hunks << ("from %s to %s" % [NUM2MONTH_LONG[bit[0]] || "MONTH_#{bit[0]}??",
259
+ NUM2MONTH_LONG[bit[1]] || "MONTH_#{bit[1]}??"])
260
+
261
+ elsif bit.size == 3 # "7-20/2"
262
+ hunks << ("every %d %s from %s to %s" % [bit[2],
263
+ bit[2] == 1 ? 'month' : 'months',
264
+ NUM2MONTH_LONG[bit[0]] || "MONTH_#{bit[0]}??",
265
+ NUM2MONTH_LONG[bit[1]] || "MONTH_#{bit[1]}??"])
266
+ end
267
+ end
268
+
269
+ time_lines << conj_and(hunks)
270
+ end
271
+
272
+ #######################################################################
273
+ # Weekday
274
+ #
275
+ #
276
+ #
277
+ #
278
+ # From man 5 crontab:
279
+ # Note: The day of a command's execution can be specified by two fields
280
+ # -- day of month, and day of week. If both fields are restricted
281
+ # (ie, aren't *), the command will be run when either field matches the
282
+ # current time. For example, "30 4 1,15 * 5" would cause a command to
283
+ # be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
284
+ #
285
+ # [But if both fields ARE *, then it just means "every day".
286
+ # and if one but not both are *, then ignore the *'d one --
287
+ # so "1 2 3 4 *" means just 2:01, April 3rd
288
+ # and "1 2 * 4 5" means just 2:01, on every Friday in April
289
+ # But "1 2 3 4 5" means 2:01 of every 3rd or Friday in April. ]
290
+ #
291
+ #
292
+ #
293
+ #
294
+ # And that's a bit tricky.
295
+
296
+ if bits[4][0][0] == '*' and (bits[4][0].size == 1 or bits[4][0][1] == 1)
297
+ # Most common case: any weekday. Do nothing really.
298
+ #
299
+ # Hmm, does "*/1" really many "*" here, given the above note?
300
+
301
+ # Tidy things up while we're here:
302
+ if time_lines[-2] == 'every day of' and
303
+ time_lines[-1] == 'every month'
304
+ time_lines[-2] == 'every day'
305
+ time_lines.pop
306
+ end
307
+
308
+ else
309
+ # Ugh, there's some restriction on weekdays.
310
+
311
+ # Translate the DOW-expression
312
+ expression = nil
313
+ hunks = []
314
+ bits[4].each do |bit|
315
+ if bit.size == 1
316
+ hunks << (NUM2DOW_LONG[bit[0]] || "DOW_#{bit[0]}??")
317
+
318
+ elsif bit.size == 2
319
+ if bit[0] == '*' # It's like */3
320
+ # hunks << ("every %s day of the week" % freq(bit[1]))
321
+ # The above was ambiguous: "every third day of the week"
322
+ # sounds synonymous with just "3"
323
+ if bit[1] == 2
324
+ # common and unambiguous case.
325
+ hunks << "every other day of the week"
326
+ else
327
+ # rare cases: N > 2
328
+ hunks << "every #{bit[1]} days of the week"
329
+ # sounds clunky, but it's a clunky concept
330
+ end
331
+
332
+ else
333
+ # It's like "7-9"
334
+ hunks << ("%s through %s" % [NUM2DOW_LONG[bit[0]] || "DOW_#{bit[0]}??",
335
+ NUM2DOW_LONG[bit[1]] || "DOW_#{bit[1]}??"])
336
+ end
337
+
338
+ elsif bit.size == 3 # "7-20/2"
339
+ hunks << ("every %s %s from %s through %s" % [ordinate_soft(bit[2]),
340
+ 'day',
341
+ NUM2DOW_LONG[bit[0]] || "DOW_#{bit[0]}??",
342
+ NUM2DOW_LONG[bit[1]] || "DOW_#{bit[1]}??"])
343
+ end
344
+ end
345
+ expression = conj_or(hunks)
346
+
347
+ # Now figure out where to put it. . . .
348
+
349
+ if time_lines[-2] == 'every day of'
350
+ # Unrestricted day-of-month, hooray.
351
+ if time_lines[-1] == 'every month'
352
+ # change it to "every Thursday", killing the "of every month"
353
+ time_lines[-2] = "every #{expression}"
354
+ time_lines[-2].gsub!(%r{every every }, 'every ')
355
+ time_lines.pop
356
+ else
357
+ # change it to "every Thursday in"
358
+ time_lines[-2] = "every #{expression} in"
359
+ time_lines[2].gsub!(%r{every every }, 'every ')
360
+ end
361
+ else
362
+ # This is the messy case where there's a DOM and DOW restriction
363
+
364
+ time_lines[-2] += " -- or every #{expression} in --"
365
+ # Yes, dashes look very strange, but then this is a very rare case.
366
+ time_lines[-2].gsub!(%r{every every }, 'every ')
367
+ end
368
+ end
369
+ time_lines[-1].sub!(/ of$/, '')
370
+ return time_lines
371
+ end
372
+
373
+ def conj_and(bits)
374
+ if bits.grep(/every|from/).any?
375
+ # put in semicolons in case of complex constituency
376
+ return bits.join('; and ') if bits.size < 2
377
+ last = bits.pop
378
+ return "#{bits.join('; ')}; and #{last}"
379
+ else
380
+ return bits.join(' and ') if bits.size < 3
381
+ last = bits.pop
382
+ return "#{bits.join(', ')}, and #{last}"
383
+ end
384
+ end
385
+
386
+ def conj_or(bits)
387
+ if bits.grep(/every|from/).any?
388
+ # put in semicolons in case of complex constituency
389
+ return bits.join('; or ') if bits.size < 2
390
+ last = bits.pop
391
+ return "#{bits.join('; ')}; or #{last}"
392
+ else
393
+ return bits.join(' or ') if bits.size < 3
394
+ last = bits.pop
395
+ return "#{bits.join(', ')}, or #{last}"
396
+ end
397
+ end
398
+
399
+ ORDINATIONS = %w{zeroth first second third fourth fifth sixth seventh eighth ninth tenth}
400
+
401
+ def ordsuf(n=nil)
402
+ return 'th' if not n or n.to_i == 0
403
+ # 'th' for undef, 0, or anything non-number
404
+ n = n.abs
405
+ return 'th' unless n == n.to_i
406
+ n %= 100
407
+ return 'th' if n == 11 or n == 12 or n == 13
408
+ n %= 10
409
+ return 'st' if n == 1
410
+ return 'nd' if n == 2
411
+ return 'rd' if n == 3
412
+ return 'th'
413
+ end
414
+
415
+ def ordinate(n=0)
416
+ ORDINATIONS[n] || "#{n}#{ordsuf(n)}"
417
+ end
418
+
419
+ def freq(n=0)
420
+ # frequentive form. Like ordinal, except that 2 -> 'other'
421
+ # (as in every other)
422
+ return 'other' if n == 2
423
+ ORDINATIONS[n] || "#{n}#{ordsuf(n)}"
424
+ end
425
+
426
+ def ordinate_soft(n=0)
427
+ "#{n}#{ordsuf(n)}"
428
+ end
429
+
430
+ def give_up(str)
431
+ raise "Unparseable crontab spec: #{str}"
432
+ end
433
+
434
+
435
+ end
436
+
437
+ end
@@ -0,0 +1,3 @@
1
+ module Cron2English
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'cron2english/all.rb'
2
+
3
+ module Cron2English
4
+
5
+ end
6
+
@@ -0,0 +1,36 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Cron2English do
4
+
5
+ [
6
+ ["40 5 * * *", ["5:40am", "every day"]],
7
+ ["0 5 * * 1", ["5am", "every Monday"]],
8
+ ["10 8 15 * *", ["8:10:am on", "the 15th of", "every month"]],
9
+ ["40 5 * * *", ["5:40:am", "every day"]],
10
+ ["50 6 * * 1", ["6:50:am", "every Monday"]],
11
+ ["1 2 * apr mOn", ["2:01:am", "every Monday in", "April"]],
12
+ ["1 2 3 4 7", ["2:01:am on", "the third of -- or every Sunday in --", "April"]],
13
+ ["1-20/3 * * * *", ["every 3 minutes from 1 to 20 past", "every hour of", "every day"]],
14
+ ["1,2,3 * * * *", ["1, 2, and 3 minutes past", "every hour of", "every day"]],
15
+ ["1-9,15-30 * * * *", ["from 1 to 9 minutes; and from 15 to 30 minutes past", "every hour of", "every day"]],
16
+ ["1-9/3,15-30/4 * * * *", ["every 3 minutes from 1 to 9; and every 4 minutes from 15 to 30 past", "every hour of", "every day"]],
17
+ ["1 2 3 jan mon", ["2:01:am on", "the third of -- or every Monday in --", "January"]],
18
+ ["1 2 3 4 mON", ["2:01:am on", "the third of -- or every Monday in --", "April"]],
19
+ ["1 2 3 jan 5", ["2:01:am on", "the third of -- or every Friday in --", "January"]],
20
+ ["@reboot", ["reboot"]],
21
+ ["@yearly", ["yearly"]],
22
+ ["@annually", ["annually"]],
23
+ ["@monthly", ["monthly"]],
24
+ ["@weekly", ["weekly"]],
25
+ ["@daily", ["daily"]],
26
+ ["@midnight", ["midnight"]],
27
+ ["@hourly", ["hourly"]],
28
+ ["*/3 * * * *", ["every third minute of", "every hour of", "every day"]],
29
+ ].each do |cronspec, english|
30
+ it "should parse the cronspec #{cronspec}" do
31
+ result = Cron2English.parse(cronspec)
32
+ result.should == english
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'rspec/matchers'
5
+ require 'cron2english'
6
+
7
+ # Requires supporting files with custom matchers and macros, etc.,
8
+ # in ./support/ and its subdirectories.
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
+
11
+ RSpec.configure do |config|
12
+
13
+ end
14
+
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cron2english
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul A. Jungwirth
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.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: 2.4.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 2.4.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
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: pj@illuminatedcomputing.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - .gitignore
62
+ - .rspec
63
+ - .rvmrc
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - cron2english.gemspec
70
+ - lib/cron2english.rb
71
+ - lib/cron2english/all.rb
72
+ - lib/cron2english/version.rb
73
+ - spec/parse_spec.rb
74
+ - spec/spec_helper.rb
75
+ homepage: http://github.com/pjungwir/cron2english
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 2.0.3
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Converts a crontab schedule into English text.
99
+ test_files: []