aws-cron-parser
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.
Installation
gem 'aws-cron-parser'
require 'aws_cron_parser'
Usage
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"
Next and previous occurrences
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.
One-time schedules
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 # => 1
Weekday numbering
Inside 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.
Validation
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.
Descriptions
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.
Time zones
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'))
Development
bundle install
bundle exec rspec
License
MIT