Skip to content

Cron ​

wvlet.uni.text.cron parses cron expressions, computes when a schedule fires next, and describes it in English. It works on JVM, Scala.js, and Scala Native.

Next Run Times ​

scala
import wvlet.uni.text.cron.{Cron, UtcOffset}
import java.time.Instant

val cron = Cron.parse("30 9 * * MON-FRI")

cron.next(Instant.parse("2026-10-30T12:00:00Z"))     // 2026-11-02T09:30:00Z (Monday)
cron.nextN(Instant.parse("2026-10-30T12:00:00Z"), 3) // Mon, Tue, and Wed at 09:30
cron.matches(Instant.parse("2026-11-02T09:30:00Z"))  // true

next returns the first time strictly after the given instant. Fractions of a second are ignored.

Describing a Schedule ​

describe renders the schedule in English, in the style of cronstrue, with 24-hour times:

scala
Cron.parse("30 9 * * 1-5").describe   // At 09:30, Monday through Friday
Cron.parse("*/15 * * * *").describe   // Every 15 minutes
Cron.parse("*/5 9-17 * * *").describe // Every 5 minutes, between 09:00 and 17:59
Cron.parse("@yearly").describe        // At 00:00, on day 1 of the month, only in January

toString returns the expression exactly as given, so a parsed Cron can be stored and shown back unchanged:

scala
Cron.parse("30 9 * * mon-fri").toString // 30 9 * * mon-fri

Syntax ​

An expression has five fields, or six with a leading seconds field:

FieldValuesNames
second (optional)0-59
minute0-59
hour0-23
day of month1-31
month1-12JAN-DEC
day of week0-7 (0 and 7 are Sunday)SUN-SAT

Each field is a comma-separated list of items:

ItemMeaning
*every value
?same as * (day of month and day of week only)
5a single value
1-5a range
*/15every 15th value from the first
1-30/5every 5th value in a range
10/20every 20th value from 10 to the field's maximum

Names are case-insensitive and also work in ranges (MON-FRI). The macros @yearly, @annually, @monthly, @weekly, @daily, @midnight, and @hourly stand for their usual five-field schedules.

When both day of month and day of week are restricted, a day matches if either one matches, as in Vixie cron and most Unix crontabs: 0 0 13 * FRI runs on the 13th and on every Friday. A field that starts with * or ? counts as unrestricted, so 0 0 */2 * FRI runs on Fridays that fall on odd days.

Quartz extensions (L, W, #, a year field) are not supported.

Time Zones ​

Fields are read in UTC by default. Pass a fixed offset to read them in local time:

scala
val tokyo = UtcOffset.ofHours(9) // or UtcOffset.parse("+09:00")
Cron.parse("0 9 * * *").next(Instant.parse("2026-10-28T01:00:00Z"), tokyo)
// 2026-10-29T00:00:00Z, which is 09:00 in +09:00

Named time zones such as Asia/Tokyo need java.time.ZoneId, which Scala.js doesn't provide, so next takes only a fixed offset. To follow daylight saving time, resolve the zone's offset yourself (on the JVM, ZoneId.of(name).getRules.getOffset(instant)) and pass it in. For full time-zone support on the JVM, cron-utils is a good choice and also covers Quartz and Spring cron dialects.

Errors ​

Invalid expressions throw TextParseException with the column and a caret under the problem:

text
unknown day-of-week name 'FOO': expected one of SUN, MON, TUE, WED, THU, FRI, SAT (line 1, column 13)
* * * * MON-FOO
            ^

A day of month that never occurs in the selected months, such as 0 0 30 2 *, is rejected at parse time, so next always finds a time.

Released under the Apache 2.0 License.