aws-cron-parser

CI Gem Version

Parse, validate and describe the scheduling expressions accepted by AWS EventBridge and Systems Manager, plus Quartz and standard 5-field CRON.

  • AWS croncron(0 9 ? * MON-FRI *), 6 fields, ? / L / W / #, weekdays 1=SUN..7=SAT
  • AWS raterate(5 minutes), rate(1 day)
  • AWS atat(2025-06-15T10:30:00), a one-time schedule
  • Standard / Quartz cron0 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