Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

crondemo.gno

7.67 Kb · 206 lines
  1// Package crondemo is a small gnoweb demo of the cron expression parser
  2// provided by the [p/moul/x/daily/cron](/p/moul/x/daily/cron/v0) library: the
  3// grammar, the next firing times of a handful of schedules, the union rule
  4// that governs the two day fields, and the expression that never fires.
  5//
  6// It contains no scheduling logic of its own, and no state. Every instant on
  7// the page is computed from the fixed anchor below rather than from
  8// time.Now(), which is what lets an example test pin the output: a block
  9// timestamp would make the page different on every call and identical to
 10// nothing.
 11package crondemo
 12
 13import (
 14	"strconv"
 15	"strings"
 16	"time"
 17
 18	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/ui/v0"
 19	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/cron/v0"
 20)
 21
 22// anchor is the instant every "next" on this page is measured from: a Tuesday
 23// morning, deliberately mid-minute so the exclusive boundary is visible.
 24var anchor = time.Date(2026, time.March, 10, 9, 17, 0, 0, time.UTC)
 25
 26// schedules is the worked example: one line per shape of the grammar.
 27var schedules = []struct {
 28	expr string
 29	what string
 30}{
 31	{"* * * * *", "every minute"},
 32	{"*/15 * * * *", "every quarter hour"},
 33	{"0 * * * *", "on the hour"},
 34	{"5,25,45 * * * *", "a list of minutes"},
 35	{"5/20 * * * *", "from minute 5, every 20"},
 36	{"30 8 * * 1-5", "weekday mornings"},
 37	{"0 12 * * sat,sun", "weekend noon"},
 38	{"0 0 1 jan,jul *", "twice a year"},
 39	{"0 0 29 2 *", "the leap day"},
 40}
 41
 42// macros is the @shorthand table, each paired with what it expands to.
 43var macros = []struct {
 44	macro string
 45	same  string
 46}{
 47	{"@hourly", "0 * * * *"},
 48	{"@daily", "0 0 * * *"},
 49	{"@weekly", "0 0 * * 0"},
 50	{"@monthly", "0 0 1 * *"},
 51	{"@yearly", "0 0 1 1 *"},
 52}
 53
 54// Render renders the demo for gnoweb.
 55//
 56//	Render("") / Render("/") -> the full demo
 57func Render(path string) string {
 58	var b strings.Builder
 59
 60	b.WriteString("# Cron expressions\n\n")
 61	b.WriteString("The five-field crontab grammar, parsed and answered: given a ")
 62	b.WriteString("schedule and an instant, when does it next fire. Demoing the ")
 63	b.WriteString("[`p/moul/x/daily/cron`](/p/moul/x/daily/cron/v0) library.\n\n")
 64
 65	b.WriteString("## The grammar\n\n")
 66	gt := ui.NewTable("position", "field", "range", "names")
 67	gt.Row("1", "minute", "`0-59`", "")
 68	gt.Row("2", "hour", "`0-23`", "")
 69	gt.Row("3", "day of month", "`1-31`", "")
 70	gt.Row("4", "month", "`1-12`", "`jan`..`dec`")
 71	gt.Row("5", "day of week", "`0-7`", "`sun`..`sat`")
 72	b.WriteString(gt.String() + "\n")
 73	b.WriteString("Every field is a comma-separated list whose items are a value, ")
 74	b.WriteString("a range, a star, or any of those with a step: `5`, `5-30`, ")
 75	b.WriteString("`*`, `*/15`, `5-30/5`, `5/20`. In the weekday field `0` and ")
 76	b.WriteString("`7` are both Sunday, which is what keeps `5-7` meaning Friday ")
 77	b.WriteString("through Sunday instead of running backwards.\n\n")
 78
 79	b.WriteString("## Next firing times\n\n")
 80	b.WriteString("Measured from `" + stamp(anchor) + "`, and strictly after it: ")
 81	b.WriteString("a schedule handed its own firing time answers with the ")
 82	b.WriteString("following one.\n\n")
 83	st := ui.NewTable("expression", "meaning", "next", "then", "and then")
 84	for _, sc := range schedules {
 85		s, err := cron.Parse(sc.expr)
 86		if err != nil {
 87			st.Row(ui.Cell(sc.expr), ui.Cell(sc.what), ui.Cell(err.Error()), "", "")
 88			continue
 89		}
 90		cells := []string{"never", "", ""}
 91		for i, t := range s.NextN(anchor, 3) {
 92			cells[i] = stamp(t)
 93		}
 94		st.Row("`"+sc.expr+"`", sc.what, cells[0], cells[1], cells[2])
 95	}
 96	b.WriteString(st.String() + "\n")
 97	b.WriteString("The last row is not a failure. `0 0 29 2 *` is a perfectly ")
 98	b.WriteString("well formed expression that skips three years out of four, ")
 99	b.WriteString("and the parser has no business rejecting it.\n\n")
100
101	b.WriteString("## The two day fields are a union\n\n")
102	b.WriteString("This is the rule a reimplementation gets wrong. When **both** ")
103	b.WriteString("the day-of-month and the day-of-week field are narrowed, a ")
104	b.WriteString("day matches when **either** does. `0 0 13 * 5` is the ")
105	b.WriteString("classic: the 13th of the month OR any Friday, never the ")
106	b.WriteString("intersection, which is why it also fires on ordinary ")
107	b.WriteString("Fridays.\n\n")
108	ut := ui.NewTable("expression", "day of month", "day of week", "combined", "next three")
109	for _, c := range []struct {
110		expr string
111		dom  string
112		dow  string
113		rule string
114	}{
115		{"0 0 13 * 5", "the 13th", "Friday", "**either**"},
116		{"0 0 13 * *", "the 13th", "any", "the 13th"},
117		{"0 0 * * 5", "any", "Friday", "Friday"},
118	} {
119		s := cron.MustParse(c.expr)
120		days := make([]string, 0, 3)
121		for _, t := range s.NextN(anchor, 3) {
122			days = append(days, stamp(t))
123		}
124		ut.Row("`"+c.expr+"`", c.dom, c.dow, c.rule, strings.Join(days, ", "))
125	}
126	b.WriteString(ut.String() + "\n")
127	b.WriteString("When only one of the two is narrowed the other matches every ")
128	b.WriteString("day, so the union and the intersection agree and there is ")
129	b.WriteString("nothing to decide. The rule only bites on the first row.\n\n")
130
131	b.WriteString("## Macros\n\n")
132	mt := ui.NewTable("macro", "expands to", "next")
133	for _, m := range macros {
134		s := cron.MustParse(m.macro)
135		next, ok := s.Next(anchor)
136		when := "never"
137		if ok {
138			when = stamp(next)
139		}
140		mt.Row("`"+m.macro+"`", "`"+m.same+"`", when)
141	}
142	b.WriteString(mt.String() + "\n")
143	b.WriteString("`@annually` is `@yearly` and `@midnight` is `@daily`. A ")
144	b.WriteString("schedule reports the expression it was parsed from, so a ")
145	b.WriteString("macro stays a macro on the way back out.\n\n")
146
147	b.WriteString("## What a bad expression says\n\n")
148	et := ui.NewTable("expression", "error")
149	for _, bad := range []string{
150		"* * * *",
151		"60 * * * *",
152		"* * * * 8",
153		"* * * * 5-1",
154		"*/0 * * * *",
155		"@fortnightly",
156	} {
157		_, err := cron.Parse(bad)
158		msg := "accepted"
159		if err != nil {
160			msg = err.Error()
161		}
162		et.Row("`"+bad+"`", ui.Cell(msg))
163	}
164	b.WriteString(et.String() + "\n")
165	b.WriteString("The field is named in the message. Five fields all accept ")
166	b.WriteString("numbers, so `out of range` on its own says nothing about ")
167	b.WriteString("which one to go and fix.\n\n")
168
169	b.WriteString("## Why a realm would want this\n\n")
170	b.WriteString("A chain has no timer. Nothing on gno.land wakes a realm up, ")
171	b.WriteString("so a schedule is not a thing that runs: it is a thing that ")
172	b.WriteString("is *checked*. A realm stores the expression, and whoever ")
173	b.WriteString("calls in next asks whether the due instant has passed. ")
174	b.WriteString("`Next` turns a crontab line into that one comparison, and ")
175	b.WriteString("`Matches` answers it for the current block without any ")
176	b.WriteString("arithmetic at the call site.\n\n")
177	b.WriteString("Everything here is UTC on purpose. A block timestamp carries ")
178	b.WriteString("no local zone, and a schedule whose answer depended on one ")
179	b.WriteString("would not be the same schedule on every validating node.\n")
180
181	return b.String()
182}
183
184// stamp formats an instant as the page shows it: date, minute, weekday.
185//
186// Written out with strconv rather than reached for from a formatter because
187// ufmt/v0 has no width flags, and a zero-padded minute is the whole point.
188func stamp(t time.Time) string {
189	return strconv.Itoa(t.Year()) + "-" + pad2(int(t.Month())) + "-" + pad2(t.Day()) +
190		" " + pad2(t.Hour()) + ":" + pad2(t.Minute()) +
191		" " + weekdayName(t.Weekday())
192}
193
194// pad2 renders 0..99 with a leading zero.
195func pad2(v int) string {
196	if v < 10 {
197		return "0" + strconv.Itoa(v)
198	}
199	return strconv.Itoa(v)
200}
201
202// weekdayName is the three-letter form, spelled the way the grammar spells it.
203func weekdayName(d time.Weekday) string {
204	names := []string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}
205	return names[int(d)]
206}