diff --git a/README.md b/README.md index 40330c8..dcffc5c 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,22 @@ which agrees with the Gregorian for every date until 2800. So a new-calendar church keeps the Nativity with the West and Pascha with the East, not a compromise but exactly what the 1923 congress decided. +### Greek or Slavic usage + +Both keep the same Typikon and read it a little differently. Two of those +differences are modelled, and each is visible in the output: + +| | Slavic | Greek | +|---|---|---| +| The Protection, 1 October | great feast | lower rank; Greece keeps it on 28 October besides | +| Fish in the Nativity Fast | Saturdays and Sundays | any day but Wednesday and Friday, until 17 December | + +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. + +### Which calendar? + Pick a reckoning without naming a church: ```console @@ -137,6 +153,10 @@ $ typikon --format ics --days 365 > orthodox.ics $ typikon --year 2027 # a year's key dates, to check against yours ``` +Every day of vigil rank or above carries the mark its books print beside it: +⊕ for a great feast, ✚ for a vigil, ☩ for a polyeleos. It is the `sign` +field in the JSON and a column in the delimited formats. + `--no-emoji` drops the glyphs from every format, for a terminal or a bar without them. `--list-fields` prints the columns `csv` and `tsv` can take, and `--fields` picks a subset in the order you name them. `--json` and `--ics` diff --git a/data/tradition-greek.json b/data/tradition-greek.json new file mode 100644 index 0000000..cfcf1bd --- /dev/null +++ b/data/tradition-greek.json @@ -0,0 +1,7 @@ +{ + "_meta": { + "mode": "add", + "title": "Greek usage", + "note": "Where Greek books differ from Slavic ones. Nothing overrides the common menologion yet; the fasting difference lives in the rule, not the calendar." + } +} diff --git a/data/tradition-slavic.json b/data/tradition-slavic.json new file mode 100644 index 0000000..445d5ab --- /dev/null +++ b/data/tradition-slavic.json @@ -0,0 +1,10 @@ +{ + "_meta": { + "mode": "add", + "title": "Slavic usage", + "note": "Where Slavic books differ from Greek ones. Ranks stated here override the common menologion; local commemorations would be added the same way." + }, + "10-01": { + "rank": "great" + } +} diff --git a/examples/dump.rs b/examples/dump.rs index 94c1e43..275ec1f 100644 --- a/examples/dump.rs +++ b/examples/dump.rs @@ -10,7 +10,7 @@ fn main() { let start = Date::parse_iso(&args[1]).expect("start date"); let days: i32 = args[2].parse().expect("days"); for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) { - let men = menologion::for_jurisdiction(jur.menaia); + let men = menologion::for_jurisdiction(jur.tradition, jur.menaia); for n in 0..days { let d = describe_with(start + n, jur, &men); println!("{}", serde_json::to_string(&d.to_json()).unwrap()); diff --git a/src/day.rs b/src/day.rs index 3a16e15..f655ed9 100644 --- a/src/day.rs +++ b/src/day.rs @@ -27,6 +27,7 @@ pub struct Day { pub cycle: Option, pub feast: Option, pub rank: Option, + pub tradition: &'static str, pub commemorations: Vec, pub fast: Level, pub fast_label: &'static str, @@ -69,6 +70,9 @@ pub struct DayJson<'a> { pub cycle: Option<&'a str>, pub feast: Option<&'a str>, pub rank: Option<&'a str>, + /// The mark the books print beside a day of this rank. + pub sign: Option<&'a str>, + pub tradition: &'a str, pub title: &'a str, pub commemorations: &'a [String], pub fast: &'a str, @@ -94,6 +98,8 @@ impl Day { cycle: self.cycle.as_deref(), feast: self.feast.as_deref(), rank: self.rank.map(|r| r.key()), + sign: self.rank.map(|r| r.sign()), + tradition: self.tradition, title: self.title(), commemorations: &self.commemorations, fast: self.fast.key(), @@ -114,7 +120,7 @@ impl Day { /// Reckon one civil date in one jurisdiction. pub fn describe(date: Date, jur: &'static Jurisdiction) -> Day { - let men = menologion::for_jurisdiction(jur.menaia); + let men = menologion::for_jurisdiction(jur.tradition, jur.menaia); describe_with(date, jur, &men) } @@ -176,6 +182,7 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) - .unwrap_or_default(), rank, jurisdiction: jur, + tradition: jur.tradition, }; let Fast { level, reason, obligation, binds } = (rule.fast_for)(&ctx); @@ -190,6 +197,7 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) - cycle: entry.cycle, feast, rank, + tradition: jur.tradition.key(), commemorations, fast: level, fast_label: (rule.vocabulary.label)(level), @@ -205,7 +213,7 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) - /// `days` consecutive days from `start`. pub fn week(start: Date, days: i32, jur: &'static Jurisdiction) -> Vec { - let men = menologion::for_jurisdiction(jur.menaia); + let men = menologion::for_jurisdiction(jur.tradition, jur.menaia); (0..days.max(1)) .map(|n| describe_with(start + n, jur, &men)) .collect() diff --git a/src/export/csv.rs b/src/export/csv.rs index b1dd251..8b45f83 100644 --- a/src/export/csv.rs +++ b/src/export/csv.rs @@ -8,7 +8,7 @@ use crate::day::Day; pub const COLUMNS: &[&str] = &[ "date", "weekday", "jurisdiction", "calendar", "church_date", "season", - "cycle", "feast", "rank", "title", "fast", "fast_label", "emoji", + "cycle", "feast", "rank", "sign", "tradition", "title", "fast", "fast_label", "emoji", "reason", "obligation", "binds", "pascha", "days_to_pascha", "julian_offset", "commemorations", ]; @@ -35,6 +35,8 @@ fn cell(day: &Day, column: &str) -> String { "cycle" => day.cycle.clone().unwrap_or_default(), "feast" => day.feast.clone().unwrap_or_default(), "rank" => day.rank.map(|r| r.key()).unwrap_or("").to_string(), + "sign" => day.rank.map(|r| r.sign()).unwrap_or("").to_string(), + "tradition" => day.tradition.to_string(), "title" => day.title().to_string(), "fast" => day.fast.key().to_string(), "fast_label" => day.fast_label.to_string(), diff --git a/src/jurisdictions.rs b/src/jurisdictions.rs index 6bcbdae..ff89a3c 100644 --- a/src/jurisdictions.rs +++ b/src/jurisdictions.rs @@ -30,6 +30,44 @@ pub enum MenologionKind { RevisedJulian, } +/// Greek or Slavic usage. Both keep the same Typikon; they read it a little +/// differently, and the differences are small, real and printed in every +/// calendar that takes a side. +/// +/// Two are modelled here: +/// +/// - **The Protection of the Theotokos**, 1 October, is a great feast in +/// Slavic usage. Greek practice ranks it lower, and the Church of Greece +/// keeps it on 28 October besides. +/// - **Fish in the Nativity Fast.** Greek practice permits fish on any day but +/// Wednesday and Friday until 17 December; Slavic practice keeps fish to +/// Saturdays and Sundays throughout. +/// +/// The split is coarse: a church is assigned the usage its books follow, and +/// no jurisdiction is wholly one thing. Override it with `--tradition`. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Tradition { + Slavic, + Greek, +} + +impl Tradition { + pub fn key(self) -> &'static str { + match self { + Tradition::Slavic => "slavic", + Tradition::Greek => "greek", + } + } + + pub fn parse(s: &str) -> Option { + match s.trim().to_ascii_lowercase().as_str() { + "slavic" | "russian" | "slav" => Some(Tradition::Slavic), + "greek" | "byzantine" => Some(Tradition::Greek), + _ => None, + } + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum RuleKind { Orthodox, @@ -61,6 +99,7 @@ pub struct Jurisdiction { pub paschalion: Paschalion, pub menologion: MenologionKind, pub rule: RuleKind, + pub tradition: Tradition, /// Supplementary commemoration sets to overlay, fetched not shipped. pub menaia: &'static [&'static str], pub note: &'static str, @@ -101,59 +140,60 @@ impl Jurisdiction { use MenologionKind::{Julian as JulMen, RevisedJulian as RevJul}; use Paschalion::{Gregorian as GregP, Julian as JulP}; use RuleKind::{Orthodox as Orth, Ruthenian as Ruth}; +use Tradition::{Greek as Gk, Slavic as Sl}; const NO_MENAIA: &[&str] = &[]; const RUTHENIAN_MENAIA: &[&str] = &["ruthenian"]; /// A reckoning without naming a church. `new` is the default. pub const CALENDARS: &[Jurisdiction] = &[ - Jurisdiction { key: "old", name: "Old Calendar", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "old", name: "Old Calendar", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "Julian throughout, the reckoning of Moscow, Serbia, Georgia, Jerusalem and the Holy Mountain." }, - Jurisdiction { key: "new", name: "New Calendar", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "new", name: "New Calendar", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "Revised Julian fixed feasts with the Julian Paschalion: Constantinople, Greece, Antioch, Romania, Bulgaria, the OCA." }, - Jurisdiction { key: "western", name: "Gregorian", paschalion: GregP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "western", name: "Gregorian", paschalion: GregP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "Gregorian throughout, Pascha included. Kept by the Finnish Orthodox Church and by most Byzantine Catholics." }, ]; pub const JURISDICTIONS: &[Jurisdiction] = &[ // old calendar - Jurisdiction { key: "moscow", name: "Russian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "rocor", name: "Russian Orthodox Church Outside Russia", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "serbian", name: "Serbian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "georgian", name: "Georgian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "jerusalem", name: "Patriarchate of Jerusalem", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "athos", name: "The Holy Mountain", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "moscow", name: "Russian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "rocor", name: "Russian Orthodox Church Outside Russia", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "serbian", name: "Serbian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "georgian", name: "Georgian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "jerusalem", name: "Patriarchate of Jerusalem", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "athos", name: "The Holy Mountain", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "Athonite practice is stricter than the printed Typikon at many points." }, - Jurisdiction { key: "polish", name: "Polish Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "polish", name: "Polish Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "Returned to the Julian menologion in 2014 after decades on the Revised Julian; some parishes kept the new calendar." }, - Jurisdiction { key: "macedonian", name: "Macedonian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "macedonian", name: "Macedonian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "" }, // new calendar - Jurisdiction { key: "constantinople", name: "Ecumenical Patriarchate", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "goarch", name: "Greek Orthodox Archdiocese of America", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "greece", name: "Church of Greece", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "antiochian", name: "Antiochian Orthodox Church", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "oca", name: "Orthodox Church in America", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "constantinople", name: "Ecumenical Patriarchate", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "goarch", name: "Greek Orthodox Archdiocese of America", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "greece", name: "Church of Greece", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "antiochian", name: "Antiochian Orthodox Church", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "oca", name: "Orthodox Church in America", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "Most parishes keep the new calendar; a minority, chiefly of Carpatho-Russian and Alaskan background, keep the old." }, - Jurisdiction { key: "romanian", name: "Romanian Orthodox Church", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "bulgarian", name: "Bulgarian Orthodox Church", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "cyprus", name: "Church of Cyprus", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "albanian", name: "Orthodox Church of Albania", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, - Jurisdiction { key: "ukrainian", name: "Orthodox Church of Ukraine", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "romanian", name: "Romanian Orthodox Church", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "bulgarian", name: "Bulgarian Orthodox Church", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "cyprus", name: "Church of Cyprus", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "albanian", name: "Orthodox Church of Albania", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "ukrainian", name: "Orthodox Church of Ukraine", paschalion: JulP, menologion: RevJul, rule: Orth, tradition: Sl, menaia: NO_MENAIA, note: "Moved the fixed feasts to the Revised Julian calendar on 1 September 2023. The Julian Paschalion is retained." }, // gregorian throughout - Jurisdiction { key: "finnish", name: "Finnish Orthodox Church", paschalion: GregP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, + Jurisdiction { key: "finnish", name: "Finnish Orthodox Church", paschalion: GregP, menologion: RevJul, rule: Orth, tradition: Gk, menaia: NO_MENAIA, note: "The only Orthodox Church keeping the Gregorian Paschalion." }, // eastern catholic - Jurisdiction { key: "ruthenian", name: "Byzantine Catholic Metropolia of Pittsburgh", paschalion: GregP, menologion: RevJul, rule: Ruth, menaia: RUTHENIAN_MENAIA, + Jurisdiction { key: "ruthenian", name: "Byzantine Catholic Metropolia of Pittsburgh", paschalion: GregP, menologion: RevJul, rule: Ruth, tradition: Sl, menaia: RUTHENIAN_MENAIA, note: "Gregorian throughout. Fasting is reported under Ruthenian particular law, which binds far less than the Typikon prescribes." }, - Jurisdiction { key: "ugcc", name: "Ukrainian Greek Catholic Church", paschalion: JulP, menologion: RevJul, rule: Ruth, menaia: RUTHENIAN_MENAIA, + Jurisdiction { key: "ugcc", name: "Ukrainian Greek Catholic Church", paschalion: JulP, menologion: RevJul, rule: Ruth, tradition: Sl, menaia: RUTHENIAN_MENAIA, note: "Moved the fixed feasts to the Revised Julian calendar on 1 September 2023, keeping the Julian Paschalion. Eparchies outside Ukraine vary; several keep the Gregorian Paschalion." }, - Jurisdiction { key: "melkite", name: "Melkite Greek Catholic Church", paschalion: GregP, menologion: RevJul, rule: Ruth, menaia: NO_MENAIA, + Jurisdiction { key: "melkite", name: "Melkite Greek Catholic Church", paschalion: GregP, menologion: RevJul, rule: Ruth, tradition: Gk, menaia: NO_MENAIA, note: "Practice is not uniform: parishes in the Middle East commonly keep the Julian Paschalion for the sake of the local Orthodox." }, - Jurisdiction { key: "romanian-catholic", name: "Romanian Greek Catholic Church", paschalion: GregP, menologion: RevJul, rule: Ruth, menaia: NO_MENAIA, note: "" }, + Jurisdiction { key: "romanian-catholic", name: "Romanian Greek Catholic Church", paschalion: GregP, menologion: RevJul, rule: Ruth, tradition: Gk, menaia: NO_MENAIA, note: "" }, ]; /// A jurisdiction key, a calendar name, or `None` for the default. diff --git a/src/main.rs b/src/main.rs index b888ca9..cae2670 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,10 @@ struct Args { #[arg(short = 'j', long, visible_alias = "calendar", short_alias = 'c')] jurisdiction: Option, + /// Greek or Slavic usage, overriding the jurisdiction's own + #[arg(short = 't', long)] + tradition: Option, + /// YYYY-MM-DD (default: today) #[arg(short, long)] date: Option, @@ -167,12 +171,13 @@ fn print_days(days: &[Day], jur: &Jurisdiction, source: Source, verbose: bool) { Source::Config => " (from config)".into(), Source::Default => String::new(), }; - println!(" {} \u{b7} {}{from}", jur.name, rule.title); + println!(" {} \u{b7} {} \u{b7} {} usage{from}", jur.name, rule.title, jur.tradition.key()); println!(); + let sign = d.rank.map(|r| format!("{} ", r.sign())).unwrap_or_default(); if d.emoji.is_empty() { - println!(" {}", d.title()); + println!(" {sign}{}", d.title()); } else { - println!(" {} {}", d.emoji, d.title()); + println!(" {} {sign}{}", d.emoji, d.title()); } if let Some(c) = &d.cycle { println!(" {c}"); @@ -284,9 +289,22 @@ fn run() -> Result { Some(j) => (Some(j.clone()), Source::Flag), None => config::default_jurisdiction(), }; - let jur = jurisdictions::resolve(name.as_deref()) + let base = jurisdictions::resolve(name.as_deref()) .map_err(|n| format!("unknown jurisdiction {n:?}; try --list"))?; + // A tradition given on the command line overrides the one the jurisdiction + // carries. Leaked as a 'static because a Jurisdiction is otherwise a + // compile-time constant, and one override per run is not worth threading + // a lifetime through the whole library for. + let jur: &'static Jurisdiction = match &args.tradition { + None => base, + Some(t) => { + let tradition = jurisdictions::Tradition::parse(t) + .ok_or_else(|| format!("unknown tradition {t:?}; try slavic or greek"))?; + Box::leak(Box::new(Jurisdiction { tradition, ..*base })) + } + }; + if let Some(year) = args.year { if !(1..=9999).contains(&year) { return Err(format!("{year} is not a year this calendar reaches")); diff --git a/src/menologion.rs b/src/menologion.rs index 3a840af..f021325 100644 --- a/src/menologion.rs +++ b/src/menologion.rs @@ -15,6 +15,7 @@ use std::collections::BTreeMap; use serde::Deserialize; use crate::calendars::Date; +use crate::jurisdictions::Tradition; use crate::paschalion::Movables; /// The degrees of solemnity the menologion marks, and the order they rank in. @@ -40,6 +41,20 @@ impl Rank { pub fn names_the_day(self) -> bool { matches!(self, Rank::Great | Rank::Vigil) } + + /// The mark the liturgical books print beside the day. + /// + /// In the Slavic books these are printed in red: a cross in a circle for a + /// great feast, a plain cross for a vigil, a cross over a half-circle for + /// a polyeleos. Unicode has no glyph for the last, so the Jerusalem cross + /// stands in for it, which is what digital calendars generally settle on. + pub fn sign(self) -> &'static str { + match self { + Rank::Great => "\u{2295}", + Rank::Vigil => "\u{271a}", + Rank::Polyeleos => "\u{2629}", + } + } } #[derive(Clone, Debug, Default, Deserialize)] @@ -81,6 +96,11 @@ pub struct Menologion { /// get wrong, and the binary answers for every day of the year on its own. const COMMON: &str = include_str!("../data/menologion.json"); +/// Where the two usages part company. Compiled in beside the common set, and +/// laid over it according to the jurisdiction's tradition. +const SLAVIC: &str = include_str!("../data/tradition-slavic.json"); +const GREEK: &str = include_str!("../data/tradition-greek.json"); + fn parse(text: &str) -> Result { let raw: BTreeMap = serde_json::from_str(text).map_err(|e| e.to_string())?; @@ -165,10 +185,21 @@ pub fn data_dir() -> std::path::PathBuf { base.join("typikon") } -/// The common menologion with a jurisdiction's supplementary sets overlaid. -/// A missing set is not an error: it simply has not been fetched. -pub fn for_jurisdiction(menaia: &[&str]) -> Menologion { - let mut m = Menologion::common().clone(); +fn tradition_overlay(t: Tradition) -> &'static Menologion { + use std::sync::OnceLock; + static SL: OnceLock = OnceLock::new(); + static GK: OnceLock = OnceLock::new(); + match t { + Tradition::Slavic => SL.get_or_init(|| parse(SLAVIC).expect("slavic usage parses")), + Tradition::Greek => GK.get_or_init(|| parse(GREEK).expect("greek usage parses")), + } +} + +/// The common menologion, the jurisdiction's usage laid over it, then any +/// supplementary sets. A missing fetched set is not an error: it simply has +/// not been fetched. +pub fn for_jurisdiction(tradition: Tradition, menaia: &[&str]) -> Menologion { + let mut m = Menologion::common().overlay(tradition_overlay(tradition)); for name in menaia { let path = data_dir().join(format!("{name}.json")); if let Ok(extra) = Menologion::load_file(&path) { diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 1832807..ee2cbac 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -23,6 +23,7 @@ pub struct Context<'a> { pub feast: String, pub rank: Option, pub jurisdiction: &'a Jurisdiction, + pub tradition: crate::jurisdictions::Tradition, } impl Context<'_> { diff --git a/src/rules/orthodox.rs b/src/rules/orthodox.rs index 53e2149..c1d0b77 100644 --- a/src/rules/orthodox.rs +++ b/src/rules/orthodox.rs @@ -24,6 +24,8 @@ //! 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. +//! - **Fish in the Nativity Fast** follows the usage, Greek or Slavic. See +//! `Tradition`. //! - **Monastic practice**, and the Holy Mountain in particular, is stricter //! than any of this throughout. //! @@ -32,6 +34,7 @@ use super::{Context, Rule, Vocabulary}; use crate::calendars::Weekday; use crate::fasting::{Fast, Level}; +use crate::jurisdictions::Tradition; use crate::menologion::Rank; use crate::seasons::Season; @@ -183,6 +186,16 @@ pub fn fast_for(ctx: &Context) -> Fast { } return for_feast(Level::Xerophagy, ctx, Level::WineOil, "the forefeast of the Nativity"); } + // Greek practice permits fish on any day but Wednesday and Friday + // until 17 December; Slavic practice keeps it to Saturdays and + // Sundays. This is the one place the two usages part on the fast. + if ctx.tradition == Tradition::Greek && md <= (12, 17) { + if matches!(ctx.weekday, Weekday::Wednesday | Weekday::Friday) { + return for_feast(Level::Xerophagy, ctx, Level::Fish, + "Wednesday and Friday of the Nativity Fast"); + } + return Fast::customary(Level::Fish, "the Nativity Fast, Greek usage"); + } if weekend { return Fast::customary(Level::Fish, "Saturday and Sunday of the Nativity Fast"); } diff --git a/tests/typikon.rs b/tests/typikon.rs index 0a71cc3..23580a8 100644 --- a/tests/typikon.rs +++ b/tests/typikon.rs @@ -465,3 +465,56 @@ fn every_advertised_column_actually_renders() { assert_eq!(lines[0].split('\t').count(), COLUMNS.len()); assert_eq!(lines[1].split('\t').count(), COLUMNS.len()); } + +// ───────────────────────── Greek and Slavic usage ───────────────────────── + +#[test] +fn the_protection_is_a_great_feast_in_slavic_usage_only() { + use typikon::jurisdictions::{Jurisdiction, Tradition}; + let base = *resolve(Some("goarch")).unwrap(); + let slavic: &'static Jurisdiction = + Box::leak(Box::new(Jurisdiction { tradition: Tradition::Slavic, ..base })); + let greek: &'static Jurisdiction = + Box::leak(Box::new(Jurisdiction { tradition: Tradition::Greek, ..base })); + assert_eq!(describe(d("2026-10-01"), slavic).rank, Some(Rank::Great)); + assert_eq!(describe(d("2026-10-01"), greek).rank, Some(Rank::Vigil)); +} + +#[test] +fn greek_usage_admits_fish_in_the_nativity_fast_where_slavic_does_not() { + // A Tuesday before 17 December. + assert_eq!(at("2026-12-01", "rocor").fast, Level::WineOil); + assert_eq!(at("2026-12-01", "goarch").fast, Level::Fish); + // Wednesday is a fast day in both. + assert!(at("2026-12-02", "goarch").fast <= Level::WineOil); + // And after the 17th the Greek relaxation stops. + assert_eq!(at("2026-12-22", "goarch").fast, Level::WineOil); +} + +#[test] +fn a_jurisdiction_carries_the_usage_its_books_follow() { + use typikon::jurisdictions::Tradition; + for key in ["moscow", "rocor", "serbian", "oca", "ruthenian", "ukrainian"] { + assert_eq!(resolve(Some(key)).unwrap().tradition, Tradition::Slavic, "{key}"); + } + for key in ["goarch", "greece", "constantinople", "antiochian", "cyprus"] { + assert_eq!(resolve(Some(key)).unwrap().tradition, Tradition::Greek, "{key}"); + } +} + +#[test] +fn every_rank_has_a_mark_and_they_are_distinct() { + let signs: Vec<&str> = [Rank::Great, Rank::Vigil, Rank::Polyeleos] + .iter().map(|r| r.sign()).collect(); + assert!(signs.iter().all(|s| !s.is_empty())); + assert_eq!(signs.iter().collect::>().len(), 3); +} + +#[test] +fn tradition_parses_the_names_people_use() { + use typikon::jurisdictions::Tradition; + assert_eq!(Tradition::parse("Slavic"), Some(Tradition::Slavic)); + assert_eq!(Tradition::parse("russian"), Some(Tradition::Slavic)); + assert_eq!(Tradition::parse("GREEK"), Some(Tradition::Greek)); + assert_eq!(Tradition::parse("coptic"), None); +}