agendexpr

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 4 Imported by: 0

README

agendexpr

A high-performance, zero-allocation Go library for lightweight cron expression parsing.

Go Reference License Go 1.26 Coverage


Table of Contents


Features

  • 6-field cron — seconds, minutes, hours, day-of-month, month, day-of-week
  • Bitmask storage — O(1) match checks on every field
  • Generic Field[T] — int-sized bitfields (uint8uint64) mapped to field range
  • Next() scheduling — find the next matching timestamp from any time.Time
  • Due() checking — test if a timestamp matches the expression (second to month)
  • Countdown() — seconds until the next match
  • Name normalizationJANDEC, SUNSAT in any case
  • Named aliases@hourly, @weekly, @monthly, @ticks, etc.
  • Zero heap allocs in parsing and matching hot paths
  • No external dependencies beyond stdlib (testify for tests only)

Installation

go get github.com/hanzjefferson/agendexpr

Quick Start

package main

import (
	"fmt"
	"time"

	"github.com/hanzjefferson/agendexpr"
)

func main() {
	// Every day at 08:30:00
	expr, _ := agendexpr.NewExpression("0 30 8 * * *")

	now := time.Now()
	next := expr.Next(now)
	fmt.Println("Next run:", next)

	due := expr.Due(next)
	fmt.Println("Is due:", due)

	countdown := expr.Countdown(now)
	fmt.Println("Seconds until next:", countdown)
}

Cron Format

Six space-separated fields:

┌───────── second (0–59)
│ ┌───────── minute (0–59)
│ │ ┌───────── hour   (0–23)
│ │ │ ┌───────── day-of-month (1–31)
│ │ │ │ ┌───────── month (1–12, JAN–DEC)
│ │ │ │ │ ┌───────── day-of-week (0–6, SUN–SAT)
│ │ │ │ │ │
* * * * * *

Each field supports:

Notation Example Description
* * Every value
N 5 Single value
N,M,... 1,15,30 Multiple values
N-M 1–5 Inclusive range
*/S */15 Step over full range
N-M/S 10–20/5 Step over a range

Month and day-of-week names are case-insensitive: jan, JAN, Jan all resolve to 1.

NOTES: Some formats have not yet been implemented and are still under further development.

API Reference

NewExpression(expr string) (Expression, error)

Parse a cron expression string into an Expression. Returns an error for invalid syntax, out-of-bounds values, or empty input.

Sentinal error Cause
ErrEmptyExpr Empty string
ErrInvalidFormat More than 6 fields
ErrParseDigits Non-numeric token
ErrOverflow Value exceeds uint8 (255)
ErrNumberOutOfBounds Value outside field range
ErrUnknownAlias Unrecognised @ alias

(Expression) Next(from time.Time) time.Time

Return the next time.Time that matches the expression, strictly after from. Returns zero time.Time if no match is found within a 4-year lookahead window.

(Expression) Due(t time.Time) bool

Check whether t matches the full expression (second through month).

Cascading helpers check increasingly coarse granularity:

expr.DueMonth(t)    // month matches?
expr.DueDay(t)      // month AND day match?
expr.DueHour(t)     // month AND day AND hour?
expr.DueMinute(t)   // month AND day AND hour AND minute?
expr.Due(t)         // full second-level match?

(Expression) Countdown(from time.Time) int64

Seconds from from until the next scheduled match. Returns -1 if no match exists within the lookahead window.

ParseField[T](tok string, bounds [2]uint8) (Field[T], error)

Parse a single cron field token into a typed bitmask. T must be uint8, uint16, uint32, or uint64.

(Field[T]) Match(val uint8) bool

Check if a value is set in the bitmask (respects field bounds).

(Field[T]) Next(from uint8) int

Return the next set value strictly after from, wrapping around. Returns -1 on an empty field.

(Field[T]) List() []uint8

Return all set values in ascending order.

Named Aliases

"@ticks"      → * * * * * *       (every second)
"@minutely"   → 0 * * * * *       (every minute)
"@5minutes"   → 0 */5 * * * *     (every 5 minutes)
"@10minutes"  → 0 */10 * * * *    (every 10 minutes)
"@30minutes"  → 0 */30 * * * *    (every 30 minutes)
"@hourly"     → 0 0 * * * *       (every hour)
"@daily"      → 0 0 0 * * *       (every day at midnight)
"@weekly"     → 0 0 0 * * 0       (every Sunday midnight)
"@monthly"    → 0 0 0 1 * *       (1st of every month)

Benchmarks

