mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
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.
This commit is contained in:
@@ -7,12 +7,11 @@ Everything derives from the paschalion and the fixed calendar, so it needs no
|
||||
network, cannot go stale, and answers for any year you ask about. No API key,
|
||||
no service that might be gone next year, no cache to invalidate.
|
||||
|
||||

|
||||
|
||||
```console
|
||||
$ typikon
|
||||
$ typikon --jurisdiction rocor --date 2026-03-18
|
||||
|
||||
Wednesday 18 March 2026 — 5 March in the church calendar
|
||||
Russian Orthodox Church Outside Russia · the Typikon
|
||||
Russian Orthodox Church Outside Russia · the Typikon · slavic usage
|
||||
|
||||
🥬 The Holy Martyr Conon
|
||||
|
||||
@@ -21,6 +20,22 @@ $ typikon
|
||||
Great Fast
|
||||
```
|
||||
|
||||
A week at a time, which is what you want the day before a fast begins:
|
||||
|
||||
```console
|
||||
$ typikon --jurisdiction antiochian --date 2027-04-26 --days 7
|
||||
|
||||
Antiochian Orthodox Church · the Typikon
|
||||
|
||||
🥬 Mon 26 Apr xerophagy — no oil or wine The Holy Hieromartyr Basil, Bishop…
|
||||
🥬 Tue 27 Apr xerophagy — no oil or wine The Holy Hieromartyr Symeon, kinsman…
|
||||
🥬 Wed 28 Apr xerophagy — no oil or wine The Holy Apostles Jason and Sosipater
|
||||
🫒 Thu 29 Apr wine and oil permitted Great and Holy Thursday
|
||||
⛔ Fri 30 Apr no food until evening Great and Holy Friday
|
||||
🥬 Sat 1 May xerophagy — no oil or wine Great and Holy Saturday
|
||||
🍖 Sun 2 May no fast The Radiant and Life-giving…
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
One static binary, no runtime to install first.
|
||||
@@ -81,6 +96,21 @@ Every jurisdiction carries the usage its books follow. `--tradition slavic` or
|
||||
`--tradition greek` overrides it, which is what you want for a parish that does
|
||||
not match its jurisdiction's default.
|
||||
|
||||
```console
|
||||
$ typikon -j goarch --date 2026-10-01 --format tsv --fields rank,sign,fast,title
|
||||
rank sign fast title
|
||||
vigil ✚ none The Protection of our Most Holy Lady the Theotokos
|
||||
```
|
||||
|
||||
```console
|
||||
$ typikon -j goarch --date 2026-10-01 --tradition slavic --format tsv --fields rank,sign,fast,title
|
||||
rank sign fast title
|
||||
great ⊕ none The Protection of our Most Holy Lady the Theotokos
|
||||
```
|
||||
|
||||
The rank is not decoration: it is what lifts a Wednesday or Friday fast, so the
|
||||
usage changes what you eat as well as what you sing.
|
||||
|
||||
### Which calendar?
|
||||
|
||||
Pick a reckoning without naming a church:
|
||||
@@ -151,18 +181,43 @@ For an Eastern Catholic church the two come apart sharply, and reporting only
|
||||
the law makes the Great Fast look like four days of abstinence a year. That is
|
||||
what binds; it is not what is kept.
|
||||
|
||||
What the law requires, which is the first four days of the Great Fast:
|
||||
|
||||
```console
|
||||
$ typikon -j ruthenian --typikon
|
||||
$ typikon -j ruthenian --no-typikon --days 4 --date 2026-02-16
|
||||
|
||||
Byzantine Catholic Metropolia of Pittsburgh · Ruthenian particular law
|
||||
|
||||
🥬 Mon 16 Feb* no meat, fish, eggs or dairy Clean Monday — the first day of the…
|
||||
🥬 Tue 17 Feb no meat, fish, eggs or dairy The Holy Great Martyr Theodore the…
|
||||
🥬 Wed 18 Feb* no meat, fish, eggs or dairy Our Holy Father Leo, Pope of Rome (461)
|
||||
🥬 Thu 19 Feb no meat, fish, eggs or dairy The Holy Apostle Archippus
|
||||
|
||||
* obligatory. Everything unmarked is customary and does not bind.
|
||||
```
|
||||
|
||||
shows the day at the Typikon's full discipline while still reporting what your
|
||||
own church's law obliges, and still marking it. A week of the Great Fast under
|
||||
Ruthenian law is seven days of "no meat, fish, eggs or dairy" with three of
|
||||
them starred; the same week at the Typikon's discipline runs from a total fast
|
||||
on Clean Monday through xerophagy, wine and oil, and a saint's relaxation, with
|
||||
the same three days starred. Nothing about the obligation changes.
|
||||
And what is kept, with `--typikon`:
|
||||
|
||||
It is a no-op for a jurisdiction already reckoning by the Typikon.
|
||||
```console
|
||||
$ typikon -j ruthenian --typikon --days 4 --date 2026-02-16
|
||||
|
||||
Byzantine Catholic Metropolia of Pittsburgh · the Typikon, obliging under Ruthenian particular law
|
||||
|
||||
⛔ Mon 16 Feb* no food until evening Clean Monday — the first day of the…
|
||||
🫒 Tue 17 Feb wine and oil permitted The Holy Great Martyr Theodore the…
|
||||
🥬 Wed 18 Feb* xerophagy — no oil or wine Our Holy Father Leo, Pope of Rome (461)
|
||||
🥬 Thu 19 Feb xerophagy — no oil or wine The Holy Apostle Archippus
|
||||
|
||||
* obligatory. Everything unmarked is customary and does not bind.
|
||||
```
|
||||
|
||||
The same three days are starred in both. That is the point: the flag changes
|
||||
what is *kept*, not what *binds*. Tuesday relaxes to wine and oil because St
|
||||
Theodore the Recruit is of polyeleos rank, which is the menologion doing its
|
||||
work.
|
||||
|
||||
`--no-typikon` forces the law back, for when the config remembers otherwise.
|
||||
Either way it is a no-op for a jurisdiction already reckoning by the Typikon.
|
||||
|
||||
Where usages differ (Great Thursday, the Exaltation, Lazarus Saturday), this
|
||||
follows the common published rule and names it in the day's reason so you can
|
||||
@@ -175,10 +230,39 @@ spiritual father can.
|
||||
```console
|
||||
$ typikon --days 7 # for a person
|
||||
$ typikon --format json --days 7 # for a bar, a widget, a script
|
||||
$ typikon --format csv --days 365 # for a spreadsheet
|
||||
$ typikon --format tsv --fields date,fast,title
|
||||
$ typikon --format ics --days 365 > orthodox.ics
|
||||
$ typikon --year 2027 # a year's key dates, to check against yours
|
||||
```
|
||||
|
||||
Delimited output, for a spreadsheet or a pipeline. RFC 4180 quoting, one header
|
||||
row, one row per day:
|
||||
|
||||
```console
|
||||
$ typikon -j rocor --date 2026-03-16 --days 3 --format csv \
|
||||
--fields date,church_date,fast,title
|
||||
|
||||
date,church_date,fast,title
|
||||
2026-03-16,3 March,xerophagy,"The Holy Martyrs Eutropius, Cleonicus and Basiliscus"
|
||||
2026-03-17,4 March,xerophagy,Our Venerable Father Gerasimus of the Jordan
|
||||
2026-03-18,5 March,xerophagy,The Holy Martyr Conon
|
||||
```
|
||||
|
||||
A year's key dates, to check against the one your parish prints:
|
||||
|
||||
```console
|
||||
$ typikon --year 2027 -j goarch
|
||||
|
||||
2027 · Greek Orthodox Archdiocese of America
|
||||
|
||||
publican pharisee 2027-02-21 Sun
|
||||
meatfare 2027-03-07 Sun
|
||||
cheesefare 2027-03-14 Sun
|
||||
clean monday 2027-03-15 Mon
|
||||
great fast end 2027-04-23 Fri
|
||||
lazarus saturday 2027-04-24 Sat
|
||||
palm sunday 2027-04-25 Sun
|
||||
great friday 2027-04-30 Fri
|
||||
pascha 2027-05-02 Sun
|
||||
…
|
||||
```
|
||||
|
||||
Every day of vigil rank or above carries the mark its books print beside it:
|
||||
@@ -194,6 +278,38 @@ The JSON shape is flat and stable. Front ends are the thing most likely to be
|
||||
written by someone else, and a schema that shifts underneath them is a bad
|
||||
neighbour.
|
||||
|
||||
```console
|
||||
$ typikon -j rocor --date 2026-03-18 --json --indent 2
|
||||
{
|
||||
"date": "2026-03-18",
|
||||
"weekday": "Wednesday",
|
||||
"jurisdiction": "rocor",
|
||||
"calendar": "old",
|
||||
"churchDate": "5 March",
|
||||
"season": "Great Fast",
|
||||
"cycle": null,
|
||||
"feast": null,
|
||||
"rank": null,
|
||||
"sign": null,
|
||||
"tradition": "slavic",
|
||||
"title": "The Holy Martyr Conon",
|
||||
"commemorations": [
|
||||
"The Holy Martyr Conon"
|
||||
],
|
||||
"fast": "xerophagy",
|
||||
"fastLabel": "xerophagy — no oil or wine",
|
||||
"emoji": "🥬",
|
||||
"reason": "a weekday of the Great Fast",
|
||||
"obligation": "customary",
|
||||
"binds": "none",
|
||||
"pascha": "2026-04-12",
|
||||
"daysToPascha": 25,
|
||||
"julianOffset": 13
|
||||
}
|
||||
```
|
||||
|
||||
One day is an object; several are `{"days": [...]}`.
|
||||
|
||||
## As a library
|
||||
|
||||
```rust
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
+13
-2
@@ -39,9 +39,14 @@ struct Args {
|
||||
|
||||
/// Show the day at the Typikon's full discipline rather than only what
|
||||
/// your church's particular law requires. Obligations are still reported.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "no_typikon")]
|
||||
typikon: bool,
|
||||
|
||||
/// Show only what your church's particular law requires, overriding a
|
||||
/// `typikon = true` remembered in the config.
|
||||
#[arg(long)]
|
||||
no_typikon: bool,
|
||||
|
||||
/// YYYY-MM-DD (default: today)
|
||||
#[arg(short, long)]
|
||||
date: Option<String>,
|
||||
@@ -345,7 +350,13 @@ fn run() -> Result<i32, String> {
|
||||
Some(t) => jurisdictions::Tradition::parse(t)
|
||||
.ok_or_else(|| format!("unknown tradition {t:?}; try slavic or greek"))?,
|
||||
};
|
||||
let discipline = if args.typikon || saved.typikon {
|
||||
// A flag either way beats the file; with neither, the file decides.
|
||||
let typikon = if args.no_typikon {
|
||||
false
|
||||
} else {
|
||||
args.typikon || saved.typikon
|
||||
};
|
||||
let discipline = if typikon {
|
||||
Some(jurisdictions::RuleKind::Orthodox)
|
||||
} else {
|
||||
base.discipline
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run every console example in the README and diff it against the real output.
|
||||
|
||||
A README whose examples have quietly stopped being true is worse than one with
|
||||
no examples in it, and the output of this program changes shape often enough
|
||||
that hand-checking does not hold. Every ```console block whose single command
|
||||
is `typikon ...` is executed and compared line for line.
|
||||
|
||||
tests/readme-examples.py # against ./target/release/typikon
|
||||
TYPIKON=/usr/bin/typikon tests/readme-examples.py
|
||||
|
||||
A block ending in a horizontal ellipsis is treated as a prefix, for listings
|
||||
too long to paste whole.
|
||||
"""
|
||||
import difflib
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BINARY = os.environ.get("TYPIKON") or str(ROOT / "target" / "release" / "typikon")
|
||||
|
||||
|
||||
def commands_and_body(block):
|
||||
"""Split a console block into its commands and its expected output,
|
||||
joining backslash continuations."""
|
||||
out, buf = [], ""
|
||||
for line in block.rstrip("\n").split("\n"):
|
||||
if buf:
|
||||
buf += " " + line.strip().rstrip("\\ ")
|
||||
if not line.rstrip().endswith("\\"):
|
||||
out.append("$ " + buf)
|
||||
buf = ""
|
||||
continue
|
||||
if line.startswith("$ ") and line.rstrip().endswith("\\"):
|
||||
buf = line[2:].rstrip("\\ ")
|
||||
else:
|
||||
out.append(line)
|
||||
if buf:
|
||||
out.append("$ " + buf)
|
||||
cmds = [l[2:] for l in out if l.startswith("$ ")]
|
||||
body = "\n".join(l for l in out if not l.startswith("$ ")).strip("\n")
|
||||
return cmds, body
|
||||
|
||||
|
||||
def main():
|
||||
if not Path(BINARY).exists():
|
||||
print(f" {BINARY} not built; run cargo build --release", file=sys.stderr)
|
||||
return 77
|
||||
env = dict(os.environ, COLUMNS="88")
|
||||
# The examples must not depend on whatever this machine remembers.
|
||||
env.pop("TYPIKON_JURISDICTION", None)
|
||||
env["XDG_CONFIG_HOME"] = str(ROOT / "target" / "readme-config")
|
||||
|
||||
text = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
checked = bad = skipped = 0
|
||||
for block in re.findall(r"```console\n(.*?)```", text, re.S):
|
||||
cmds, expected = commands_and_body(block)
|
||||
if len(cmds) != 1 or not cmds[0].startswith("typikon") or not expected:
|
||||
skipped += 1
|
||||
continue
|
||||
argv = [BINARY] + shlex.split(cmds[0])[1:]
|
||||
got = subprocess.run(argv, capture_output=True, text=True, env=env).stdout.strip("\n")
|
||||
checked += 1
|
||||
if expected.rstrip().endswith("…"):
|
||||
ok = got.startswith(expected.rstrip()[:-1].rstrip("\n"))
|
||||
else:
|
||||
ok = got == expected
|
||||
if not ok:
|
||||
bad += 1
|
||||
print(f" MISMATCH {cmds[0]}")
|
||||
for line in difflib.unified_diff(
|
||||
expected.split("\n"), got.split("\n"), "README", "actual", lineterm=""
|
||||
):
|
||||
print(" ", line)
|
||||
print(f"\n {checked} README examples run, {bad} mismatched, {skipped} not executable")
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,30 @@
|
||||
//! The README's Rust example, compiled and asserted, so it cannot quietly
|
||||
//! stop being true. The prose version elides the error handling; this is the
|
||||
//! same calls with the results checked.
|
||||
|
||||
use typikon::calendars::Date;
|
||||
use typikon::day::describe;
|
||||
use typikon::fasting::Level;
|
||||
use typikon::jurisdictions::resolve;
|
||||
|
||||
#[test]
|
||||
fn the_readme_library_example_is_true() {
|
||||
let rocor = resolve(Some("rocor")).unwrap();
|
||||
let day = describe(Date::from_ymd(2026, 9, 18).unwrap(), rocor);
|
||||
|
||||
assert_eq!(day.church_date, "5 September");
|
||||
assert_eq!(day.fast, Level::Xerophagy);
|
||||
assert_eq!(day.title(), "The Holy Prophet Zachariah, father of the Forerunner");
|
||||
assert!(!day.is_obligatory());
|
||||
|
||||
assert_eq!(typikon::paschalion::julian_pascha(2027).iso(), "2027-05-02");
|
||||
assert_eq!(typikon::paschalion::gregorian_pascha(2027).iso(), "2027-03-28");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_readme_level_example_is_true() {
|
||||
// No saint brings fish into Lent.
|
||||
assert_eq!(Level::Xerophagy.relaxed_to(Level::Fish, Level::WineOil), Level::WineOil);
|
||||
// And relaxing never tightens.
|
||||
assert_eq!(Level::Fish.relaxed_to(Level::WineOil, Level::WineOil), Level::Fish);
|
||||
}
|
||||
Reference in New Issue
Block a user