mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-23 03:07:12 +00:00
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.
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
//! The Typikon's fast, as the Orthodox Church keeps it.
|
||||
//!
|
||||
//! This is the discipline printed in the Typikon and reproduced, with small
|
||||
//! variations, by every Orthodox jurisdiction's published calendar. It binds no
|
||||
//! one in the canonical sense: the Orthodox Church legislates fasting as
|
||||
//! tradition rather than as precept, so every day here is reported as
|
||||
//! customary. That is not a softening. It is the difference between a rule of
|
||||
//! life and a law, and the Eastern Catholic churches, which do have law about
|
||||
//! this, are handled in their own module.
|
||||
//!
|
||||
//! A saint of vigil or polyeleos rank lifts the weekday discipline of a fast:
|
||||
//! to wine and oil inside the Great Fast, as far as fish in the three lesser
|
||||
//! fasts. That is why the printed calendars mark so many Wednesdays in the
|
||||
//! Nativity Fast for wine and oil, and it is the rank in the menologion that
|
||||
//! decides it.
|
||||
//!
|
||||
//! Where usages differ, this follows the common published rule and says so in
|
||||
//! the reason. The known divergences:
|
||||
//!
|
||||
//! - **Great Thursday** is given wine and oil here. Some typika keep xerophagy.
|
||||
//! - **The Exaltation of the Cross** and the **Beheading of the Forerunner**
|
||||
//! are given wine and oil. Several jurisdictions print them as strict.
|
||||
//! - **Lazarus Saturday** is given wine and oil; caviar is the traditional
|
||||
//! addition, which this vocabulary has no level for.
|
||||
//! - **Great Week** is kept at xerophagy on its weekdays whatever saint falls
|
||||
//! in it, because the days of the Passion outrank the menologion.
|
||||
//! - **Monastic practice**, and the Holy Mountain in particular, is stricter
|
||||
//! than any of this throughout.
|
||||
//!
|
||||
//! A calendar cannot settle those. Your bishop and your spiritual father can.
|
||||
|
||||
use super::{Context, Rule, Vocabulary};
|
||||
use crate::calendars::Weekday;
|
||||
use crate::fasting::{Fast, Level};
|
||||
use crate::menologion::Rank;
|
||||
use crate::seasons::Season;
|
||||
|
||||
fn label(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::Strict => "no food until evening",
|
||||
Level::Xerophagy => "xerophagy \u{2014} no oil or wine",
|
||||
Level::WineOil => "wine and oil permitted",
|
||||
Level::Fish => "fish, wine and oil",
|
||||
Level::Dairy => "dairy and eggs; no meat",
|
||||
Level::None => "no fast",
|
||||
}
|
||||
}
|
||||
|
||||
fn emoji(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::Strict => "\u{26d4}",
|
||||
Level::Xerophagy => "\u{1f96c}",
|
||||
Level::WineOil => "\u{1fad2}",
|
||||
Level::Fish => "\u{1f41f}",
|
||||
Level::Dairy => "\u{1f9c0}",
|
||||
Level::None => "\u{1f356}",
|
||||
}
|
||||
}
|
||||
|
||||
/// How far a feast lifts a fast day.
|
||||
fn relaxed_by(rank: Option<Rank>) -> Option<Level> {
|
||||
match rank {
|
||||
Some(Rank::Great) => Some(Level::Fish),
|
||||
Some(Rank::Vigil) | Some(Rank::Polyeleos) => Some(Level::WineOil),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Relax `level` for the day's saint, never past `cap`, never tightening.
|
||||
fn for_feast(level: Level, ctx: &Context, cap: Level, reason: &str) -> Fast {
|
||||
let Some(target) = relaxed_by(ctx.rank) else {
|
||||
return Fast::customary(level, reason);
|
||||
};
|
||||
let lifted = level.relaxed_to(target, cap);
|
||||
if lifted == level {
|
||||
return Fast::customary(level, reason);
|
||||
}
|
||||
Fast::customary(lifted, &format!("{} \u{2014} the fast is relaxed", ctx.feast))
|
||||
}
|
||||
|
||||
pub fn fast_for(ctx: &Context) -> Fast {
|
||||
let md = ctx.fixed;
|
||||
let mv = ctx.mv;
|
||||
let weekend = ctx.is_weekend();
|
||||
|
||||
// The fast-free stretches, settled before anything else can override them.
|
||||
// A Friday inside Bright Week is not a fast day, and the ordinary Friday
|
||||
// rule further down would say otherwise.
|
||||
match ctx.season {
|
||||
Some(Season::BrightWeek) => return Fast::free("Bright Week \u{2014} no fasting"),
|
||||
Some(Season::PentecostWeek) => {
|
||||
return Fast::free("the week after Pentecost \u{2014} no fasting")
|
||||
}
|
||||
Some(Season::Christmastide) => {
|
||||
return Fast::free("the Nativity to the eve of Theophany")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if ctx.within(mv.publican_pharisee, 6) {
|
||||
// Fast-free precisely because the Pharisee boasted of fasting twice a week.
|
||||
return Fast::free("fast-free week (the Publican and the Pharisee)");
|
||||
}
|
||||
|
||||
// The Great Fast and Great Week.
|
||||
if matches!(ctx.season, Some(Season::GreatFast) | Some(Season::GreatWeek)) {
|
||||
let season = ctx.season.unwrap();
|
||||
// The Annunciation is tested before the strict days, not after. It can
|
||||
// land on Great Friday or Great Saturday, and when it does the feast
|
||||
// still lifts the total fast to wine and oil: it does not vanish.
|
||||
if md == (3, 25) {
|
||||
if ctx.civil == mv.great_friday || ctx.civil == mv.holy_saturday {
|
||||
return Fast::customary(
|
||||
Level::WineOil,
|
||||
"the Annunciation, on a day of the total fast",
|
||||
);
|
||||
}
|
||||
return Fast::customary(Level::Fish, "the Annunciation");
|
||||
}
|
||||
if ctx.civil == mv.palm_sunday {
|
||||
return Fast::customary(Level::Fish, "Palm Sunday");
|
||||
}
|
||||
if ctx.civil == mv.lazarus_saturday {
|
||||
return Fast::customary(Level::WineOil, "Lazarus Saturday");
|
||||
}
|
||||
if ctx.civil == mv.clean_monday {
|
||||
return Fast::customary(Level::Strict, "the first day of the Great Fast");
|
||||
}
|
||||
if ctx.civil == mv.great_friday {
|
||||
return Fast::customary(Level::Strict, "Great and Holy Friday");
|
||||
}
|
||||
if ctx.civil == mv.holy_saturday {
|
||||
// The one Saturday of the year kept without oil.
|
||||
return Fast::customary(Level::Xerophagy, "Great and Holy Saturday");
|
||||
}
|
||||
if ctx.civil == mv.great_thursday {
|
||||
return Fast::customary(Level::WineOil, "Great and Holy Thursday");
|
||||
}
|
||||
if season == Season::GreatWeek {
|
||||
return Fast::customary(
|
||||
Level::Xerophagy,
|
||||
&format!("a weekday of the {}", season.name()),
|
||||
);
|
||||
}
|
||||
if weekend {
|
||||
return Fast::customary(
|
||||
Level::WineOil,
|
||||
&format!("Saturday and Sunday of the {}", season.name()),
|
||||
);
|
||||
}
|
||||
return for_feast(
|
||||
Level::Xerophagy,
|
||||
ctx,
|
||||
Level::WineOil,
|
||||
&format!("a weekday of the {}", season.name()),
|
||||
);
|
||||
}
|
||||
|
||||
// Cheesefare: meat is set aside for good, everything else stays,
|
||||
// Wednesday and Friday included.
|
||||
if ctx.season == Some(Season::CheesefareWeek) {
|
||||
return Fast::customary(Level::Dairy, "Cheesefare Week");
|
||||
}
|
||||
if ctx.civil == mv.meatfare {
|
||||
return Fast::free("Meatfare Sunday \u{2014} the last day of meat");
|
||||
}
|
||||
|
||||
// The Nativity Fast, which tightens for its last five days.
|
||||
if ctx.season == Some(Season::NativityFast) {
|
||||
if md == (12, 24) {
|
||||
// The eve is relaxed to wine and oil on a Saturday or Sunday,
|
||||
// because the Liturgy of St Basil is served with Vespers.
|
||||
if weekend {
|
||||
return Fast::customary(Level::WineOil, "the eve of the Nativity, on a weekend");
|
||||
}
|
||||
return Fast::customary(Level::Xerophagy, "the eve of the Nativity");
|
||||
}
|
||||
if md == (11, 21) {
|
||||
return Fast::customary(Level::Fish, "the Entrance of the Theotokos");
|
||||
}
|
||||
if md >= (12, 20) {
|
||||
if matches!(ctx.weekday, Weekday::Tuesday | Weekday::Thursday) || weekend {
|
||||
return Fast::customary(Level::WineOil, "the forefeast of the Nativity");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::WineOil, "the forefeast of the Nativity");
|
||||
}
|
||||
if weekend {
|
||||
return Fast::customary(Level::Fish, "Saturday and Sunday of the Nativity Fast");
|
||||
}
|
||||
if matches!(ctx.weekday, Weekday::Tuesday | Weekday::Thursday) {
|
||||
return for_feast(Level::WineOil, ctx, Level::Fish, "Tuesday and Thursday of the Nativity Fast");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::Fish, "Monday, Wednesday and Friday of the Nativity Fast");
|
||||
}
|
||||
|
||||
// The Apostles' Fast, kept the same way.
|
||||
if ctx.season == Some(Season::Apostles) {
|
||||
if weekend {
|
||||
return Fast::customary(Level::Fish, "Saturday and Sunday of the Apostles' Fast");
|
||||
}
|
||||
if matches!(ctx.weekday, Weekday::Tuesday | Weekday::Thursday) {
|
||||
return for_feast(Level::WineOil, ctx, Level::Fish, "Tuesday and Thursday of the Apostles' Fast");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::Fish, "Monday, Wednesday and Friday of the Apostles' Fast");
|
||||
}
|
||||
|
||||
// The Dormition Fast, kept like the Great Fast.
|
||||
if ctx.season == Some(Season::Dormition) {
|
||||
if md == (8, 6) {
|
||||
return Fast::customary(Level::Fish, "the Transfiguration");
|
||||
}
|
||||
if weekend {
|
||||
return Fast::customary(Level::WineOil, "Saturday and Sunday of the Dormition Fast");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::WineOil, "a weekday of the Dormition Fast");
|
||||
}
|
||||
|
||||
// The three fixed fast days outside any season.
|
||||
if md == (1, 5) {
|
||||
if weekend {
|
||||
return Fast::customary(Level::WineOil, "the eve of Theophany, on a weekend");
|
||||
}
|
||||
return Fast::customary(Level::Xerophagy, "the eve of Theophany");
|
||||
}
|
||||
if md == (9, 14) {
|
||||
return Fast::customary(Level::WineOil, "the Exaltation of the Holy Cross");
|
||||
}
|
||||
if md == (8, 29) {
|
||||
return Fast::customary(Level::WineOil, "the Beheading of the Forerunner");
|
||||
}
|
||||
|
||||
// Ordinary Wednesdays and Fridays, which a feast can lift.
|
||||
if matches!(ctx.weekday, Weekday::Wednesday | Weekday::Friday) {
|
||||
if let Some(target) = relaxed_by(ctx.rank) {
|
||||
return Fast::customary(target, &format!("{} \u{2014} the fast is relaxed", ctx.feast));
|
||||
}
|
||||
return Fast::customary(Level::Xerophagy, "Wednesday and Friday");
|
||||
}
|
||||
|
||||
Fast::new(Level::None, "", None)
|
||||
}
|
||||
|
||||
pub static RULE: Rule = Rule {
|
||||
name: "orthodox",
|
||||
title: "the Typikon",
|
||||
vocabulary: Vocabulary { label, emoji },
|
||||
fast_for,
|
||||
// The Orthodox Church does not legislate the fast as precept, so nothing
|
||||
// here is reported as binding. See the module docs.
|
||||
has_obligation: false,
|
||||
};
|
||||
Reference in New Issue
Block a user