mooncron

A cron expression parser and schedule calculator written in MoonBit.

cron
schedule
parser
time
calendar
moon add 001-Elsa/mooncron@0.1.4
Download zip
Author
Version
0.1.4
License
Apache-2.0
Last updated
16 days ago
Downloads
17

Dependencies

README

#MoonCron

A cron expression parser and schedule calculator written in MoonBit.

MoonCron parses, validates, describes, matches, builds, normalizes, and analyzes cron expressions. It also calculates previous and future matching times. It is a cron rule library and CLI; it does not execute or schedule tasks.

#Features

  • Standard 5-field expressions: minute hour day-of-month month day-of-week
  • Extended 6-field expressions with seconds
  • Values, lists, ranges, steps, and range-with-step syntax
  • Seven aliases: @hourly, @daily, @midnight, @weekly, @monthly, @yearly, and @annually
  • Detailed validation errors
  • Date/time matching with cron day-of-month OR day-of-week semantics
  • Previous, next, bounded, and range-based execution-time queries
  • English and Chinese descriptions
  • Fluent CronBuilder and quick constructors
  • Normalization, equivalence checks, field set operations, and frequency analysis
  • CLI commands for validation, description, calculation, and matching
  • 151 tests across Wasm, Wasm-GC, JavaScript, and Native

#Supported Syntax

SyntaxExampleMeaning
Wildcard*Any valid value
Value5One specific value
List1,3,5Any listed value
Range1-5Inclusive range
Step*/10Every 10 values
Range + step10-50/5Every 5 values from 10 through 50

Field layouts:

# Standard minute hour day-of-month month day-of-week # Extended second minute hour day-of-month month day-of-week

See docs/syntax.md for the complete syntax guide.

#Installation

Add MoonCron to moon.mod:

import {
"001-Elsa/mooncron@0.1.4"
}

Import it in moon.pkg:

import {
"001-Elsa/mooncron" @mooncron,
}

#Library Usage

let cron = @mooncron.CronExpr::parse("*/15 9-17 * * 1-5")

match cron {
Ok(expr) => {
let now = @mooncron.DateTime::new(2026, 7, 27, 9, 0)
println(expr.matches(now).to_string())
println(expr.describe_en())
println(expr.describe_cn())

for time in expr.next_n(now, 5) {
println(time.to_string())
}
}
Err(err) => println(@mooncron.cron_error_to_string(err))
}

#Common API

pub fn CronExpr::parse(String) -> Result[CronExpr, CronError]
pub fn CronExpr::matches(Self, DateTime) -> Bool
pub fn CronExpr::next_after(Self, DateTime) -> DateTime?
pub fn CronExpr::next_n(Self, DateTime, Int) -> Array[DateTime]
pub fn CronExpr::describe_en(Self) -> String
pub fn CronExpr::describe_cn(Self) -> String

pub fn DateTime::new(Int, Int, Int, Int, Int) -> DateTime
pub fn DateTime::new_with_second(Int, Int, Int, Int, Int, Int) -> DateTime
pub fn DateTime::to_string_with_seconds(Self) -> String
pub fn parse_datetime(String) -> DateTime?

pub fn between(Array[CronField], DateTime, DateTime) -> Array[DateTime]
pub fn count_between(Array[CronField], DateTime, DateTime) -> Int
pub fn prev_before(Array[CronField], DateTime) -> DateTime?
pub fn normalize_expression(String) -> Result[String, CronError]
pub fn are_equivalent(Array[CronField], Array[CronField]) -> Bool

The generated public interface is available in pkg.generated.mbti.

#CLI Usage

# Validate an expression moon run cmd/main -- check "*/15 9-18 * * 1-5" # Generate an English description moon run cmd/main -- explain "0 9 * * 1-5" # Generate a Chinese description moon run cmd/main -- describe "0 9 * * 1-5" --cn # Calculate future execution times moon run cmd/main -- next "0 9 * * 1-5" --count 5 \ --time "2026-07-25 12:00" # Match a specific time moon run cmd/main -- matches "0 9 * * 1-5" \ --time "2026-07-27 09:00"

#Validation Errors

MoonCron returns errors that identify the invalid field and constraint:

61 * * * * -> invalid minute value 61: expected 0..59 */0 * * * * -> step value cannot be 0 in field 'minute' 1--5 * * * * -> invalid range syntax in field 'minute' * * * * * * * -> expected 5 or 6 fields but got 7

#Verification

Run the same checks used by the submission workflow:

moon fmt --check moon check --target all --deny-warn moon test --target all --deny-warn moon info moon package

Expected test result for each formal backend:

Total tests: 151, passed: 151, failed: 0.

The all target covers Wasm, Wasm-GC, JavaScript, and Native.

#Project Structure

mooncron/ ├── cmd/main/ # CLI package ├── docs/ # Design and syntax documentation ├── *.mbt # Library source files ├── *_test.mbt # Tests ├── moon.mod # Module metadata ├── moon.pkg # Root package configuration ├── ACCEPTANCE.md # Acceptance checklist ├── MoonCron项目申报书.md # Competition application └── LICENSE

#Project Boundaries

MoonCron v0.1.4 intentionally does not provide:

  • Actual task execution or a background scheduler
  • Distributed scheduling or task persistence
  • A web UI or user system
  • Timezone or daylight-saving-time support
  • Quartz extensions (L, W, #)
  • A year field (7-field format)
  • Natural-language-to-cron conversion
  • AI integration

#Roadmap

  • Timezone and daylight-saving-time support
  • Quartz extensions (L, W, #)
  • Year field support
  • Cron expression visualization
  • Natural-language-to-cron conversion

#License

Apache-2.0

#
CronBuilder

pub struct CronBuilder {
seconds : String
minutes : String
hours : String
dom : String
month : String
dow : String
include_seconds : Bool
}

A builder for constructing cron expressions step by step.

#
CronBuilder::at_hours

fn CronBuilder::at_hours(self : CronBuilder, hrs : Array[Int]) -> CronBuilder

Set specific hours.

#
CronBuilder::at_minutes

fn CronBuilder::at_minutes(self : CronBuilder, mins : Array[Int]) -> CronBuilder

Set specific minutes (e.g., 0,15,30,45).

#
CronBuilder::at_second

fn CronBuilder::at_second(self : CronBuilder, n : Int) -> CronBuilder

Start building the seconds field with a specific value.

#
CronBuilder::between_hours

fn CronBuilder::between_hours(self : CronBuilder, start : Int, end : Int) -> CronBuilder

Set a range of hours.

#
CronBuilder::build

fn CronBuilder::build(self : CronBuilder) -> String

Build the cron expression string.

#
CronBuilder::build_expr

fn CronBuilder::build_expr(self : CronBuilder) -> Result[CronExpr, CronError]

Build and parse into a CronExpr.

#
CronBuilder::dom

fn CronBuilder::dom(self : CronBuilder, expr : String) -> CronBuilder

Set the day-of-month field directly.

#
CronBuilder::dow

fn CronBuilder::dow(self : CronBuilder, expr : String) -> CronBuilder

Set the day-of-week field directly.

#
CronBuilder::every

fn CronBuilder::every(self : CronBuilder, n : Int) -> CronFieldBuilder

Start building the seconds field.

#
CronBuilder::every_minute

fn CronBuilder::every_minute(self : CronBuilder) -> CronBuilder

Set every minute.

#
CronBuilder::every_n_minutes

fn CronBuilder::every_n_minutes(self : CronBuilder, n : Int) -> CronBuilder

Set minutes to run every N minutes.

#
CronBuilder::hours

fn CronBuilder::hours(self : CronBuilder, expr : String) -> CronBuilder

Set the hours field directly.

#
CronBuilder::in_months

fn CronBuilder::in_months(self : CronBuilder, months : Array[Int]) -> CronBuilder

Set specific months.

#
CronBuilder::minutes

fn CronBuilder::minutes(self : CronBuilder, expr : String) -> CronBuilder

Set the minutes field directly.

#
CronBuilder::month

fn CronBuilder::month(self : CronBuilder, expr : String) -> CronBuilder

Set the month field directly.

#
CronBuilder::new

Create a new CronBuilder with default values (all wildcards).

#
CronBuilder::on_days_of_month

fn CronBuilder::on_days_of_month(self : CronBuilder, days : Array[Int]) -> CronBuilder

Set specific days of month.

#
CronBuilder::on_days_of_week

fn CronBuilder::on_days_of_week(self : CronBuilder, days : Array[Int]) -> CronBuilder

Set specific days of week.

#
CronBuilder::on_weekdays

fn CronBuilder::on_weekdays(self : CronBuilder) -> CronBuilder

Set the expression to run on weekdays only (Mon-Fri).

#
CronBuilder::on_weekends

fn CronBuilder::on_weekends(self : CronBuilder) -> CronBuilder

Set the expression to run on weekends only.

#
CronBuilder::seconds

fn CronBuilder::seconds(self : CronBuilder, expr : String) -> CronBuilder

Set the seconds field directly.

#
CronError

pub struct CronError {
kind : CronErrorKind
expression : String
}

A parsed cron error with context.

#
CronErrorKind

pub enum CronErrorKind {
EmptyExpression
WrongFieldCount(Int)
InvalidFieldValue(String)
FieldValueOutOfRange(String, Int, Int, Int)
StepCannotBeZero(String)
InvalidRange(String)
InvalidSyntax(String)
InvalidCharacter(String)
}

Types of validation errors that can occur in cron expressions.

#
CronExpr

pub struct CronExpr {
fields : Array[CronField]
expression : String
}

A parsed and validated cron expression.

#
CronExpr::describe_cn

fn CronExpr::describe_cn(self : CronExpr) -> String

Get a Chinese description of this cron expression.

#
CronExpr::describe_en

fn CronExpr::describe_en(self : CronExpr) -> String

Get an English description of this cron expression.

#
CronExpr::is_valid

fn CronExpr::is_valid(self : CronExpr) -> Bool

Check if the expression is syntactically valid.

#
CronExpr::matches

fn CronExpr::matches(self : CronExpr, dt : DateTime) -> Bool

Check if a DateTime matches this cron expression.

#
CronExpr::next_after

fn CronExpr::next_after(self : CronExpr, after : DateTime) -> DateTime?

Get the next execution time after the given reference time.

#
CronExpr::next_n

fn CronExpr::next_n(self : CronExpr, after : DateTime, n : Int) -> Array[DateTime]

Get the next N execution times after the given reference time.

#
CronExpr::parse

fn CronExpr::parse(raw : String) -> Result[CronExpr, CronError]

Parse a cron expression string. Returns a CronExpr or an error with details.

#
CronExpr::raw

fn CronExpr::raw(self : CronExpr) -> String

Get the original expression string.

#
CronExpr::to_string

fn CronExpr::to_string(self : CronExpr) -> String

Convert the expression to a human-readable string.

#
CronField

pub struct CronField {
field_type : FieldType
values : Array[Int]
raw : String
}

A parsed and expanded cron field.

#
CronFieldBuilder

pub struct CronFieldBuilder {
builder : CronBuilder
value : String
}

A field-level builder returned by every(n) to chain .minutes(), .hours(), etc.

#
CronFieldBuilder::between

fn CronFieldBuilder::between(self : CronFieldBuilder, start : Int, end : Int) -> CronRangeBuilder

Set range for hours between start and end.

#
CronFieldBuilder::days_of_month

fn CronFieldBuilder::days_of_month(self : CronFieldBuilder) -> CronBuilder

Apply to the day-of-month field.

#
CronFieldBuilder::hours

Apply to the hours field.

#
CronFieldBuilder::minutes

Apply to the minutes field.

#
CronFieldBuilder::months

Apply to the month field.

#
CronFieldBuilder::seconds

Apply to the seconds field.

#
CronRangeBuilder

pub struct CronRangeBuilder {
builder : CronBuilder
step : String
start : Int
end : Int
}

A range builder for constructing "between X and Y" clauses.

#
CronRangeBuilder::hours

Apply range to hours with the step.

#
DateTime

pub struct DateTime {
year : Int
month : Int
day : Int
hour : Int
minute : Int
second : Int
}

A simple DateTime type for cron calculations. The five-argument constructor defaults seconds to zero.

#
DateTime::eq

fn DateTime::eq(self : DateTime, other : DateTime) -> Bool

Compare two DateTime values for equality.

#
DateTime::ge

fn DateTime::ge(self : DateTime, other : DateTime) -> Bool

Check if self is after or equal to other.

#
DateTime::gt

fn DateTime::gt(self : DateTime, other : DateTime) -> Bool

Check if self is after other.

#
DateTime::le

fn DateTime::le(self : DateTime, other : DateTime) -> Bool

Check if self is before or equal to other.

#
DateTime::lt

fn DateTime::lt(self : DateTime, other : DateTime) -> Bool

Check if self is before other.

#
DateTime::new

fn DateTime::new(year : Int, month : Int, day : Int, hour : Int, minute : Int) -> DateTime

Create a new DateTime value.

#
DateTime::new_with_second

fn DateTime::new_with_second(year : Int, month : Int, day : Int, hour : Int, minute : Int, second : Int) -> DateTime

Create a DateTime value with explicit second precision.

#
DateTime::to_string

fn DateTime::to_string(self : DateTime) -> String

Format DateTime as "YYYY-MM-DD HH:MM", adding ":SS" when seconds are non-zero.

#
DateTime::to_string_with_seconds

fn DateTime::to_string_with_seconds(self : DateTime) -> String

Format DateTime with an explicit seconds component.

#
FieldType

pub enum FieldType {
Second
Minute
Hour
DayOfMonth
Month
DayOfWeek
}

Cron field types. Supports both 5-field (standard) and 6-field (with seconds) formats.

#
FrequencyAnalysis

pub struct FrequencyAnalysis {
per_minute : Int
per_hour : Int
per_day : Int
per_week : Int
per_month : Int
per_year : Int
max_gap_minutes : Int
avg_gap_minutes : Int
}

Result of a frequency analysis.

#
ScheduleDensity

pub enum ScheduleDensity {
VerySparse
Sparse
Moderate
Frequent
Dense
}

Get a schedule density as a category.

#
add_minute

fn add_minute(dt : DateTime) -> DateTime

Add one minute to a DateTime, handling all overflow cases including cross-day, cross-month, and cross-year.

#
add_second

fn add_second(dt : DateTime) -> DateTime

Add one second to a DateTime, handling all overflow cases.

#
alias_names

fn alias_names() -> Array[String]

Get the list of known alias names.

#
analyze_frequency

fn analyze_frequency(fields : Array[CronField]) -> FrequencyAnalysis

Analyze the frequency of a cron expression. Returns statistics about how often the expression fires.

#
are_equivalent

fn are_equivalent(a : Array[CronField], b : Array[CronField]) -> Bool

Check if two cron expressions are semantically equivalent. This compares the expanded field values rather than the raw strings.

#
between

fn between(fields : Array[CronField], start : DateTime, end : DateTime) -> Array[DateTime]

Get all execution times in a date range (inclusive).

#
classify_density

fn classify_density(analysis : FrequencyAnalysis) -> ScheduleDensity

Classify the schedule density.

#
compact_to_ranges

fn compact_to_ranges(values : Array[Int]) -> Array[String]

Compact a sorted array of values into range strings. e.g., [1,2,3,5,6,7,10] -> ["1-3","5-7","10"]

#
count_between

fn count_between(fields : Array[CronField], start : DateTime, end : DateTime) -> Int

Count how many times an expression fires in a date range.

#
cron_error_to_string

fn cron_error_to_string(err : CronError) -> String

Convert a CronError to a human-readable string.

#
daily_at

fn daily_at(hour : Int, minute : Int) -> String

Daily at a specific time.

#
day_of_week

fn day_of_week(year : Int, month : Int, day : Int) -> Int

Get the day of week for a given date. Returns 0=Sunday, 1=Monday, ..., 6=Saturday Uses Zeller's congruence.

#
days_in_month

fn days_in_month(year : Int, month : Int) -> Int

Return the number of days in a given month (1-indexed).

#
density_to_string

fn density_to_string(density : ScheduleDensity) -> String

Get a human-readable density label.

#
describe_cn

fn describe_cn(fields : Array[CronField]) -> String

Generate a Chinese description of a cron expression.

#
describe_en

fn describe_en(fields : Array[CronField]) -> String

Generate an English description of a cron expression.

#
every_n_hours

fn every_n_hours(n : Int) -> String

Every N hours at minute 0.

#
every_n_minutes

fn every_n_minutes(n : Int) -> String

Every N minutes.

#
field_cardinality

fn field_cardinality(field : CronField) -> Int

Count the number of distinct execution times per field.

#
field_effective_range

fn field_effective_range(field : CronField) -> (Int, Int)

Get the effective range (min..max) of a field's values.

#
field_intersection

fn field_intersection(a : CronField, b : CronField) -> Array[Int]

Find the intersection of two cron fields (values present in both).

#
field_key

fn field_key(field_type : FieldType) -> String

Get a short key name for a field type (used in error messages).

#
field_name

fn field_name(field_type : FieldType) -> String

Get the display name for a field type.

#
field_range

fn field_range(field_type : FieldType) -> (Int, Int)

Get the allowed value range for a field type.

#
field_union

fn field_union(a : CronField, b : CronField) -> Array[Int]

Find the union of two cron fields (all values from both).

#
frequency_to_string

fn frequency_to_string(analysis : FrequencyAnalysis) -> String

Format a FrequencyAnalysis as a human-readable string.

#
is_alias

fn is_alias(s : String) -> Bool

Check if a string is a known alias.

#
is_any_in_range

fn is_any_in_range(fields : Array[CronField], start : DateTime, end : DateTime) -> Bool

Check if there are any execution times in a date range.

#
is_five_field

fn is_five_field(fields : Array[CronField]) -> Bool

Check if a parsed expression uses the 5-field (standard) format.

#
is_impossible

fn is_impossible(fields : Array[CronField]) -> Bool

Check if a cron expression would ever fire.

#
is_leap_year

fn is_leap_year(year : Int) -> Bool

Check if a year is a leap year in the Gregorian calendar.

#
is_six_field

fn is_six_field(fields : Array[CronField]) -> Bool

Check if a parsed expression uses the 6-field (seconds) format.

#
is_valid_date

fn is_valid_date(year : Int, month : Int, day : Int) -> Bool

Validate that a date actually exists in the calendar.

#
is_valid_datetime

fn is_valid_datetime(dt : DateTime) -> Bool

Validate a full DateTime.

#
join_parts

fn join_parts(parts : Array[String]) -> String

Join parts with commas.

#
matches_day_constraint

fn matches_day_constraint(fields : Array[CronField], dt : DateTime) -> Bool

Check if any day-of-week or day-of-month would match. When both day-of-month and day-of-week are not restricted to all values, cron semantics say a match on EITHER is sufficient (OR logic).

#
matches_time

fn matches_time(fields : Array[CronField], dt : DateTime) -> Bool

Check if a DateTime matches all fields of a cron expression.

#
monthly_at

fn monthly_at(day : Int, hour : Int, minute : Int) -> String

Monthly on a specific day at a specific time.

#
next_after

fn next_after(fields : Array[CronField], after : DateTime) -> DateTime?

Find the next execution time after a given reference time. Returns None if no matching time exists within a reasonable search range.

#
next_n

fn next_n(fields : Array[CronField], after : DateTime, n : Int) -> Array[DateTime]

Find the next N execution times after a given reference time.

#
next_n_bounded

fn next_n_bounded(fields : Array[CronField], after : DateTime, n : Int, max_bound : DateTime) -> Array[DateTime]

Estimate the next N execution times with a maximum search bound. Returns fewer than N if not enough matches exist within the bound.

#
normalize_expression

fn normalize_expression(raw : String) -> Result[String, CronError]

Normalize a cron expression string to its canonical form. This simplifies the expression while preserving its meaning.

#
normalize_fields

fn normalize_fields(fields : Array[CronField]) -> String

Normalize an already-parsed field array into a canonical string.

#
parse_cron_expression

fn parse_cron_expression(raw : String) -> Result[Array[CronField], CronError]

Parse a full cron expression string into an array of CronField. Supports 5-field format, 6-field format (seconds first), and named aliases.

#
parse_datetime

fn parse_datetime(s : String) -> DateTime?

Parse a DateTime from "YYYY-MM-DD HH:MM" or "YYYY-MM-DD HH:MM:SS".

#
parse_field

fn parse_field(field_type : FieldType, raw : String) -> CronField?

Parse a single cron field expression into a CronField. Returns None if the field has invalid syntax or values.

#
prev_before

fn prev_before(fields : Array[CronField], before : DateTime) -> DateTime?

Find the previous execution time before a given reference time.

#
validate

fn validate(raw : String) -> Result[Unit, CronError]

Validate a cron expression string without constructing a CronExpr.

#
validate_expression

fn validate_expression(raw : String) -> Result[Unit, CronError]

Validate a raw cron expression string. Returns Ok(()) if valid, or an error with details.

#
weekdays_at

fn weekdays_at(hour : Int, minute : Int) -> String

Weekdays at a specific time.

#
weekly_at

fn weekly_at(day_of_week : Int, hour : Int, minute : Int) -> String

Weekly on a specific day at a specific time.

#
yearly_at

fn yearly_at(month : Int, day : Int, hour : Int, minute : Int) -> String

Yearly on a specific date and time.