Parse, validate and describe the scheduling expressions accepted by AWS EventBridge and Systems Manager, plus Quartz and standard 5-field CRON.
- AWS cron —
cron(0 9 ? * MON-FRI *), 6 fields,?/L/W/#, weekdays1=SUN..7=SAT - AWS rate —
rate(5 minutes),rate(1 day) - AWS at —
at(2025-06-15T10:30:00), a one-time schedule - Standard / Quartz cron —
0 9 * * 1-5,*/10 * * * *, 6-field with seconds - Macros —
@yearly,@monthly,@weekly,@daily,@hourly
No Rails required. The gem autoloads through its own Zeitwerk loader.
gem 'aws-cron-parser'require 'aws_cron_parser'parser = AWSCronParser.parse('cron(0 9 ? * MON-FRI *)')
parser.describe # => "Every Monday through Friday at 9 AM"
parser.expression # => "cron(0 9 ? * MON-FRI *)"
parser.source # => "0 9 ? * MON-FRI"now = Time.utc(2025, 1, 15, 10, 30)
parser = AWSCronParser.parse('0 9 * * 1-5')
parser.next(now) # => 2025-01-16 09:00:00 UTC
parser.last(now) # => 2025-01-15 09:00:00 UTC
parser.next_runs(3, now) # => [2025-01-16 09:00, 2025-01-17 09:00, 2025-01-20 09:00]next_unique_dates(count, from) and next_unique_hours(count, from) collapse repeated
executions down to one entry per day or per hour — useful when rendering a schedule preview.
last is only available for standard/Quartz expressions; AWS cron(...), rate(...)
and at(...) raise ArgumentError, since they are anchored forwards in time.
at(...) fires exactly once. next returns the instant while it is still ahead and
nil once it has passed, so next_runs yields at most one entry:
parser = AWSCronParser.parse('at(2025-06-15T10:30:00)')
parser.next(Time.utc(2025, 1, 1)) # => 2025-06-15 10:30:00 UTC
parser.next(Time.utc(2026, 1, 1)) # => nil
parser.next_runs(5, Time.utc(2025, 1, 1)).size # => 1Inside cron(...) weekdays follow AWS: 1 = Sunday through 7 = Saturday. Bare
5-field expressions follow standard CRON, where 0 and 7 are Sunday and 1 is Monday.
cron(0 9 ? * 1 *) and 0 9 * * 1 are therefore different schedules.
AWSCronParser::Validator.validate('0 9 * * 1-5') # => true
AWSCronParser.parse('cron(99 9 ? * MON *)') # raises ArgumentError: minute 99 out of range
parser = AWSCronParser.parse('0 9 * * 1-5')
parser.valid? # => true
parser.errors # => []AWSCronParser.parse validates eagerly and raises ArgumentError with a field-level
message. Use valid? / errors when you want to report the problem instead of raising.
AWSCronParser::Describer.describe('cron(0/15 * * * ? *)') # => "Every 15 minutes"
AWSCronParser::Describer.describe('cron(0 0 L * ? *)') # => "On the last day of the month at 12 AM"
AWSCronParser::Describer.describe('cron(0 12 ? * 6#3 *)') # => "On the third Saturday at 12 PM"
AWSCronParser::Describer.describe('rate(5 minutes)') # => "Every 5 minutes"Wording lives in lib/aws_cron_parser/describer/template.en.yml.
The parsers build zone-aware times through ActiveSupport's Time.zone. Outside Rails the
gem defaults it to UTC if the host has not set one; inside Rails your config.time_zone
wins. You can also pass an explicit time source:
AWSCronParser.parse('0 9 * * 1-5', Time.find_zone('America/Sao_Paulo'))bundle install
bundle exec rspecMIT