mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
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.
113 lines
3.9 KiB
Rust
113 lines
3.9 KiB
Rust
//! Which season of the year a day falls in.
|
|
//!
|
|
//! The movable seasons are counted from Pascha and so are compared as civil
|
|
//! dates. The fixed ones, the Nativity and Dormition Fasts and the fast-free
|
|
//! days after the Nativity, are compared in **ecclesiastical** space, against
|
|
//! the date the Church calls the day. That is the whole trick that lets one set
|
|
//! of rules serve the old and new calendars at once: the Nativity Fast begins
|
|
//! on 15 November in both, and only the mornings differ.
|
|
//!
|
|
//! The Great Fast here runs from Clean Monday through Palm Sunday. The Forty
|
|
//! Days proper end on the Friday before Lazarus Saturday, which is what the
|
|
//! fasting rules count from, but a season is a label for a reader, and a Palm
|
|
//! Sunday belonging to no season at all is worse than one belonging loosely to
|
|
//! the Fast.
|
|
|
|
use crate::calendars::{fixed_between, Date};
|
|
use crate::paschalion::Movables;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
pub enum Season {
|
|
GreatFast,
|
|
GreatWeek,
|
|
BrightWeek,
|
|
PreLenten,
|
|
CheesefareWeek,
|
|
Apostles,
|
|
Dormition,
|
|
NativityFast,
|
|
PentecostWeek,
|
|
Christmastide,
|
|
}
|
|
|
|
impl Season {
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
Season::GreatFast => "Great Fast",
|
|
Season::GreatWeek => "Great and Holy Week",
|
|
Season::BrightWeek => "Bright Week",
|
|
Season::PreLenten => "Triodion",
|
|
Season::CheesefareWeek => "Cheesefare Week",
|
|
Season::Apostles => "Apostles' Fast",
|
|
Season::Dormition => "Dormition Fast",
|
|
Season::NativityFast => "Nativity Fast",
|
|
Season::PentecostWeek => "Week after Pentecost",
|
|
Season::Christmastide => "Christmastide",
|
|
}
|
|
}
|
|
|
|
/// The seasons in which a fast is kept, as distinct from the ones that are
|
|
/// merely named.
|
|
pub fn is_fasting_season(self) -> bool {
|
|
matches!(
|
|
self,
|
|
Season::GreatFast
|
|
| Season::GreatWeek
|
|
| Season::CheesefareWeek
|
|
| Season::Apostles
|
|
| Season::Dormition
|
|
| Season::NativityFast
|
|
)
|
|
}
|
|
}
|
|
|
|
/// The season a day belongs to, or `None` for ordinary time.
|
|
///
|
|
/// `fixed_md` is the ecclesiastical (month, day). `apostles_end` is the civil
|
|
/// date of 28 June reckoned in this jurisdiction's menologion, the eve of Ss
|
|
/// Peter and Paul, where the Apostles' Fast stops.
|
|
pub fn season_for(
|
|
civil: Date,
|
|
fixed_md: (u32, u32),
|
|
mv: &Movables,
|
|
apostles_end: Date,
|
|
) -> Option<Season> {
|
|
// Movable first: Great Week outranks the Great Fast it follows, and Bright
|
|
// Week outranks everything.
|
|
if mv.pascha <= civil && civil <= mv.bright_week_end {
|
|
return Some(Season::BrightWeek);
|
|
}
|
|
if mv.great_week_start <= civil && civil <= mv.holy_saturday {
|
|
return Some(Season::GreatWeek);
|
|
}
|
|
if mv.clean_monday <= civil && civil <= mv.palm_sunday {
|
|
return Some(Season::GreatFast);
|
|
}
|
|
if mv.cheesefare_week_start <= civil && civil <= mv.cheesefare {
|
|
return Some(Season::CheesefareWeek);
|
|
}
|
|
if mv.publican_pharisee <= civil && civil < mv.cheesefare_week_start {
|
|
return Some(Season::PreLenten);
|
|
}
|
|
if mv.pentecost <= civil && civil <= mv.pentecost_week_end {
|
|
return Some(Season::PentecostWeek);
|
|
}
|
|
// The Apostles' Fast can be empty in a year with a late Pascha, which this
|
|
// comparison handles without a special case.
|
|
if mv.apostles_start <= civil && civil <= apostles_end {
|
|
return Some(Season::Apostles);
|
|
}
|
|
|
|
// Fixed seasons, in ecclesiastical space.
|
|
if fixed_between(fixed_md, (8, 1), (8, 14)) {
|
|
return Some(Season::Dormition);
|
|
}
|
|
if fixed_between(fixed_md, (11, 15), (12, 24)) {
|
|
return Some(Season::NativityFast);
|
|
}
|
|
if fixed_between(fixed_md, (12, 25), (1, 4)) {
|
|
return Some(Season::Christmastide);
|
|
}
|
|
None
|
|
}
|