Project

cronify

0.0
No release in over 3 years
Cronify parses human-friendly schedule descriptions (e.g. 'every weekday at 9am', 'first Monday of each month') into standard cron strings and next-occurrence timestamps. Designed for SaaS apps where users configure their own recurring schedules, with output compatible with Sidekiq, Whenever, and similar tools.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 1.11
~> 2.0
 Project Readme

Cronify

Parse human-friendly schedule descriptions into cron expressions and next-occurrence timestamps.

Cronify.parse("every weekday at 9am")
# => #<Cronify::Schedule cron="0 9 * * 1-5" ...>

Cronify.parse("first Monday of each month at noon")
# => #<Cronify::Schedule cron="0 12 ? * 1#1" ...>

Cronify.parse("every 2 hours between 8am and 6pm")
# => #<Cronify::Schedule cron="0 8,10,12,14,16,18 * * *" ...>

Cronify fills the gap between raw cron syntax and heavy scheduling libraries. It outputs cron strings compatible with Sidekiq, Whenever, and similar tools, plus the next N fire times as Ruby Time objects.

Installation

bundle add cronify

Or add to your Gemfile manually:

gem "cronify"

Usage

Parsing a schedule

schedule = Cronify.parse("every weekday at 9am")

schedule.cron                     # => "0 9 * * 1-5"
schedule.next_occurrence          # => 2026-03-16 09:00:00 UTC
schedule.next_occurrences(n: 3)   # => [2026-03-16 09:00:00 UTC, 2026-03-17 09:00:00 UTC, 2026-03-18 09:00:00 UTC]
schedule.original_input           # => "every weekday at 9am"
schedule.timezone                 # => "UTC"

Timezones

Pass any valid TZInfo identifier via the timezone: keyword:

schedule = Cronify.parse("every weekday at 9am", timezone: "Europe/Athens")
schedule.next_occurrence # => time expressed in Europe/Athens

An invalid timezone raises Cronify::Error immediately with a descriptive message.

Supported patterns

Input Cron output
"every N hours" 0 */N * * *
"every N minutes" */N * * * *
"every day at <time>" 0 H * * *
"every weekday at <time>" 0 H * * 1-5
"every weekend at <time>" 0 H * * 6,0
"first/second/third/fourth/last <weekday> of each month at <time>" 0 H ? * W#N / 0 H ? * WL
"every N hours between <time> and <time>" 0 H1,H2,... * * *

Accepted time formats: 9am, 5pm, 9:30am, noon, midnight

Cron dialect

Cronify targets Quartz/Sidekiq cron syntax. This means:

  • Ordinal weekday patterns use #N notation: 1#1 = first Monday, 5#3 = third Friday
  • Last weekday of month uses L suffix: 5L = last Friday
  • The ? wildcard is used in the day-of-month field when day-of-week is specified

This syntax is supported by Sidekiq Pro, sidekiq-cron, and Whenever. It is not compatible with standard POSIX cron.

Error handling

# Unrecognized input
Cronify.parse("whenever I feel like it")
# => raises Cronify::ParseError: Unrecognized schedule: 'whenever I feel like it'

# Invalid timezone
Cronify.parse("every day at 9am", timezone: "Not/Real")
# => raises Cronify::Error: Unknown timezone: 'Not/Real'. Use a valid TZInfo identifier...

Exception hierarchy

StandardError
└── Cronify::Error
    ├── Cronify::ParseError           # input string not recognized
    └── Cronify::AmbiguousInputError  # input matches multiple patterns

Development

bin/setup            # install dependencies
bundle exec rspec    # run tests
bundle exec rubocop  # lint
bin/console          # interactive prompt

Type signatures

Cronify ships with RBS type signatures in the sig/ directory. To use them with your project's type checker:

bundle exec rbs collection install

Signatures cover the full public API (Cronify.parse, Schedule, and all error classes) and are validated in CI.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/achristopoulos/cronify.

License

MIT — see LICENSE.txt.