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,142 @@
|
||||
//! Pascha, and the movable cycle that hangs off it.
|
||||
//!
|
||||
//! Two reckonings are in use. The **Julian Paschalion** is what nearly every
|
||||
//! Orthodox Church keeps: the old computus, worked in the Julian calendar and
|
||||
//! then read off onto a civil date. The **Gregorian Paschalion** is the Western
|
||||
//! one, kept by the Finnish Orthodox Church and by most Byzantine Catholics.
|
||||
//!
|
||||
//! Neither is a variant of the other. They are separate tables that happen to
|
||||
//! agree in some years, 2025 falls on 20 April by both, and can diverge by as
|
||||
//! much as five weeks.
|
||||
|
||||
use crate::calendars::{Date, JulianDate};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Paschalion {
|
||||
Julian,
|
||||
Gregorian,
|
||||
}
|
||||
|
||||
/// Pascha by the Julian Paschalion, as a civil (Gregorian) date.
|
||||
///
|
||||
/// Meeus' Julian algorithm yields a date in the *Julian* calendar, which is
|
||||
/// then read onto the civil one. The type system enforces that step here: the
|
||||
/// algorithm builds a `JulianDate`, and only `to_civil` produces a `Date`.
|
||||
pub fn julian_pascha(year: i32) -> Date {
|
||||
let a = year % 4;
|
||||
let b = year % 7;
|
||||
let c = year % 19;
|
||||
let d = (19 * c + 15) % 30;
|
||||
let e = (2 * a + 4 * b - d + 34) % 7;
|
||||
let month = ((d + e + 114) / 31) as u32;
|
||||
let day = (((d + e + 114) % 31) + 1) as u32;
|
||||
JulianDate { year, month, day }.to_civil()
|
||||
}
|
||||
|
||||
/// Pascha by the Gregorian Paschalion, the Western computus.
|
||||
pub fn gregorian_pascha(year: i32) -> Date {
|
||||
let a = year % 19;
|
||||
let b = year / 100;
|
||||
let c = year % 100;
|
||||
let d = b / 4;
|
||||
let e = b % 4;
|
||||
let g = (8 * b + 13) / 25;
|
||||
let h = (19 * a + b - d - g + 15) % 30;
|
||||
let i = c / 4;
|
||||
let k = c % 4;
|
||||
let l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
let m = (a + 11 * h + 19 * l) / 433;
|
||||
let month = ((h + l - 7 * m + 90) / 25) as u32;
|
||||
let day = ((h + l - 7 * m + 33 * month as i32 + 19) % 32) as u32;
|
||||
Date::from_ymd(year, month, day).expect("computus yields a real date")
|
||||
}
|
||||
|
||||
pub fn pascha(year: i32, p: Paschalion) -> Date {
|
||||
match p {
|
||||
Paschalion::Julian => julian_pascha(year),
|
||||
Paschalion::Gregorian => gregorian_pascha(year),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every movable date the calendar needs, keyed off Pascha.
|
||||
///
|
||||
/// All of these are counted in real days from Pascha, so they are civil dates
|
||||
/// already and need no calendar conversion: the distance from Pascha to
|
||||
/// Pentecost is fifty days in every reckoning.
|
||||
///
|
||||
/// A struct rather than a map, so a typo in a key is a compile error instead
|
||||
/// of a silent `None` at the far end of the program.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Movables {
|
||||
pub pascha: Date,
|
||||
|
||||
// the Triodion, counted backwards
|
||||
pub publican_pharisee: Date,
|
||||
pub prodigal_son: Date,
|
||||
pub meatfare: Date,
|
||||
pub cheesefare: Date,
|
||||
pub cheesefare_week_start: Date,
|
||||
pub clean_monday: Date,
|
||||
pub orthodoxy_sunday: Date,
|
||||
pub gregory_palamas: Date,
|
||||
pub cross_veneration: Date,
|
||||
pub john_climacus: Date,
|
||||
pub mary_of_egypt: Date,
|
||||
/// The Forty Days end here, on the Friday before Lazarus Saturday.
|
||||
pub great_fast_end: Date,
|
||||
pub lazarus_saturday: Date,
|
||||
pub palm_sunday: Date,
|
||||
pub great_week_start: Date,
|
||||
pub great_thursday: Date,
|
||||
pub great_friday: Date,
|
||||
pub holy_saturday: Date,
|
||||
|
||||
// the Pentecostarion, counted forwards
|
||||
pub bright_week_end: Date,
|
||||
pub thomas_sunday: Date,
|
||||
pub myrrhbearers: Date,
|
||||
pub midfeast: Date,
|
||||
pub ascension: Date,
|
||||
pub fathers_first_council: Date,
|
||||
pub pentecost: Date,
|
||||
pub pentecost_week_end: Date,
|
||||
pub all_saints: Date,
|
||||
/// The Apostles' Fast begins the Monday after All Saints. Its end is a
|
||||
/// fixed ecclesiastical date, so the jurisdiction resolves that.
|
||||
pub apostles_start: Date,
|
||||
}
|
||||
|
||||
pub fn movable_cycle(year: i32, p: Paschalion) -> Movables {
|
||||
let d = pascha(year, p);
|
||||
Movables {
|
||||
pascha: d,
|
||||
publican_pharisee: d - 70,
|
||||
prodigal_son: d - 63,
|
||||
meatfare: d - 56,
|
||||
cheesefare_week_start: d - 55,
|
||||
cheesefare: d - 49,
|
||||
clean_monday: d - 48,
|
||||
orthodoxy_sunday: d - 42,
|
||||
gregory_palamas: d - 35,
|
||||
cross_veneration: d - 28,
|
||||
john_climacus: d - 21,
|
||||
mary_of_egypt: d - 14,
|
||||
great_fast_end: d - 9,
|
||||
lazarus_saturday: d - 8,
|
||||
palm_sunday: d - 7,
|
||||
great_week_start: d - 6,
|
||||
great_thursday: d - 3,
|
||||
great_friday: d - 2,
|
||||
holy_saturday: d - 1,
|
||||
bright_week_end: d + 6,
|
||||
thomas_sunday: d + 7,
|
||||
myrrhbearers: d + 14,
|
||||
midfeast: d + 24,
|
||||
ascension: d + 39,
|
||||
fathers_first_council: d + 42,
|
||||
pentecost: d + 49,
|
||||
pentecost_week_end: d + 55,
|
||||
all_saints: d + 56,
|
||||
apostles_start: d + 57,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user