Commit Graph
11 Commits
Author SHA1 Message Date
funkpower 47649983f4 drop the terminal screenshot, and make the examples checkable
The screenshot was a picture of the Python CLI: it predated the formats, the
tradition in the header and --typikon, and a picture cannot be diffed against
the program anyway. Every example is now a console block of real output.

tests/readme-examples.py runs each one and diffs it, which found four things
wrong in the process of writing them: the hero example printed today rather
than a fixed date, so it was wrong for any reader on any other day; the
Ruthenian example depended on whatever the reader's config remembered; a
tradition block held two commands and was checking neither; and the JSON had
a hand-collapsed array that the program does not emit.

It also found a real bug. --typikon could be turned on by the config file and
there was no way to turn it off again from the command line, so
on this machine showed the Typikon's discipline with no way to ask for the
law. --no-typikon now does that.

The Rust example is a test rather than prose, for the same reason.
2026-09-18 12:44:09 -04:00
funkpower 3e3b585b2a a jurisdiction is not just a key any more
describe() in Python was answering a different fast than the same date on the
command line: xerophagy on a Wednesday of the Great Fast read as wine_oil. Not a
cosmetic difference in a tool whose whole job is to say what may be eaten.

The cause is that Jurisdiction grew a tradition and a discipline, and config
carries all three — main.rs reads the saved jurisdiction, then the saved
tradition, then whether the Typikon's full discipline was asked for, and builds
an overridden Jurisdiction from the lot. The bindings resolved the key and
stopped, so everything the other two decide was silently dropped.

They now take the same three steps, and tradition= and typikon= are arguments, so
the library can say what the command line can. Pascha does not take them: it
follows the paschalion alone and neither can move it.

The overrides are interned rather than leaked per call. main.rs leaks one per run
and exits; a library gets called in a loop, and a leak per call adds up. The
combinations are bounded by jurisdictions x traditions x discipline, so the map
cannot grow past that.

The parity test spread widens to seven dates across both fasts and both
calendars, and clippy now runs under the feature as well — the binding code only
exists there, so a bare clippy run had never looked at it.
2026-09-18 12:33:56 -04:00
funkpower 78c69e5979 one place decides how the reckoning reads
The single-day view said 'the Typikon, obliging under Ruthenian particular
law' while the week view above the same data said 'Ruthenian particular law'.
The second was actively wrong: it named the law while printing the Typikon's
levels.
2026-09-18 12:30:55 -04:00
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 4838d68b66 add ACROD, the Orthodox diocese this project's own aux menaion came from
The Carpatho-Rusyn diocese under Constantinople, formed from the Greek
Catholic parishes that returned to Orthodoxy in 1938. It was missing, which
is a strange gap in a calendar that already ships a fetcher for the Ruthenian
Metropolia's menaion: the two churches are the same people and the same
books, on either side of 1938.

The diocese publishes both an old-calendar and a new-calendar cycle and its
parishes keep one or the other, which acrod.org's own calendar toggle shows.
This entry is the old one; the note says how to get the other.
2026-09-18 12:15:05 -04:00
funkpower 43242c3837 carry sign and tradition through the Python bindings too
The bindings hand-map DayJson field by field, so the two fields added with
Greek and Slavic usage were in --json and not in to_dict(). The parity test
would have caught it; this is the fix rather than the discovery.

Both are getters as well, since every other field is.
2026-09-18 11:29:41 -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 5d7e02cffb answer from Python too
The engine is the same; only the front door is new. pyo3 behind an optional
feature, maturin for the wheel, and the bindings mirror DayJson rather than Day
because DayJson is already the shape front ends read and the one that has to stay
still. to_dict() therefore gives exactly what --json gives, and a test holds the
two to that.

That test earned its place immediately. The bindings were calling
jurisdictions::resolve(None), which answers with a built-in default that knows
nothing about the machine, so Python was reckoning by the New Calendar while the
command line on the same box reckoned by ROCOR — every field downstream of the
calendar differed, including the fast. They now take the same two steps main.rs
takes, and agree field for field on Pascha, the Nativity and Theophany.

The wheel is abi3, so one build serves 3.9 upward rather than one per
interpreter. That costs the datetime C API, which the limited API does not
expose, so a date is read through its attributes instead — which incidentally
makes datetime.datetime work as well as datetime.date. Constructing dates is
unaffected, pyo3 calls Python's own constructor for that.

panic = "abort" comes out of the release profile. An extension module has to be
able to unwind a panic into a Python exception; aborting would take the
interpreter down with it. The CLI keeps the same behaviour it always had, a
panic there just unwinds now.

Mixed layout so the wheel can carry py.typed and a stub file, which a bare
extension module has nowhere to put. mypy --strict is clean against the stubs
and catches a deliberate error, so the Typing :: Typed classifier is not a claim
about nothing.
2026-09-18 11:28:08 -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