cron: fix step offset matching, replace AST with bitmask
While reviewing !93 (merged) (which fixed wrap-around range matching in cron expressions), I (and Claude) took a closer look at the cron package and found a second bug in expression matching, along with broader test gaps.
Step expressions used global divisibility (v % step == 0) instead of checking offset from the range start. This meant 1/3 in minutes matched nothing at all (the only candidate, minute 1, fails 1 % 3 == 0), and 5-20/3 matched minutes 6, 9, 12, 15, 18 instead of the correct 5, 8, 11, 14, 17, 20.
This went undetected because the test suite (ported from Cloudflare's saffron) only covered cases with zero-aligned starts where the modulo happened to produce correct results.
To fix this, the runtime AST matching (Expr interface with Many, Range, Step, All, Value types) has been replaced with a bitmask approach: each cron field is precomputed into a uint64 at parse time, where bit N being set means value N is included. Ranges and steps are resolved by iterating and setting bits directly, so there is no runtime matching logic that can get step offset or wrap-around wrong. Contains() becomes a single bit-check per field.
What changed
expression.go: Replaced theExprinterface and its implementing types with abitfieldtype.Contains()checks bits instead of walking an AST.String()returns the original input.parser.go: Produces bitmasks directly via a singlesetStepfunction that handles values, ranges, and steps (including wrap-around) by iterating and setting bits. Usedstrings.Cutto simplify the parsing flow.parser_test.go: Rewritten to verify bitmask content rather than AST structure.expression_test.go: Added test coverage for non-aligned steps, wrap-around range+step combinations, and Sunday DOW edge cases.