render_example_test.gno
5.23 Kb · 87 lines
1package crondemo
2
3// ExampleRender pins the realm Render output as a testable example.
4//
5// The block below is copied verbatim from what gno printed, never hand
6// typed: every instant in it is computed by the library under test, so a
7// change in the parser shows up here as a diff rather than as prose.
8func ExampleRender() {
9 print(Render(""))
10 // Output:
11 // # Cron expressions
12 //
13 // The five-field crontab grammar, parsed and answered: given a schedule and an instant, when does it next fire. Demoing the [`p/moul/x/daily/cron`](/p/moul/x/daily/cron/v0) library.
14 //
15 // ## The grammar
16 //
17 // | position | field | range | names |
18 // | --- | --- | --- | --- |
19 // | 1 | minute | `0-59` | |
20 // | 2 | hour | `0-23` | |
21 // | 3 | day of month | `1-31` | |
22 // | 4 | month | `1-12` | `jan`..`dec` |
23 // | 5 | day of week | `0-7` | `sun`..`sat` |
24 //
25 // Every field is a comma-separated list whose items are a value, a range, a star, or any of those with a step: `5`, `5-30`, `*`, `*/15`, `5-30/5`, `5/20`. In the weekday field `0` and `7` are both Sunday, which is what keeps `5-7` meaning Friday through Sunday instead of running backwards.
26 //
27 // ## Next firing times
28 //
29 // Measured from `2026-03-10 09:17 tue`, and strictly after it: a schedule handed its own firing time answers with the following one.
30 //
31 // | expression | meaning | next | then | and then |
32 // | --- | --- | --- | --- | --- |
33 // | `* * * * *` | every minute | 2026-03-10 09:18 tue | 2026-03-10 09:19 tue | 2026-03-10 09:20 tue |
34 // | `*/15 * * * *` | every quarter hour | 2026-03-10 09:30 tue | 2026-03-10 09:45 tue | 2026-03-10 10:00 tue |
35 // | `0 * * * *` | on the hour | 2026-03-10 10:00 tue | 2026-03-10 11:00 tue | 2026-03-10 12:00 tue |
36 // | `5,25,45 * * * *` | a list of minutes | 2026-03-10 09:25 tue | 2026-03-10 09:45 tue | 2026-03-10 10:05 tue |
37 // | `5/20 * * * *` | from minute 5, every 20 | 2026-03-10 09:25 tue | 2026-03-10 09:45 tue | 2026-03-10 10:05 tue |
38 // | `30 8 * * 1-5` | weekday mornings | 2026-03-11 08:30 wed | 2026-03-12 08:30 thu | 2026-03-13 08:30 fri |
39 // | `0 12 * * sat,sun` | weekend noon | 2026-03-14 12:00 sat | 2026-03-15 12:00 sun | 2026-03-21 12:00 sat |
40 // | `0 0 1 jan,jul *` | twice a year | 2026-07-01 00:00 wed | 2027-01-01 00:00 fri | 2027-07-01 00:00 thu |
41 // | `0 0 29 2 *` | the leap day | 2028-02-29 00:00 tue | 2032-02-29 00:00 sun | 2036-02-29 00:00 fri |
42 //
43 // The last row is not a failure. `0 0 29 2 *` is a perfectly well formed expression that skips three years out of four, and the parser has no business rejecting it.
44 //
45 // ## The two day fields are a union
46 //
47 // This is the rule a reimplementation gets wrong. When **both** the day-of-month and the day-of-week field are narrowed, a day matches when **either** does. `0 0 13 * 5` is the classic: the 13th of the month OR any Friday, never the intersection, which is why it also fires on ordinary Fridays.
48 //
49 // | expression | day of month | day of week | combined | next three |
50 // | --- | --- | --- | --- | --- |
51 // | `0 0 13 * 5` | the 13th | Friday | **either** | 2026-03-13 00:00 fri, 2026-03-20 00:00 fri, 2026-03-27 00:00 fri |
52 // | `0 0 13 * *` | the 13th | any | the 13th | 2026-03-13 00:00 fri, 2026-04-13 00:00 mon, 2026-05-13 00:00 wed |
53 // | `0 0 * * 5` | any | Friday | Friday | 2026-03-13 00:00 fri, 2026-03-20 00:00 fri, 2026-03-27 00:00 fri |
54 //
55 // When only one of the two is narrowed the other matches every day, so the union and the intersection agree and there is nothing to decide. The rule only bites on the first row.
56 //
57 // ## Macros
58 //
59 // | macro | expands to | next |
60 // | --- | --- | --- |
61 // | `@hourly` | `0 * * * *` | 2026-03-10 10:00 tue |
62 // | `@daily` | `0 0 * * *` | 2026-03-11 00:00 wed |
63 // | `@weekly` | `0 0 * * 0` | 2026-03-15 00:00 sun |
64 // | `@monthly` | `0 0 1 * *` | 2026-04-01 00:00 wed |
65 // | `@yearly` | `0 0 1 1 *` | 2027-01-01 00:00 fri |
66 //
67 // `@annually` is `@yearly` and `@midnight` is `@daily`. A schedule reports the expression it was parsed from, so a macro stays a macro on the way back out.
68 //
69 // ## What a bad expression says
70 //
71 // | expression | error |
72 // | --- | --- |
73 // | `* * * *` | cron: want 5 fields, got 4 in "\* \* \* \*" |
74 // | `60 * * * *` | cron: minute field "60": out of range 0\-59: 60 |
75 // | `* * * * 8` | cron: day of week field "8": out of range 0\-7: 8 |
76 // | `* * * * 5-1` | cron: day of week field "5\-1": range runs backwards: "5\-1" |
77 // | `*/0 * * * *` | cron: minute field "\*/0": step must be at least 1, got 0 |
78 // | `@fortnightly` | cron: unknown macro "@fortnightly" |
79 //
80 // The field is named in the message. Five fields all accept numbers, so `out of range` on its own says nothing about which one to go and fix.
81 //
82 // ## Why a realm would want this
83 //
84 // A chain has no timer. Nothing on gno.land wakes a realm up, so a schedule is not a thing that runs: it is a thing that is *checked*. A realm stores the expression, and whoever calls in next asks whether the due instant has passed. `Next` turns a crontab line into that one comparison, and `Matches` answers it for the current block without any arithmetic at the call site.
85 //
86 // Everything here is UTC on purpose. A block timestamp carries no local zone, and a schedule whose answer depended on one would not be the same schedule on every validating node.
87}