Processor Name: Intel i5 450M (2010s processor)`
Base clock: 2.40 GHz
Turbo boost: Up to 2.66 GHz (or 2.7 GHz depending on workload)
Cores/Threads: 2 physical cores, 4 processing threads via Hyper-Threading
Cache: 3 MB of L3 Intel Smart Cache

Results:
BenchmarkNewExpression     ~500 ns/op      0 allocs/op
BenchmarkExpression_Next   ~1300 ns/op      0 allocs/op
BenchmarkExpression_Due    ~120 ns/op      0 allocs/op
BenchmarkParseField        ~250 ns/op      0 allocs/op
BenchmarkNormalizer        ~230 ns/op      0 allocs/op

Contributing

Contributions are welcome. Open an issue or pull request on GitHub.

License

MIT — see LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (

	// ErrUnknownAlias is returned when an @alias does not match any known alias.
	ErrUnknownAlias = errors.New("unknown alias for an expression")

	// ErrInvalidFormat is returned when the expression has more than six fields.
	ErrInvalidFormat = errors.New("invalid format")

	// ErrEmptyExpr is returned when the expression string is empty after trimming.
	ErrEmptyExpr = errors.New("empty expression not allowed")
)
View Source
var (
	// ErrParseDigits is returned when a token contains non-digit characters.
	ErrParseDigits = errors.New("unable to parse digits")

	// ErrOverflow is returned when a numeric value exceeds the uint8 maximum (255).
	ErrOverflow = errors.New("value out of range for uint8 (overflow)")

	// ErrNumberOutOfBounds is returned when a value falls outside a field's
	// valid range (e.g. 60 in the seconds field).
	ErrNumberOutOfBounds = errors.New("number was out of bounds")
)

Functions

This section is empty.

Types

type Expression

type Expression struct {
	// contains filtered or unexported fields
}

Expression is a compiled cron expression.

Each field is stored as a bitmask for O(1) match checks. Construct an Expression with NewExpression.

func NewExpression

func NewExpression(expr string) (Expression, error)

NewExpression parses a cron expression string and returns a compiled Expression.

The expression supports six space-separated fields:

second minute hour day-of-month month day-of-week

Fewer than six fields are left-padded with "*". Month names (jan-dec) and day-of-week names (sun-sat) are accepted case-insensitively. The "@" prefix expands named aliases such as "@hourly", "@daily", "@weekly", and "@ticks".

NewExpression returns an error for empty input, invalid syntax, out-of-bounds values, overflow, too many fields, or unknown aliases.

func (Expression) Countdown

func (expr Expression) Countdown(from time.Time) int64

Countdown returns the number of whole seconds from from until the next scheduled time. It returns -1 if no match exists within the 4-year lookahead window.

func (Expression) Due

func (expr Expression) Due(of time.Time) bool

Due reports whether the time of matches the full expression at second precision.

func (Expression) DueDay

func (expr Expression) DueDay(of time.Time) bool

DueDay reports whether the month, day-of-month, and day-of-week all match the time of.

Unlike Next, DueDay requires both dayOfMonth and dayOfWeek to match.

func (Expression) DueHour

func (expr Expression) DueHour(of time.Time) bool

DueHour reports whether month, day, and hour all match the time of.

func (Expression) DueMinute

func (expr Expression) DueMinute(of time.Time) bool

DueMinute reports whether month, day, hour, and minute all match the time of.

func (Expression) DueMonth

func (expr Expression) DueMonth(of time.Time) bool

DueMonth reports whether the month field matches the month of of.

func (Expression) Next

func (expr Expression) Next(from time.Time) time.Time

Next returns the first time.Time strictly after from that matches the expression. It searches forward through seconds, minutes, hours, days, and months, rolling over to the next higher unit when no match is found.

Next applies a 4-year lookahead window to prevent infinite loops on sparse schedules. It returns the zero time.Time if no match is found within the window.

type Field

type Field[T fieldMask] struct {
	Value T
	Range [2]uint8
}

Field is a bitmask-based cron field parameterised over an unsigned integer type.

Value holds a bitmask where each set bit represents a matching value in the schedule. Range records the effective [min, max] of the set bits, used for bounds checking in Match.

T must be one of uint8, uint16, uint32, or uint64. Choose the smallest type that covers the field's range.

func ParseField

func ParseField[T fieldMask](tok string, bounds [2]uint8) (Field[T], error)

ParseField parses a single cron field token into a typed Field[T].

The token supports standard cron notation:

  • — every value N — single value N,M,... — multiple values N-M — inclusive range */S — step over full range N-M/S — step over a range

bounds is the inclusive [min, max] for the field (e.g. [0, 59] for seconds). T must be uint8, uint16, uint32, or uint64.

ParseField returns an error for invalid syntax, out-of-range values, or numeric overflow.

func (Field[T]) List

func (t Field[T]) List() []uint8

List returns all set values in ascending order.

List returns nil if the field is empty.

func (Field[T]) Match

func (t Field[T]) Match(val uint8) bool

Match reports whether val is set in the field's bitmask and falls within the field's effective range.

Values outside Range are always reported as not matching, even if the corresponding bit happens to be set.

func (Field[T]) Next

func (t Field[T]) Next(from uint8) int

Next returns the next set value strictly greater than from, wrapping around to the smallest value if none exists after from.

Next returns -1 if the field is empty (Value == 0).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL