mooncron

    A cron expression parser and schedule calculator written in MoonBit.

    cron
    schedule
    parser
    time
    calendar
    Download zip
    Author
    Version
    0.1.4
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    22

    Dependencies

    #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.