Commit Graph
5 Commits
Author SHA1 Message Date
funkpower c3a5c67f70 remember the whole setup, not a third of it
The config file held one key while the program grew three axes, so
--set-default recorded the jurisdiction and silently dropped the discipline
and the usage. On this machine that produced exactly the failure it sounds
like: a terminal reckoning by ROCOR beside a desktop widget reckoning by the
Ruthenian calendar, both correct, neither agreeing.

--set-default now writes whatever was given alongside it, and an ordinary run
reads all three back with flags still winning over the file.
2026-09-18 12:29:57 -04:00
funkpower be45fa6a27 separate the law that binds from the discipline that is shown
For an Eastern Catholic church these are sharply different questions and the
library was collapsing them. Ruthenian particular law binds on four kinds of
day in the year; the tradition those parishes actually keep is the Byzantine
fast entire. Reporting only the law made a week of the Great Fast read as
'no meat, fish, eggs or dairy' seven times over, which is the legal floor and
not the fast.

--typikon shows the day at the Typikon's discipline while the obligation
still comes from the jurisdiction's own law, because that is the part only it
can say. The Ruthenian rule now answers 'what binds here' separately from
'how is this day kept', which is what makes the two composable at all.

Three tests hold the seam: exactly the same days bind whichever discipline is
displayed, the binding minimum is never stricter than the level shown, and
the flag is a no-op for a jurisdiction already on the Typikon.
2026-09-18 12:21:06 -04:00
funkpower 56cc1a3d92 Greek and Slavic usage, and the marks the books print
Both keep the same Typikon and read it a little differently, and until now
this library silently picked one reading and called it the Typikon. Two
differences are modelled, each one visible in the output and named in the
day's reason:

  - The Protection of the Theotokos is a great feast in Slavic usage. Greek
    practice ranks it lower, and the Church of Greece keeps it on 28 October
    besides. This has teeth: rank is what lifts a Wednesday or Friday fast.
  - Greek practice admits fish on any day of the Nativity Fast but Wednesday
    and Friday until 17 December; Slavic practice keeps fish to Saturdays and
    Sundays throughout.

Every jurisdiction carries the usage its books follow, and --tradition
overrides it for a parish that does not match its jurisdiction's default. The
difference in the calendar lives in a data overlay compiled in beside the
common menologion, which is where a local church's own commemorations would
go; the difference in the fast lives in the rule, because that is where it is.

Each rank also now carries the mark the books print beside it, circled cross,
cross and Jerusalem cross for great, vigil and polyeleos. Unicode has no glyph
for the polyeleos sign, a cross over a half-circle, so the Jerusalem cross
stands in as digital calendars generally settle on.
2026-09-18 11:28:40 -04:00
funkpower 227abac6f6 print it as a table, or without the glyphs
--format takes text, json, csv, tsv or ics. --json and --ics stay as
shorthand, because the widgets already call them.

csv and tsv are RFC 4180 quoted, one header row, one row per day, with
commemorations joined by a semicolon: a cell that is itself a table is worse
than a cell you have to split. --list-fields prints the twenty columns,
--fields picks a subset in the order you name them, and a column that does
not exist is caught before anything is printed rather than silently emitting
an empty cell.

--no-emoji drops the glyphs from every format, including the emoji field in
the JSON, and collapses the column in the text output rather than leaving a
hole where a glyph used to be.
2026-09-18 11:15:22 -04:00
funkpower d115616eff rebuild the engine in Rust
Python was never the house language here: the mw-* desktop surface it came
from is 39 bash scripts and was exactly one Python script, the one this was
extracted from. What it cost was distribution, a widget dependency that
needed pipx and a Python floor, and 24ms of interpreter startup before a line
of the program ran.

The port is verified rather than asserted. Both implementations were run over
199,446 day-records, 26 jurisdictions across 21 years, with every field of
every record required to match; the JSON, the iCalendar and 480 renderings of
the text output are identical too. examples/dump.rs is the harness.

What the type system actually buys, which is less than the usual claim and
more than nothing:

  - Level is an ordered enum, strictest first, so  reads as 'a is
    stricter' and relaxed_to() is the one place a feast can lift a fast. No
    rule can get the direction backwards.
  - Date is a newtype over the Julian Day Number and JulianDate is a separate
    type, so a civil date and an ecclesiastical one cannot be mixed up. That
    confusion is the single most common way calendar code goes wrong.
  - Octets is a newtype in the iCalendar folder, because the bug it replaced
    counted characters against a limit RFC 5545 states in octets.
  - Movables is a struct, not a map, so a mistyped key is a compile error
    rather than a None at the far end of the program.

Of the six defects an adversarial review found in the Python, exactly one had
a shape a type system would have caught. The rest were logic, and the tests
came with them.

The menologion is compiled in with include_str!, so the binary answers on its
own: 930KB, no runtime, 1.2ms to start against Python's 36ms.
2026-09-18 11:13:15 -04:00