cronex 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ module Cronex
2
+ class SecondsDescription < Description
3
+ def single_item_description(expression)
4
+ expression
5
+ end
6
+
7
+ def interval_description_format(expression)
8
+ format(resources.get('every_x_seconds'), expression)
9
+ end
10
+
11
+ def between_description_format(expression)
12
+ resources.get('seconds_through_past_the_minute')
13
+ end
14
+
15
+ def description_format(expression)
16
+ resources.get('at_x_seconds_past_the_minute')
17
+ end
18
+
19
+ def starting_description_format(expression)
20
+ resources.get('starting') + ' ' + description_format(expression)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Cronex
2
+ class YearDescription < Description
3
+ def single_item_description(expression)
4
+ DateTime.new(Integer(expression)).strftime('%Y')
5
+ end
6
+
7
+ def interval_description_format(expression)
8
+ format(', ' + resources.get('every_x') + ' ' + plural(expression, resources.get('year'), resources.get('years')), expression)
9
+ end
10
+
11
+ def between_description_format(expression)
12
+ ', ' + resources.get('between_description_format')
13
+ end
14
+
15
+ def description_format(expression)
16
+ ', ' + resources.get('only_in')
17
+ end
18
+
19
+ def starting_description_format(expression)
20
+ resources.get('starting') + ' ' + resources.get('in_x')
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ module Cronex
2
+ ExpressionError = Class.new(StandardError)
3
+ ResourceError = Class.new(StandardError)
4
+ end
@@ -0,0 +1,189 @@
1
+ module Cronex
2
+
3
+ CASINGS = [:title, :sentence, :lower]
4
+ SEGMENTS = [:seconds, :minutes, :hours, :dayofmonth, :month, :dayofweek, :year, :timeofday, :full]
5
+
6
+ SPECIAL_CHARS = ['/', '-', ',', '*']
7
+
8
+ CRONEX_OPTS = {
9
+ casing: :sentence,
10
+ verbose: false,
11
+ zero_based_dow: true,
12
+ use_24_hour_time_format: false,
13
+ throw_exception_on_parse_error: true
14
+ }
15
+
16
+ class ExpressionDescriptor
17
+ attr_accessor :expression, :expression_parts, :options, :parsed, :resources
18
+
19
+ def initialize(expression, options = {}, locale = nil)
20
+ @expression = expression
21
+ @options = CRONEX_OPTS.merge(options)
22
+ @expression_parts = []
23
+ @parsed = false
24
+ @resources = Cronex::Resource.new(locale)
25
+ end
26
+
27
+ def to_hash
28
+ Hash[SEGMENTS.take(7).zip(expression_parts)]
29
+ end
30
+
31
+ def description(type = :full)
32
+ desc = ''
33
+
34
+ begin
35
+ unless parsed
36
+ parser = Parser.new(expression, options)
37
+ @expression_parts = parser.parse
38
+ @parsed = true
39
+ end
40
+
41
+ desc = case type
42
+ when :full
43
+ full_description(expression_parts)
44
+ when :timeofday
45
+ time_of_day_description(expression_parts)
46
+ when :hours
47
+ hours_description(expression_parts)
48
+ when :minutes
49
+ minutes_description(expression_parts)
50
+ when :seconds
51
+ seconds_description(expression_parts)
52
+ when :dayofmonth
53
+ day_of_month_description(expression_parts)
54
+ when :month
55
+ month_description(expression_parts)
56
+ when :dayofweek
57
+ day_of_week_description(expression_parts)
58
+ when :year
59
+ year_description(expression_parts)
60
+ else
61
+ seconds_description(expression_parts)
62
+ end
63
+ rescue StandardError => e
64
+ if options[:throw_exception_on_parse_error]
65
+ raise e
66
+ else
67
+ desc = e.message
68
+ puts "Exception parsing expression: #{e}"
69
+ end
70
+ end
71
+
72
+ desc
73
+ end
74
+
75
+ def seconds_description(expression_parts)
76
+ desc = SecondsDescription.new(resources)
77
+ desc.segment_description(expression_parts[0], resources.get('every_second'))
78
+ end
79
+
80
+ def minutes_description(expression_parts)
81
+ desc = MinutesDescription.new(resources)
82
+ desc.segment_description(expression_parts[1], resources.get('every_minute'))
83
+ end
84
+
85
+ def hours_description(expression_parts)
86
+ desc = HoursDescription.new(resources)
87
+ desc.segment_description(expression_parts[2], resources.get('every_hour'))
88
+ end
89
+
90
+ def year_description(expression_parts)
91
+ desc = YearDescription.new(resources, options)
92
+ desc.segment_description(expression_parts[6], ', ' + resources.get('every_year'))
93
+ end
94
+
95
+ def day_of_week_description(expression_parts)
96
+ desc = DayOfWeekDescription.new(resources, options)
97
+ desc.segment_description(expression_parts[5], ', ' + resources.get('every_day'))
98
+ end
99
+
100
+ def month_description(expression_parts)
101
+ desc = MonthDescription.new(resources)
102
+ desc.segment_description(expression_parts[4], '')
103
+ end
104
+
105
+ def day_of_month_description(expression_parts)
106
+ exp = expression_parts[3].gsub('?', '*')
107
+ if exp == 'L'
108
+ description = ', ' + resources.get('on_the_last_day_of_the_month')
109
+ elsif exp == 'LW' || exp == 'WL'
110
+ description = ', ' + resources.get('on_the_last_weekday_of_the_month')
111
+ else
112
+ match = exp.match(/(\d{1,2}W)|(W\d{1,2})/)
113
+ if match
114
+ day_num = Integer(match[0].gsub('W', ''))
115
+ day_str = day_num == 1 ? resources.get('first_weekday') : format(resources.get('weekday_nearest_day'), day_num)
116
+ description = format(', ' + resources.get('on_the_of_the_month'), day_str)
117
+ else
118
+ desc = DayOfMonthDescription.new(resources)
119
+ description = desc.segment_description(exp, ', ' + resources.get('every_day'))
120
+ end
121
+ end
122
+ description
123
+ end
124
+
125
+ def time_of_day_description(expression_parts)
126
+ sec_exp, min_exp, hour_exp = expression_parts
127
+ description = ''
128
+ if [sec_exp, min_exp, hour_exp].all? { |exp| !Cronex::Utils.include_any?(exp, SPECIAL_CHARS) }
129
+ # specific time of day (i.e. 10 14)
130
+ description += resources.get('at') + ' ' + Cronex::Utils.format_time(hour_exp, min_exp, sec_exp)
131
+ elsif min_exp.include?('-') && !min_exp.include?('/') && !min_exp.include?(',') && !Cronex::Utils.include_any?(hour_exp, SPECIAL_CHARS)
132
+ # Minute range in single hour (e.g. 0-10 11)
133
+ min_parts = min_exp.split('-')
134
+ description += format(
135
+ resources.get('every_minute_between'),
136
+ Cronex::Utils.format_time(hour_exp, min_parts[0]),
137
+ Cronex::Utils.format_time(hour_exp, min_parts[1]))
138
+ elsif hour_exp.include?(',') && !Cronex::Utils.include_any?(min_exp, SPECIAL_CHARS)
139
+ # Hours list with single minute (e.g. 30 6,14,16)
140
+ hour_parts = hour_exp.split(',')
141
+ description += resources.get('at')
142
+ h_parts = hour_parts.map { |part| ' ' + Cronex::Utils.format_time(part, min_exp) }
143
+ description += h_parts[0...-1].join(',') + ' ' + resources.get('and') + h_parts.last
144
+ else
145
+ sec_desc = seconds_description(expression_parts)
146
+ min_desc = minutes_description(expression_parts)
147
+ hour_desc = hours_description(expression_parts)
148
+ description += sec_desc
149
+ description += ', ' if description.size > 0 && !min_desc.empty?
150
+ description += min_desc
151
+ description += ', ' if description.size > 0 && !hour_desc.empty?
152
+ description += hour_desc
153
+ end
154
+ description
155
+ end
156
+
157
+ def full_description(expression_parts)
158
+ time_segment = time_of_day_description(expression_parts)
159
+ dom_desc = day_of_month_description(expression_parts)
160
+ month_desc = month_description(expression_parts)
161
+ dow_desc = day_of_week_description(expression_parts)
162
+ year_desc = year_description(expression_parts)
163
+ day_desc = expression_parts[3] == '*' ? dow_desc : dom_desc
164
+ description = format('%s%s%s%s', time_segment, day_desc, month_desc, year_desc)
165
+ description = transform_verbosity(description, options[:verbose])
166
+ description = transform_case(description, options[:casing])
167
+ description
168
+ end
169
+
170
+ def transform_case(desc, case_type = :lower)
171
+ case case_type
172
+ when :sentence
173
+ desc.sub(/\w+/, &:capitalize)
174
+ when :title
175
+ desc.gsub(/\w+/, &:capitalize)
176
+ else
177
+ desc.downcase
178
+ end
179
+ end
180
+
181
+ def transform_verbosity(desc, verbose = false)
182
+ return desc if verbose
183
+ parts = %w(every_minute every_hour every_day)
184
+ parts.each { |part| desc.gsub!(resources.get(part.gsub('_', '_1_')), resources.get(part)) }
185
+ parts.each { |part| desc.gsub!(', ' + resources.get(part), '') }
186
+ desc
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,92 @@
1
+ module Cronex
2
+
3
+ DAYS = Date::ABBR_DAYNAMES.map(&:upcase)
4
+ MONTHS = Date::ABBR_MONTHNAMES[1..-1].map(&:upcase)
5
+
6
+ DAY_NUM = Hash[DAYS.zip(0..(DAYS.size - 1))]
7
+ MONTH_NUM = Hash[MONTHS.zip(1..MONTHS.size)]
8
+
9
+ # abbr dayname => long dayname
10
+ DAY_DAY = Hash[DAYS.zip(Date::DAYNAMES.map(&:upcase))]
11
+
12
+ class Parser
13
+ attr_accessor :expression, :options
14
+
15
+ def initialize(expression, options = {})
16
+ @expression = expression
17
+ @options = options
18
+ end
19
+
20
+ def parse(exp = expression)
21
+ parsed_parts = Array.new(7, '')
22
+
23
+ fail ExpressionError, 'Error: Expression null or emtpy' unless Cronex::Utils.present?(exp)
24
+ parts = sanitize(exp).split(' ')
25
+ len = parts.size
26
+
27
+ if len < 5
28
+ fail ExpressionError, "Error: Expression only has #{len} parts. At least 5 parts are required"
29
+ elsif len == 5
30
+ # 5 part CRON so shift array past seconds element
31
+ parsed_parts.insert(1, *parts)
32
+ elsif len == 6
33
+ # if last element ends with 4 digits, a year element has been supplied and no seconds element
34
+ if parts.last.match(/\d{4}$/)
35
+ parsed_parts.insert(1, *parts)
36
+ else
37
+ parsed_parts.insert(0, *parts)
38
+ end
39
+ elsif len == 7
40
+ parsed_parts = parts
41
+ else
42
+ fail ExpressionError, "Error: Expression has too many parts (#{len}). Expression must not have more than 7 parts"
43
+ end
44
+
45
+ parsed_parts = parsed_parts.take(7) # ; p parsed_parts
46
+ normalize(parsed_parts, options)
47
+ end
48
+
49
+ def sanitize(exp = expression)
50
+ # remove extra spaces
51
+ exp = exp.strip.gsub(/\s+/, ' ').upcase
52
+ # convert non-standard day names (e.g. THURS, TUES) to 3-letter names
53
+ DAY_DAY.each do |day, longname|
54
+ matched = exp.scan(/\W*(#{day}\w*)/).flatten.uniq
55
+ matched.each { |m| exp.gsub!(m, day) if longname.include?(m) }
56
+ end
57
+ exp
58
+ end
59
+
60
+ def normalize(expression_parts, options = {})
61
+ parts = expression_parts.dup
62
+
63
+ # convert ? to * only for DOM and DOW
64
+ parts[3].gsub!('?', '*')
65
+ parts[5].gsub!('?', '*')
66
+
67
+ # Convert 0/, 1/ to */
68
+ parts[0].gsub!('0/', '*/') if parts[0].start_with?('0/') # seconds
69
+ parts[1].gsub!('0/', '*/') if parts[1].start_with?('0/') # minutes
70
+ parts[2].gsub!('0/', '*/') if parts[2].start_with?('0/') # hours
71
+ parts[3].gsub!('1/', '*/') if parts[3].start_with?('1/') # day of month
72
+ parts[4].gsub!('1/', '*/') if parts[4].start_with?('1/') # month
73
+ parts[5].gsub!('1/', '*/') if parts[5].start_with?('1/') # day of week
74
+
75
+ # convert */1 to *
76
+ parts = parts.map { |part| part == '*/1' ? '*' : part }
77
+
78
+ # convert SUN-SAT format to 0-6 format
79
+ DAY_NUM.each { |day, i| i += 1 if !options[:zero_based_dow]; parts[5].gsub!(day, i.to_s) }
80
+
81
+ # convert JAN-DEC format to 1-12 format
82
+ MONTH_NUM.each { |month, i| parts[4].gsub!(month, i.to_s) }
83
+
84
+ # convert 0 second to (empty)
85
+ parts[0] = '' if parts[0] == '0'
86
+
87
+ # convert 0 DOW to 7 so that 0 for Sunday in zeroBasedDayOfWeek is valid
88
+ parts[5] = '7' if (!options || options[:zero_based_dow]) && parts[5] == '0' # ; p parts
89
+ parts
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,33 @@
1
+ module Cronex
2
+
3
+ RESOURCES_DIR = File.expand_path('../../../resources', __FILE__)
4
+
5
+ class Resource
6
+ attr_reader :locale, :messages
7
+
8
+ def initialize(loc = :en)
9
+ self.locale = loc || :en
10
+ end
11
+
12
+ def locale=(loc)
13
+ result = load(loc)
14
+ if result.nil? || result.empty?
15
+ fail ResourceError, "Error: Invalid resource file for '#{loc}' locale"
16
+ else
17
+ @locale = loc
18
+ @messages = result
19
+ end
20
+ end
21
+
22
+ def load(loc)
23
+ file = File.join(RESOURCES_DIR, "resources_#{loc}.yml")
24
+ fail ResourceError, "Resource file #{file} for '#{loc}' locale not found" unless File.exist?(file)
25
+ YAML.load_file(file)
26
+ end
27
+
28
+ def [](key)
29
+ @messages[key]
30
+ end
31
+ alias_method :get, :[]
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ module Cronex
2
+ module Utils
3
+ extend self
4
+
5
+ def present?(str)
6
+ !str.to_s.strip.empty?
7
+ end
8
+
9
+ def include_any?(str, chars)
10
+ chars.any? { |char| str.include?(char) }
11
+ end
12
+
13
+ def number?(str)
14
+ Integer(str) rescue nil
15
+ end
16
+
17
+ def day_of_week_name(number)
18
+ Date::DAYNAMES[number.to_i % 7]
19
+ end
20
+
21
+ def format_minutes(minute_expression)
22
+ if minute_expression.include?(',')
23
+ minute_expression.split(',').map { |m| format('%02d', m) }.join(',')
24
+ else
25
+ format('%02d', minute_expression)
26
+ end
27
+ end
28
+
29
+ def format_time(hour_expression, minute_expression, second_expression = '')
30
+ hour = Integer(hour_expression)
31
+ period = hour >= 12 ? 'PM' : 'AM'
32
+ hour -= 12 if hour > 12
33
+ minute = Integer(minute_expression)
34
+ minute = format('%02d', minute)
35
+ second = ''
36
+ if Cronex::Utils.present?(second_expression)
37
+ second = Integer(second_expression)
38
+ second = ':' + format('%02d', second)
39
+ end
40
+ format('%s:%s%s %s', hour, minute, second, period)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module Cronex
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,56 @@
1
+ # resources_en.yml
2
+ ---
3
+ expression_empty_exception: 'Expression cannot be null or empty'
4
+ interval_description_format: 'every %s days of the week'
5
+ between_description_format: '%s through %s'
6
+ between_weekday_description_format: '%s through %s'
7
+ on_the_day_of_the_month: 'on the {{nth}} %s of the month'
8
+ on_the_of_the_month: 'on the %s of the month'
9
+ on_the_last_of_the_month: 'on the last %s of the month'
10
+ on_the_last_day_of_the_month: 'on the last day of the month'
11
+ on_the_last_weekday_of_the_month: 'on the last weekday of the month'
12
+ between_days_of_the_month: 'between day %s and %s of the month'
13
+ seconds_through_past_the_minute: 'seconds %s through %s past the minute'
14
+ between_x_and_y: 'between %s and %s'
15
+ past_the_hour: 'past the hour'
16
+ at_x_seconds_past_the_minute: 'at %s seconds past the minute'
17
+ minutes_through_past_the_hour: 'minutes %s through %s past the hour'
18
+ on_day_of_the_month: 'on day %s of the month'
19
+ first_weekday: 'first weekday'
20
+ weekday_nearest_day: 'weekday nearest day %s'
21
+ starting: 'starting'
22
+ only_on: 'only on %s'
23
+ only_in: 'only in %s'
24
+ every_x_seconds: 'every %s seconds'
25
+ every_minute_between: 'Every minute between %s and %s'
26
+ every_second: 'every second'
27
+ every_minute: 'every minute'
28
+ every_1_minute: 'every 1 minute'
29
+ every_hour: 'every hour'
30
+ every_1_hour: 'every 1 hour'
31
+ every_day: 'every day'
32
+ every_1_day: 'every 1 day'
33
+ every_year: 'every year'
34
+ every_x: 'every %s'
35
+ at_x: 'at %s'
36
+ on_x: 'on %s'
37
+ in_x: 'in %s'
38
+ first: first
39
+ second: second
40
+ third: third
41
+ fourth: fourth
42
+ fifth: fifth
43
+ time_pm: PM
44
+ time_am: AM
45
+ and: and
46
+ at: At
47
+ day: day
48
+ days: days
49
+ hour: hour
50
+ hours: hours
51
+ minute: minute
52
+ minutes: minutes
53
+ month: month
54
+ months: months
55
+ year: year
56
+ years: years