cronex 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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