aws-cron-parser 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +32 -0
- data/LICENSE.txt +21 -0
- data/README.md +117 -0
- data/lib/aws_cron_parser/describer/cron_composer.rb +198 -0
- data/lib/aws_cron_parser/describer/day_composer.rb +74 -0
- data/lib/aws_cron_parser/describer/field_describers/base.rb +108 -0
- data/lib/aws_cron_parser/describer/field_describers/day_of_month.rb +88 -0
- data/lib/aws_cron_parser/describer/field_describers/day_of_week.rb +143 -0
- data/lib/aws_cron_parser/describer/field_describers/month.rb +92 -0
- data/lib/aws_cron_parser/describer/field_describers/time_field.rb +101 -0
- data/lib/aws_cron_parser/describer/month_composer.rb +29 -0
- data/lib/aws_cron_parser/describer/template.en.yml +183 -0
- data/lib/aws_cron_parser/describer/template.rb +87 -0
- data/lib/aws_cron_parser/describer/time_composer.rb +255 -0
- data/lib/aws_cron_parser/describer.rb +131 -0
- data/lib/aws_cron_parser/fugit_adapter.rb +64 -0
- data/lib/aws_cron_parser/helpers/calendar.rb +115 -0
- data/lib/aws_cron_parser/helpers/clock.rb +50 -0
- data/lib/aws_cron_parser/helpers/cron_expression.rb +277 -0
- data/lib/aws_cron_parser/helpers/numbers.rb +32 -0
- data/lib/aws_cron_parser/helpers.rb +17 -0
- data/lib/aws_cron_parser/parser.rb +152 -0
- data/lib/aws_cron_parser/parsers/aws_at.rb +32 -0
- data/lib/aws_cron_parser/parsers/aws_cron.rb +413 -0
- data/lib/aws_cron_parser/parsers/aws_rate.rb +33 -0
- data/lib/aws_cron_parser/parsers/base.rb +21 -0
- data/lib/aws_cron_parser/parsers/quartz_cron.rb +21 -0
- data/lib/aws_cron_parser/validator.rb +31 -0
- data/lib/aws_cron_parser/validators/aws_at_validator.rb +28 -0
- data/lib/aws_cron_parser/validators/aws_cron_validator.rb +212 -0
- data/lib/aws_cron_parser/validators/aws_macro_validator.rb +13 -0
- data/lib/aws_cron_parser/validators/aws_rate_validator.rb +17 -0
- data/lib/aws_cron_parser/validators/base.rb +23 -0
- data/lib/aws_cron_parser/validators/quartz_cron_validator.rb +132 -0
- data/lib/aws_cron_parser/version.rb +5 -0
- data/lib/aws_cron_parser.rb +48 -0
- metadata +127 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module AWSCronParser
|
|
6
|
+
module Describer
|
|
7
|
+
module FieldDescribers
|
|
8
|
+
# Describes day-of-week field
|
|
9
|
+
class DayOfWeek < Base
|
|
10
|
+
NTH_PATTERN = /^(?<dow>\d|[A-Z]{3})#(?<occurrence>\d)$/i
|
|
11
|
+
LAST_PATTERN = /^(?<dow>\d|[A-Z]{3})L$/i
|
|
12
|
+
# Override to accept day names like MON-FRI
|
|
13
|
+
RANGE_PATTERN = /^(?<start>\d+|[A-Z]{3})-(?<end>\d+|[A-Z]{3})$/i
|
|
14
|
+
# Override to accept a named base like MON/2, which Base's numeric-only
|
|
15
|
+
# pattern rejects, leaving the raw field in the description.
|
|
16
|
+
STEP_PATTERN = %r{^(?:(?<base>\*|\d+|[A-Z]{3})|(?<start>\d+|[A-Z]{3})-(?<end>\d+|[A-Z]{3}))/(?<step>\d+)$}i
|
|
17
|
+
LIST_PATTERN = /^([\d,]+|[A-Z]{3}(,[A-Z]{3})*)$/i
|
|
18
|
+
|
|
19
|
+
DAY_ABBR_TO_NAME = {
|
|
20
|
+
'SUN' => 'Sunday', 'MON' => 'Monday', 'TUE' => 'Tuesday',
|
|
21
|
+
'WED' => 'Wednesday', 'THU' => 'Thursday', 'FRI' => 'Friday', 'SAT' => 'Saturday'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def describe
|
|
25
|
+
return nil if wildcard?
|
|
26
|
+
|
|
27
|
+
# AWS-specific patterns
|
|
28
|
+
if is_aws
|
|
29
|
+
return describe_nth_weekday if value.match?(NTH_PATTERN)
|
|
30
|
+
return describe_last_weekday if value.match?(LAST_PATTERN)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
super # Use base class patterns (range, list, single)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def describe_nth_weekday
|
|
39
|
+
match = value.match(NTH_PATTERN)
|
|
40
|
+
dow_value = match[:dow]
|
|
41
|
+
occurrence = match[:occurrence].to_i
|
|
42
|
+
|
|
43
|
+
day_text = parse_day_name(dow_value)
|
|
44
|
+
nth_word = Template.ordinal_word(occurrence)
|
|
45
|
+
|
|
46
|
+
Template.interpolate('day_of_week.nth_occurrence', nth_word: nth_word, day_name: day_text)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def describe_last_weekday
|
|
50
|
+
match = value.match(LAST_PATTERN)
|
|
51
|
+
dow_value = match[:dow]
|
|
52
|
+
|
|
53
|
+
day_text = parse_day_name(dow_value)
|
|
54
|
+
Template.interpolate('day_of_week.last_occurrence', day_name: day_text)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def describe_step
|
|
58
|
+
match = value.match(STEP_PATTERN)
|
|
59
|
+
return nil unless match
|
|
60
|
+
|
|
61
|
+
step = match[:step].to_i
|
|
62
|
+
base = match[:base] || match[:start]
|
|
63
|
+
|
|
64
|
+
base == '*' ? step_wildcard(step) : step_from(base, step)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def step_wildcard(step)
|
|
68
|
+
Template.interpolate('day_of_week.step', step: step)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def step_from(start_val, step)
|
|
72
|
+
Template.interpolate('day_of_week.step_from', step: step, start_day: parse_day_name(start_val))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def describe_range
|
|
76
|
+
return nil unless value.match?(RANGE_PATTERN)
|
|
77
|
+
|
|
78
|
+
match = value.match(RANGE_PATTERN)
|
|
79
|
+
start_val = match[:start]
|
|
80
|
+
end_val = match[:end]
|
|
81
|
+
|
|
82
|
+
# Handle both numeric and name ranges (e.g., "MON-FRI")
|
|
83
|
+
start_name = parse_day_name(start_val)
|
|
84
|
+
end_name = parse_day_name(end_val)
|
|
85
|
+
|
|
86
|
+
Template.interpolate('day_of_week.range', start_day: start_name, end_day: end_name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Override to handle lists like "MON,WED,FRI"
|
|
90
|
+
def describe_list
|
|
91
|
+
return nil unless value.include?(',') && value.match?(LIST_PATTERN)
|
|
92
|
+
|
|
93
|
+
items = value.split(',').map(&:strip)
|
|
94
|
+
formatted = items.map { |v| parse_day_name(v) }
|
|
95
|
+
list_template(formatted)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def list_template(items)
|
|
99
|
+
Template.interpolate('day_of_week.list', items: format_list(items))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def range_template(start_val, end_val)
|
|
103
|
+
start_name = parse_day_name(start_val)
|
|
104
|
+
end_name = parse_day_name(end_val)
|
|
105
|
+
Template.interpolate('day_of_week.range', start_day: start_name, end_day: end_name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Base coerces the value with to_i before formatting, which collapses a
|
|
109
|
+
# name like FRI to 0. single_template already accepts either form.
|
|
110
|
+
def describe_single
|
|
111
|
+
single_template(value)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def single_template(val)
|
|
115
|
+
day_name = parse_day_name(val)
|
|
116
|
+
Template.interpolate('day_of_week.single', day_name: day_name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def format_value(val)
|
|
120
|
+
parse_day_name(val)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def parse_day_name(val)
|
|
124
|
+
return day_name(val, is_aws: is_aws) if val.to_s.match?(/^\d+$/)
|
|
125
|
+
return DAY_ABBR_TO_NAME[val.to_s.upcase] if val.to_s.match?(/^[A-Z]{3}$/i)
|
|
126
|
+
|
|
127
|
+
val
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def format_list(items)
|
|
131
|
+
return items.first if items.length == 1
|
|
132
|
+
|
|
133
|
+
if items.length == 2
|
|
134
|
+
items.join(' and ')
|
|
135
|
+
else
|
|
136
|
+
last = items.pop
|
|
137
|
+
"#{items.join(', ')}, and #{last}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module AWSCronParser
|
|
6
|
+
module Describer
|
|
7
|
+
module FieldDescribers
|
|
8
|
+
# Describes month field
|
|
9
|
+
class Month < Base
|
|
10
|
+
def describe
|
|
11
|
+
return nil if wildcard?
|
|
12
|
+
|
|
13
|
+
# Normalize abbreviations to numbers first
|
|
14
|
+
@value = normalize_month_value(value)
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def step_wildcard(step)
|
|
22
|
+
Template.interpolate('month.step',
|
|
23
|
+
step: step,
|
|
24
|
+
step_plural: Template.plural_for(step),
|
|
25
|
+
start_month: month_name(1))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def step_from(start_val, step)
|
|
29
|
+
Template.interpolate('month.step_range',
|
|
30
|
+
step: step,
|
|
31
|
+
step_plural: Template.plural_for(step),
|
|
32
|
+
start_month: month_name(start_val))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def list_template(items)
|
|
36
|
+
Template.interpolate('month.list', items: format_list(items))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Base already ran the value through format_value, so month_name has
|
|
40
|
+
# been applied; calling it again would turn "January" into "month January".
|
|
41
|
+
def range_template(start_val, end_val)
|
|
42
|
+
Template.interpolate('month.range', start_month: start_val, end_month: end_val)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def single_template(val)
|
|
46
|
+
Template.interpolate('month.single', month_name: val)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def format_value(val)
|
|
50
|
+
month_name(val.to_i)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def normalize_month_value(val)
|
|
54
|
+
return val if val == '*' || val.match?(/^\d/)
|
|
55
|
+
|
|
56
|
+
# Handle ranges
|
|
57
|
+
if val.include?('-')
|
|
58
|
+
parts = val.split('-')
|
|
59
|
+
return "#{month_abbr_to_num(parts[0])}-#{month_abbr_to_num(parts[1])}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Handle lists
|
|
63
|
+
if val.include?(',')
|
|
64
|
+
parts = val.split(',').map { |m| month_abbr_to_num(m) }
|
|
65
|
+
return parts.join(',')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Handle steps
|
|
69
|
+
if val.include?('/')
|
|
70
|
+
parts = val.split('/')
|
|
71
|
+
base = parts[0] == '*' ? '*' : month_abbr_to_num(parts[0])
|
|
72
|
+
return "#{base}/#{parts[1]}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Single month abbreviation
|
|
76
|
+
month_abbr_to_num(val).to_s
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def format_list(items)
|
|
80
|
+
return items.first if items.length == 1
|
|
81
|
+
|
|
82
|
+
if items.length == 2
|
|
83
|
+
items.join(' and ')
|
|
84
|
+
else
|
|
85
|
+
last = items.pop
|
|
86
|
+
"#{items.join(', ')}, and #{last}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module AWSCronParser
|
|
6
|
+
module Describer
|
|
7
|
+
module FieldDescribers
|
|
8
|
+
# Describes time fields (second, minute, hour)
|
|
9
|
+
class TimeField < Base
|
|
10
|
+
attr_reader :field_type
|
|
11
|
+
|
|
12
|
+
def initialize(value, field_type, is_aws: false)
|
|
13
|
+
super(value, is_aws: is_aws)
|
|
14
|
+
@field_type = field_type # :second, :minute, or :hour
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def step_wildcard(step)
|
|
20
|
+
Template.interpolate("#{field_type}.step", step: step, step_plural: Template.plural_for(step))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def step_from(start_val, step)
|
|
24
|
+
context = { step: step, step_plural: Template.plural_for(step) }
|
|
25
|
+
|
|
26
|
+
if field_type == :hour
|
|
27
|
+
context[:start_hour] = format_hour_12(start_val)
|
|
28
|
+
Template.interpolate('hour.step_range', **context)
|
|
29
|
+
else
|
|
30
|
+
context[:start] = start_val
|
|
31
|
+
Template.interpolate("#{field_type}.step_range", **context)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def step_range(start_val, end_val, step)
|
|
36
|
+
context = { step: step, step_plural: Template.plural_for(step) }
|
|
37
|
+
|
|
38
|
+
if field_type == :hour
|
|
39
|
+
context[:start_hour] = format_hour_12(start_val)
|
|
40
|
+
context[:end_hour] = format_hour_12(end_val)
|
|
41
|
+
Template.interpolate('hour.step_between', **context)
|
|
42
|
+
else
|
|
43
|
+
context[:start] = start_val
|
|
44
|
+
context[:end] = end_val
|
|
45
|
+
Template.interpolate("#{field_type}.step_between", **context)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def list_template(items)
|
|
50
|
+
list_plural = items.length > 1 ? 's' : ''
|
|
51
|
+
Template.interpolate("#{field_type}.list", items: format_list(items), list_plural: list_plural)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def range_template(start_val, end_val)
|
|
55
|
+
if field_type == :hour
|
|
56
|
+
Template.interpolate('hour.range', start_hour: format_hour_12(start_val), end_hour: format_hour_12(end_val))
|
|
57
|
+
else
|
|
58
|
+
Template.interpolate("#{field_type}.range", start: start_val, end: end_val)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def single_template(val)
|
|
63
|
+
if field_type == :hour
|
|
64
|
+
format_hour_12(val)
|
|
65
|
+
else
|
|
66
|
+
plural = val == 1 ? '' : 's'
|
|
67
|
+
Template.interpolate("time.#{field_type}_single", value: val, value_plural: plural)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_value(val)
|
|
72
|
+
field_type == :hour ? format_hour_12(val) : val
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def format_hour_12(hour_24)
|
|
76
|
+
hour_24 = hour_24.to_i % 24
|
|
77
|
+
period = hour_24 < 12 ? 'AM' : 'PM'
|
|
78
|
+
hour_12 = if hour_24.zero?
|
|
79
|
+
12
|
|
80
|
+
elsif hour_24 > 12
|
|
81
|
+
hour_24 - 12
|
|
82
|
+
else
|
|
83
|
+
hour_24
|
|
84
|
+
end
|
|
85
|
+
"#{hour_12} #{period}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def format_list(items)
|
|
89
|
+
return items.first if items.length == 1
|
|
90
|
+
|
|
91
|
+
if items.length == 2
|
|
92
|
+
items.join(' and ')
|
|
93
|
+
else
|
|
94
|
+
last = items.pop
|
|
95
|
+
"#{items.join(', ')}, and #{last}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AWSCronParser
|
|
4
|
+
module Describer
|
|
5
|
+
# Composes month descriptions
|
|
6
|
+
class MonthComposer
|
|
7
|
+
include AWSCronParser::Helpers
|
|
8
|
+
|
|
9
|
+
attr_reader :month, :is_aws
|
|
10
|
+
|
|
11
|
+
def initialize(config, is_aws: false)
|
|
12
|
+
@month = config[:month]
|
|
13
|
+
@is_aws = is_aws
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def compose
|
|
17
|
+
return nil if wildcards?(month)
|
|
18
|
+
|
|
19
|
+
FieldDescribers::Month.new(month, is_aws: is_aws).describe
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def wildcards?(*fields)
|
|
25
|
+
fields.all? { |f| f.nil? || f == '*' || f == '?' }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# CRON Expression Description Templates
|
|
2
|
+
# Usage: Templates can use variables like {variable_name} for interpolation
|
|
3
|
+
# Example: "Every {step} minutes" becomes "Every 5 minutes" when step=5
|
|
4
|
+
|
|
5
|
+
rate:
|
|
6
|
+
singular: "Every {unit}"
|
|
7
|
+
plural: "Every {value} {unit}s"
|
|
8
|
+
|
|
9
|
+
macro:
|
|
10
|
+
yearly: "Every year"
|
|
11
|
+
monthly: "Every month"
|
|
12
|
+
weekly: "Every week"
|
|
13
|
+
daily: "Every day"
|
|
14
|
+
hourly: "Every hour"
|
|
15
|
+
|
|
16
|
+
time:
|
|
17
|
+
at: "At {time}"
|
|
18
|
+
every_minute: "Every minute"
|
|
19
|
+
every_second: "Every second"
|
|
20
|
+
every_hour: "Every hour"
|
|
21
|
+
every_n_minutes: "Every {step} minute{step_plural}"
|
|
22
|
+
every_n_hours: "Every {step} hour{step_plural}"
|
|
23
|
+
every_n_seconds: "Every {step} second{step_plural}"
|
|
24
|
+
|
|
25
|
+
# Specific times
|
|
26
|
+
at_specific_time: "At {hour}:{minute} {period}"
|
|
27
|
+
at_specific_time_with_seconds: "At {hour}:{minute}:{second} {period}"
|
|
28
|
+
every_minute_at_second: "Every minute at second {second}"
|
|
29
|
+
every_hour_at_minute: "Every hour at minute {minute}"
|
|
30
|
+
every_n_minutes_starting_at: "Every {step} minute{step_plural} starting at {time}"
|
|
31
|
+
|
|
32
|
+
# Range patterns
|
|
33
|
+
minute_range: "from minute {start} to minute {end}"
|
|
34
|
+
hour_range: "from {start_hour} to {end_hour}"
|
|
35
|
+
second_range: "from second {start} to second {end}"
|
|
36
|
+
|
|
37
|
+
# Step patterns
|
|
38
|
+
minute_step_range: "every {step} minute{step_plural} starting at minute {start}"
|
|
39
|
+
hour_step_range: "every {step} hour{step_plural} starting at {start_hour}"
|
|
40
|
+
second_step_range: "every {step} second{step_plural} starting at second {start}"
|
|
41
|
+
|
|
42
|
+
# Single minute/hour/second
|
|
43
|
+
minute_single: "{value} minute{value_plural}"
|
|
44
|
+
hour_single: "{value} hour{value_plural}"
|
|
45
|
+
second_single: "{value} second{value_plural}"
|
|
46
|
+
|
|
47
|
+
# Lists
|
|
48
|
+
minute_list: "at minute{plural} {items}"
|
|
49
|
+
hour_list: "at {items}"
|
|
50
|
+
second_list: "at second{plural} {items}"
|
|
51
|
+
|
|
52
|
+
minute:
|
|
53
|
+
step: "Every {step} minute{step_plural}"
|
|
54
|
+
step_range: "Every {step} minute{step_plural} starting at minute {start}"
|
|
55
|
+
range: "Every hour from minute {start} to minute {end}"
|
|
56
|
+
list: "Every hour at minute{list_plural} {items}"
|
|
57
|
+
|
|
58
|
+
second:
|
|
59
|
+
step: "Every {step} second{step_plural}"
|
|
60
|
+
step_range: "Every {step} second{step_plural} starting at second {start}"
|
|
61
|
+
range: "Every minute from second {start} to second {end}"
|
|
62
|
+
list: "Every minute at second{list_plural} {items}"
|
|
63
|
+
|
|
64
|
+
hour:
|
|
65
|
+
step: "Every {step} hour{step_plural}"
|
|
66
|
+
step_range: "Every {step} hour{step_plural} starting at {start_hour}"
|
|
67
|
+
step_between: "Every {step} hour{step_plural} from {start_hour} to {end_hour}"
|
|
68
|
+
range: "Every day from {start_hour} to {end_hour}"
|
|
69
|
+
list: "at {items}"
|
|
70
|
+
|
|
71
|
+
day_of_month:
|
|
72
|
+
single: "on the {ordinal}"
|
|
73
|
+
range: "on the {start_ordinal} through {end_ordinal}"
|
|
74
|
+
step: "Every {step} day{step_plural}"
|
|
75
|
+
step_range: "Every {step} day{step_plural} starting on the {start_ordinal}"
|
|
76
|
+
list: "on the {items}"
|
|
77
|
+
last_day: "on the last day of the month"
|
|
78
|
+
closest_weekday: "on the closest weekday to the {ordinal}"
|
|
79
|
+
|
|
80
|
+
day_of_week:
|
|
81
|
+
single: "on {day_name}"
|
|
82
|
+
range: "{start_day} through {end_day}"
|
|
83
|
+
list: "on {items}"
|
|
84
|
+
nth_occurrence: "on the {nth_word} {day_name}"
|
|
85
|
+
last_occurrence: "on the last {day_name}"
|
|
86
|
+
step: "Every {step} day{step_plural} of the week"
|
|
87
|
+
step_from: "Every {step} day{step_plural} of the week starting on {start_day}"
|
|
88
|
+
|
|
89
|
+
month:
|
|
90
|
+
single: "in {month_name}"
|
|
91
|
+
range: "in {start_month} through {end_month}"
|
|
92
|
+
step_range: "Every {step} month{step_plural} starting in {start_month}"
|
|
93
|
+
list: "in {items}"
|
|
94
|
+
step: "Every {step} month{step_plural} starting in {start_month}"
|
|
95
|
+
|
|
96
|
+
# Complex compositions with multiple conditions
|
|
97
|
+
cron:
|
|
98
|
+
# Wildcard combinations
|
|
99
|
+
all_wildcards: "Every second"
|
|
100
|
+
time_only_wildcards: "Every minute"
|
|
101
|
+
|
|
102
|
+
# Day/DOW combinations
|
|
103
|
+
day_dow_both_specified: "At {time_part} on the {day_part} ({dow_part})"
|
|
104
|
+
day_dow_both_specified_month: "At {time_part} on the {day_part} ({dow_part}) in {month_desc}"
|
|
105
|
+
day_dow_time_day: "At {time_part} on the {day_part}"
|
|
106
|
+
day_dow_time_dow: "At {time_part} on {dow_part}"
|
|
107
|
+
day_dow_time_month: "At {time_part} in {month_desc}"
|
|
108
|
+
day_dow_day_dow: "On the {day_part} ({dow_part})"
|
|
109
|
+
day_dow_day_month: "On {month_desc} {day_part}"
|
|
110
|
+
day_dow_dow_month: "On {dow_part} in {month_desc}"
|
|
111
|
+
|
|
112
|
+
# Special day combinations
|
|
113
|
+
day_special_leap_year: "On February 29th at {time_clean} (leap years only)"
|
|
114
|
+
day_special_new_year: "On January 1st at {time_clean}"
|
|
115
|
+
|
|
116
|
+
# Time range patterns
|
|
117
|
+
time_range_second: "Every minute {time_desc}"
|
|
118
|
+
time_range_minute: "Every hour {time_desc}"
|
|
119
|
+
time_range_default: "Every day {time_desc}"
|
|
120
|
+
|
|
121
|
+
helpers:
|
|
122
|
+
# Ordinal word helpers
|
|
123
|
+
ordinal_words:
|
|
124
|
+
1: "first"
|
|
125
|
+
2: "second"
|
|
126
|
+
3: "third"
|
|
127
|
+
4: "fourth"
|
|
128
|
+
5: "fifth"
|
|
129
|
+
|
|
130
|
+
# Pluralization
|
|
131
|
+
plural_suffix: "s"
|
|
132
|
+
no_plural: ""
|
|
133
|
+
|
|
134
|
+
# Composition templates for complex patterns
|
|
135
|
+
composition:
|
|
136
|
+
# Time + Day patterns
|
|
137
|
+
every_day: "Every day"
|
|
138
|
+
every_day_in_month: "Every day in {month_desc}"
|
|
139
|
+
every_day_at: "Every day at {time_clean}"
|
|
140
|
+
every_day_in_month_at_time: "Every day in {month_desc} at {time_clean}"
|
|
141
|
+
every_day_time_in_month: "Every day {time_desc} in {month_desc}"
|
|
142
|
+
|
|
143
|
+
# Specific day patterns
|
|
144
|
+
on_day_at_time: "On {day_plain}{month_suffix} at {time_clean}"
|
|
145
|
+
every_month_on_day: "On {day_desc} every month"
|
|
146
|
+
every_month_on_day_at_time: "On {day_plain} in {month_name} at {time_clean}"
|
|
147
|
+
every_year_on_month_day: "On {month_name} {day_plain} every year"
|
|
148
|
+
every_month_day_time: "{time_desc} on {month_name} {day_plain}"
|
|
149
|
+
time_on_day_of_every_month: "{time_desc} on {day_label} of every month"
|
|
150
|
+
|
|
151
|
+
# Last day patterns
|
|
152
|
+
last_day_of_month_at_time: "On the last day of {month_name} at {time_text}"
|
|
153
|
+
|
|
154
|
+
# Weekday patterns
|
|
155
|
+
nth_weekday: "On the {nth_word} {day_name}"
|
|
156
|
+
nth_weekday_at_time: "On the {nth_word} {day_name} at {time_clean}"
|
|
157
|
+
nth_weekday_in_month: "The {nth_word} {day_name} of {month_name} at {time_clean}"
|
|
158
|
+
nth_weekday_in_month_no_time: "On the {nth_word} {day_name} in {month_name}"
|
|
159
|
+
time_on_nth_weekday: "{time_desc} on the {nth_word} {day_name}"
|
|
160
|
+
time_on_nth_weekday_in_month: "{time_desc} on the {nth_word} {day_name} {month_desc}"
|
|
161
|
+
|
|
162
|
+
# Last weekday patterns
|
|
163
|
+
last_weekday: "On the last {day_name}"
|
|
164
|
+
last_weekday_at_time: "On the last {day_name} at {time_clean}"
|
|
165
|
+
last_weekday_in_month: "On the last {day_name} in {month_name}"
|
|
166
|
+
last_weekday_in_month_no_time: "On the last {day_name} in {month_name}"
|
|
167
|
+
|
|
168
|
+
# Every X minutes/hours patterns
|
|
169
|
+
every_minute_at_second: "Every minute at second {second}"
|
|
170
|
+
every_hour_at_minute: "Every hour at minute {minute}"
|
|
171
|
+
every_n_minutes_at: "Every {step} minute{step_plural} at {time_clean}"
|
|
172
|
+
every_n_hours_from: "Every {step} hour{step_plural} from {start_time} to {end_time}"
|
|
173
|
+
|
|
174
|
+
# Prepositions and connectors
|
|
175
|
+
prepositions:
|
|
176
|
+
at: "at"
|
|
177
|
+
on: "on"
|
|
178
|
+
in: "in"
|
|
179
|
+
from: "from"
|
|
180
|
+
to: "to"
|
|
181
|
+
through: "through"
|
|
182
|
+
and: "and"
|
|
183
|
+
or: "or"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module AWSCronParser
|
|
6
|
+
module Describer
|
|
7
|
+
# Simple template interpolator for CRON descriptions
|
|
8
|
+
class Template
|
|
9
|
+
TEMPLATES = YAML.load_file(File.join(__dir__, 'template.en.yml')).freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Main interpolation method with auto-pluralization
|
|
13
|
+
# Example: interpolate("time.every_n_minutes", step: 5)
|
|
14
|
+
# Returns: "Every 5 minutes"
|
|
15
|
+
def interpolate(template_path, **variables)
|
|
16
|
+
template = get(template_path)
|
|
17
|
+
return nil if template.nil?
|
|
18
|
+
|
|
19
|
+
render(template, auto_pluralize(template, variables))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get template by path (dot notation)
|
|
23
|
+
# Example: get("time.every_n_minutes") => "Every {step} minute{step_plural}"
|
|
24
|
+
def get(path)
|
|
25
|
+
path.to_s.split('.').reduce(TEMPLATES) { |hash, key| hash&.[](key) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Render template string with variables (faster, direct replacement)
|
|
29
|
+
# Example: render("Every {step} minutes", step: 5) => "Every 5 minutes"
|
|
30
|
+
def render(template, variables = {})
|
|
31
|
+
template.to_s.gsub(/\{(\w+)\}/) do
|
|
32
|
+
variables[::Regexp.last_match(1).to_sym] || variables[::Regexp.last_match(1)] ||
|
|
33
|
+
"{#{::Regexp.last_match(1)}}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Simple plural helper
|
|
38
|
+
def plural_for(count)
|
|
39
|
+
count == 1 ? '' : 's'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get ordinal word (first, second, third, etc.)
|
|
43
|
+
def ordinal_word(number)
|
|
44
|
+
ORDINAL_WORDS[number] || "#{number}th"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get all templates (for debugging)
|
|
48
|
+
def all
|
|
49
|
+
TEMPLATES
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Auto-calculate plural variables based on template and values
|
|
55
|
+
def auto_pluralize(template, variables)
|
|
56
|
+
processed = variables.dup
|
|
57
|
+
|
|
58
|
+
template.scan(/\{(\w+)\}/).flatten.uniq.each do |var_name|
|
|
59
|
+
next if processed.key?(var_name.to_sym) || processed.key?(var_name)
|
|
60
|
+
|
|
61
|
+
# Auto-pluralize: {step_plural} checks {step} value
|
|
62
|
+
if var_name.end_with?('_plural')
|
|
63
|
+
base_var = var_name.sub('_plural', '').to_sym
|
|
64
|
+
base_value = processed[base_var]
|
|
65
|
+
processed[var_name.to_sym] = plural_for(base_value) if base_value
|
|
66
|
+
# Generic plural: checks items count
|
|
67
|
+
elsif var_name == 'plural' && processed.key?(:items)
|
|
68
|
+
items_count = processed[:items].to_s.split(',').length
|
|
69
|
+
processed[:plural] = plural_for(items_count)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
processed
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Ordinal words cache
|
|
77
|
+
ORDINAL_WORDS = {
|
|
78
|
+
1 => 'first',
|
|
79
|
+
2 => 'second',
|
|
80
|
+
3 => 'third',
|
|
81
|
+
4 => 'fourth',
|
|
82
|
+
5 => 'fifth'
|
|
83
|
+
}.freeze
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|