Greek and Slavic usage, and the marks the books print

Both keep the same Typikon and read it a little differently, and until now
this library silently picked one reading and called it the Typikon. Two
differences are modelled, each one visible in the output and named in the
day's reason:

  - The Protection of the Theotokos is a great feast in Slavic usage. Greek
    practice ranks it lower, and the Church of Greece keeps it on 28 October
    besides. This has teeth: rank is what lifts a Wednesday or Friday fast.
  - Greek practice admits fish on any day of the Nativity Fast but Wednesday
    and Friday until 17 December; Slavic practice keeps fish to Saturdays and
    Sundays throughout.

Every jurisdiction carries the usage its books follow, and --tradition
overrides it for a parish that does not match its jurisdiction's default. The
difference in the calendar lives in a data overlay compiled in beside the
common menologion, which is where a local church's own commemorations would
go; the difference in the fast lives in the rule, because that is where it is.

Each rank also now carries the mark the books print beside it, circled cross,
cross and Jerusalem cross for great, vigil and polyeleos. Unicode has no glyph
for the polyeleos sign, a cross over a half-circle, so the Jerusalem cross
stands in as digital calendars generally settle on.
This commit is contained in:
funkpower
2026-09-18 11:28:40 -04:00
parent 5d7e02cffb
commit 56cc1a3d92
12 changed files with 241 additions and 38 deletions
+20
View File
@@ -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 church keeps the Nativity with the West and Pascha with the East, not a
compromise but exactly what the 1923 congress decided. 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: Pick a reckoning without naming a church:
```console ```console
@@ -137,6 +153,10 @@ $ typikon --format ics --days 365 > orthodox.ics
$ typikon --year 2027 # a year's key dates, to check against yours $ 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 `--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 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` `--fields` picks a subset in the order you name them. `--json` and `--ics`
+7
View File
@@ -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."
}
}
+10
View File
@@ -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"
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ fn main() {
let start = Date::parse_iso(&args[1]).expect("start date"); let start = Date::parse_iso(&args[1]).expect("start date");
let days: i32 = args[2].parse().expect("days"); let days: i32 = args[2].parse().expect("days");
for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) { 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 { for n in 0..days {
let d = describe_with(start + n, jur, &men); let d = describe_with(start + n, jur, &men);
println!("{}", serde_json::to_string(&d.to_json()).unwrap()); println!("{}", serde_json::to_string(&d.to_json()).unwrap());
+10 -2
View File
@@ -27,6 +27,7 @@ pub struct Day {
pub cycle: Option<String>, pub cycle: Option<String>,
pub feast: Option<String>, pub feast: Option<String>,
pub rank: Option<Rank>, pub rank: Option<Rank>,
pub tradition: &'static str,
pub commemorations: Vec<String>, pub commemorations: Vec<String>,
pub fast: Level, pub fast: Level,
pub fast_label: &'static str, pub fast_label: &'static str,
@@ -69,6 +70,9 @@ pub struct DayJson<'a> {
pub cycle: Option<&'a str>, pub cycle: Option<&'a str>,
pub feast: Option<&'a str>, pub feast: Option<&'a str>,
pub rank: 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 title: &'a str,
pub commemorations: &'a [String], pub commemorations: &'a [String],
pub fast: &'a str, pub fast: &'a str,
@@ -94,6 +98,8 @@ impl Day {
cycle: self.cycle.as_deref(), cycle: self.cycle.as_deref(),
feast: self.feast.as_deref(), feast: self.feast.as_deref(),
rank: self.rank.map(|r| r.key()), rank: self.rank.map(|r| r.key()),
sign: self.rank.map(|r| r.sign()),
tradition: self.tradition,
title: self.title(), title: self.title(),
commemorations: &self.commemorations, commemorations: &self.commemorations,
fast: self.fast.key(), fast: self.fast.key(),
@@ -114,7 +120,7 @@ impl Day {
/// Reckon one civil date in one jurisdiction. /// Reckon one civil date in one jurisdiction.
pub fn describe(date: Date, jur: &'static Jurisdiction) -> Day { 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) describe_with(date, jur, &men)
} }
@@ -176,6 +182,7 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) -
.unwrap_or_default(), .unwrap_or_default(),
rank, rank,
jurisdiction: jur, jurisdiction: jur,
tradition: jur.tradition,
}; };
let Fast { level, reason, obligation, binds } = (rule.fast_for)(&ctx); 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, cycle: entry.cycle,
feast, feast,
rank, rank,
tradition: jur.tradition.key(),
commemorations, commemorations,
fast: level, fast: level,
fast_label: (rule.vocabulary.label)(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`. /// `days` consecutive days from `start`.
pub fn week(start: Date, days: i32, jur: &'static Jurisdiction) -> Vec<Day> { pub fn week(start: Date, days: i32, jur: &'static Jurisdiction) -> Vec<Day> {
let men = menologion::for_jurisdiction(jur.menaia); let men = menologion::for_jurisdiction(jur.tradition, jur.menaia);
(0..days.max(1)) (0..days.max(1))
.map(|n| describe_with(start + n, jur, &men)) .map(|n| describe_with(start + n, jur, &men))
.collect() .collect()
+3 -1
View File
@@ -8,7 +8,7 @@ use crate::day::Day;
pub const COLUMNS: &[&str] = &[ pub const COLUMNS: &[&str] = &[
"date", "weekday", "jurisdiction", "calendar", "church_date", "season", "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", "reason", "obligation", "binds", "pascha", "days_to_pascha",
"julian_offset", "commemorations", "julian_offset", "commemorations",
]; ];
@@ -35,6 +35,8 @@ fn cell(day: &Day, column: &str) -> String {
"cycle" => day.cycle.clone().unwrap_or_default(), "cycle" => day.cycle.clone().unwrap_or_default(),
"feast" => day.feast.clone().unwrap_or_default(), "feast" => day.feast.clone().unwrap_or_default(),
"rank" => day.rank.map(|r| r.key()).unwrap_or("").to_string(), "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(), "title" => day.title().to_string(),
"fast" => day.fast.key().to_string(), "fast" => day.fast.key().to_string(),
"fast_label" => day.fast_label.to_string(), "fast_label" => day.fast_label.to_string(),
+66 -26
View File
@@ -30,6 +30,44 @@ pub enum MenologionKind {
RevisedJulian, 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<Tradition> {
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)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RuleKind { pub enum RuleKind {
Orthodox, Orthodox,
@@ -61,6 +99,7 @@ pub struct Jurisdiction {
pub paschalion: Paschalion, pub paschalion: Paschalion,
pub menologion: MenologionKind, pub menologion: MenologionKind,
pub rule: RuleKind, pub rule: RuleKind,
pub tradition: Tradition,
/// Supplementary commemoration sets to overlay, fetched not shipped. /// Supplementary commemoration sets to overlay, fetched not shipped.
pub menaia: &'static [&'static str], pub menaia: &'static [&'static str],
pub note: &'static str, pub note: &'static str,
@@ -101,59 +140,60 @@ impl Jurisdiction {
use MenologionKind::{Julian as JulMen, RevisedJulian as RevJul}; use MenologionKind::{Julian as JulMen, RevisedJulian as RevJul};
use Paschalion::{Gregorian as GregP, Julian as JulP}; use Paschalion::{Gregorian as GregP, Julian as JulP};
use RuleKind::{Orthodox as Orth, Ruthenian as Ruth}; use RuleKind::{Orthodox as Orth, Ruthenian as Ruth};
use Tradition::{Greek as Gk, Slavic as Sl};
const NO_MENAIA: &[&str] = &[]; const NO_MENAIA: &[&str] = &[];
const RUTHENIAN_MENAIA: &[&str] = &["ruthenian"]; const RUTHENIAN_MENAIA: &[&str] = &["ruthenian"];
/// A reckoning without naming a church. `new` is the default. /// A reckoning without naming a church. `new` is the default.
pub const CALENDARS: &[Jurisdiction] = &[ 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." }, 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." }, 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." }, note: "Gregorian throughout, Pascha included. Kept by the Finnish Orthodox Church and by most Byzantine Catholics." },
]; ];
pub const JURISDICTIONS: &[Jurisdiction] = &[ pub const JURISDICTIONS: &[Jurisdiction] = &[
// old calendar // old calendar
Jurisdiction { key: "moscow", name: "Russian Orthodox Church", paschalion: JulP, menologion: JulMen, rule: Orth, menaia: NO_MENAIA, note: "" }, 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, 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, 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, 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, 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, menaia: NO_MENAIA, 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." }, 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." }, 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 // new calendar
Jurisdiction { key: "constantinople", name: "Ecumenical Patriarchate", paschalion: JulP, menologion: RevJul, rule: Orth, menaia: NO_MENAIA, note: "" }, 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, 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, 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, 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, menaia: NO_MENAIA, 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." }, 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: "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, 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, 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, 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, menaia: NO_MENAIA, 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." }, note: "Moved the fixed feasts to the Revised Julian calendar on 1 September 2023. The Julian Paschalion is retained." },
// gregorian throughout // 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." }, note: "The only Orthodox Church keeping the Gregorian Paschalion." },
// eastern catholic // 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." }, 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." }, 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." }, 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. /// A jurisdiction key, a calendar name, or `None` for the default.
+22 -4
View File
@@ -33,6 +33,10 @@ struct Args {
#[arg(short = 'j', long, visible_alias = "calendar", short_alias = 'c')] #[arg(short = 'j', long, visible_alias = "calendar", short_alias = 'c')]
jurisdiction: Option<String>, jurisdiction: Option<String>,
/// Greek or Slavic usage, overriding the jurisdiction's own
#[arg(short = 't', long)]
tradition: Option<String>,
/// YYYY-MM-DD (default: today) /// YYYY-MM-DD (default: today)
#[arg(short, long)] #[arg(short, long)]
date: Option<String>, date: Option<String>,
@@ -167,12 +171,13 @@ fn print_days(days: &[Day], jur: &Jurisdiction, source: Source, verbose: bool) {
Source::Config => " (from config)".into(), Source::Config => " (from config)".into(),
Source::Default => String::new(), 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!(); println!();
let sign = d.rank.map(|r| format!("{} ", r.sign())).unwrap_or_default();
if d.emoji.is_empty() { if d.emoji.is_empty() {
println!(" {}", d.title()); println!(" {sign}{}", d.title());
} else { } else {
println!(" {} {}", d.emoji, d.title()); println!(" {} {sign}{}", d.emoji, d.title());
} }
if let Some(c) = &d.cycle { if let Some(c) = &d.cycle {
println!(" {c}"); println!(" {c}");
@@ -284,9 +289,22 @@ fn run() -> Result<i32, String> {
Some(j) => (Some(j.clone()), Source::Flag), Some(j) => (Some(j.clone()), Source::Flag),
None => config::default_jurisdiction(), 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"))?; .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 let Some(year) = args.year {
if !(1..=9999).contains(&year) { if !(1..=9999).contains(&year) {
return Err(format!("{year} is not a year this calendar reaches")); return Err(format!("{year} is not a year this calendar reaches"));
+35 -4
View File
@@ -15,6 +15,7 @@ use std::collections::BTreeMap;
use serde::Deserialize; use serde::Deserialize;
use crate::calendars::Date; use crate::calendars::Date;
use crate::jurisdictions::Tradition;
use crate::paschalion::Movables; use crate::paschalion::Movables;
/// The degrees of solemnity the menologion marks, and the order they rank in. /// 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 { pub fn names_the_day(self) -> bool {
matches!(self, Rank::Great | Rank::Vigil) 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)] #[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. /// get wrong, and the binary answers for every day of the year on its own.
const COMMON: &str = include_str!("../data/menologion.json"); 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<Menologion, String> { fn parse(text: &str) -> Result<Menologion, String> {
let raw: BTreeMap<String, serde_json::Value> = let raw: BTreeMap<String, serde_json::Value> =
serde_json::from_str(text).map_err(|e| e.to_string())?; 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") base.join("typikon")
} }
/// The common menologion with a jurisdiction's supplementary sets overlaid. fn tradition_overlay(t: Tradition) -> &'static Menologion {
/// A missing set is not an error: it simply has not been fetched. use std::sync::OnceLock;
pub fn for_jurisdiction(menaia: &[&str]) -> Menologion { static SL: OnceLock<Menologion> = OnceLock::new();
let mut m = Menologion::common().clone(); static GK: OnceLock<Menologion> = 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 { for name in menaia {
let path = data_dir().join(format!("{name}.json")); let path = data_dir().join(format!("{name}.json"));
if let Ok(extra) = Menologion::load_file(&path) { if let Ok(extra) = Menologion::load_file(&path) {
+1
View File
@@ -23,6 +23,7 @@ pub struct Context<'a> {
pub feast: String, pub feast: String,
pub rank: Option<Rank>, pub rank: Option<Rank>,
pub jurisdiction: &'a Jurisdiction, pub jurisdiction: &'a Jurisdiction,
pub tradition: crate::jurisdictions::Tradition,
} }
impl Context<'_> { impl Context<'_> {
+13
View File
@@ -24,6 +24,8 @@
//! addition, which this vocabulary has no level for. //! addition, which this vocabulary has no level for.
//! - **Great Week** is kept at xerophagy on its weekdays whatever saint falls //! - **Great Week** is kept at xerophagy on its weekdays whatever saint falls
//! in it, because the days of the Passion outrank the menologion. //! 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 //! - **Monastic practice**, and the Holy Mountain in particular, is stricter
//! than any of this throughout. //! than any of this throughout.
//! //!
@@ -32,6 +34,7 @@
use super::{Context, Rule, Vocabulary}; use super::{Context, Rule, Vocabulary};
use crate::calendars::Weekday; use crate::calendars::Weekday;
use crate::fasting::{Fast, Level}; use crate::fasting::{Fast, Level};
use crate::jurisdictions::Tradition;
use crate::menologion::Rank; use crate::menologion::Rank;
use crate::seasons::Season; 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"); 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 { if weekend {
return Fast::customary(Level::Fish, "Saturday and Sunday of the Nativity Fast"); return Fast::customary(Level::Fish, "Saturday and Sunday of the Nativity Fast");
} }
+53
View File
@@ -465,3 +465,56 @@ fn every_advertised_column_actually_renders() {
assert_eq!(lines[0].split('\t').count(), COLUMNS.len()); assert_eq!(lines[0].split('\t').count(), COLUMNS.len());
assert_eq!(lines[1].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::<std::collections::HashSet<_>>().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);
}