mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
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.
521 lines
20 KiB
Rust
521 lines
20 KiB
Rust
//! Pinned against published calendars and against dates whose answer is not
|
|
//! open to interpretation.
|
|
|
|
use typikon::calendars::{Date, JulianDate, Weekday};
|
|
use typikon::day::{describe, week, year_of};
|
|
use typikon::export::to_ics;
|
|
use typikon::fasting::{Level, Obligation};
|
|
use typikon::jurisdictions::{resolve, Calendar, MenologionKind, CALENDARS, JURISDICTIONS};
|
|
use typikon::menologion::Rank;
|
|
use typikon::paschalion::{gregorian_pascha, julian_pascha, Paschalion};
|
|
use typikon::seasons::Season;
|
|
|
|
fn d(s: &str) -> Date {
|
|
Date::parse_iso(s).expect("a real date")
|
|
}
|
|
|
|
fn at(s: &str, j: &str) -> typikon::day::Day {
|
|
describe(d(s), resolve(Some(j)).expect("a real jurisdiction"))
|
|
}
|
|
|
|
// ───────────────────────────── the paschalion ─────────────────────────────
|
|
|
|
#[test]
|
|
fn julian_pascha_matches_the_published_dates() {
|
|
for (year, expected) in [
|
|
(2024, "2024-05-05"), (2025, "2025-04-20"), (2026, "2026-04-12"),
|
|
(2027, "2027-05-02"), (2028, "2028-04-16"), (2030, "2030-04-28"),
|
|
] {
|
|
assert_eq!(julian_pascha(year).iso(), expected, "julian pascha {year}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn gregorian_pascha_matches_the_published_dates() {
|
|
for (year, expected) in [
|
|
(2024, "2024-03-31"), (2025, "2025-04-20"), (2026, "2026-04-05"),
|
|
(2027, "2027-03-28"), (2028, "2028-04-16"),
|
|
] {
|
|
assert_eq!(gregorian_pascha(year).iso(), expected, "gregorian pascha {year}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_two_paschalions_agree_in_2025_and_differ_by_a_week_in_2026() {
|
|
assert_eq!(julian_pascha(2025), gregorian_pascha(2025));
|
|
assert_eq!(julian_pascha(2026) - gregorian_pascha(2026), 7);
|
|
}
|
|
|
|
#[test]
|
|
fn the_paschalion_invariants_hold_for_two_centuries() {
|
|
for year in 1900..=2100 {
|
|
let (j, g) = (julian_pascha(year), gregorian_pascha(year));
|
|
assert_eq!(j.weekday(), Weekday::Sunday, "julian {year}");
|
|
assert_eq!(g.weekday(), Weekday::Sunday, "gregorian {year}");
|
|
assert!(
|
|
Date::from_ymd(year, 4, 4).unwrap() <= j && j <= Date::from_ymd(year, 5, 8).unwrap(),
|
|
"orthodox pascha {year} out of range: {j}"
|
|
);
|
|
assert!(
|
|
Date::from_ymd(year, 3, 22).unwrap() <= g && g <= Date::from_ymd(year, 4, 25).unwrap(),
|
|
"western pascha {year} out of range: {g}"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────── calendar drift ──────────────────────────────
|
|
|
|
#[test]
|
|
fn every_civil_date_round_trips_through_the_julian_calendar() {
|
|
let mut day = d("1800-01-01");
|
|
let last = d("2200-12-31");
|
|
while day <= last {
|
|
let j = day.to_julian();
|
|
assert_eq!(j.to_civil(), day, "round trip {day}");
|
|
day = day + 1;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn julian_lags_thirteen_days_now() {
|
|
assert_eq!(d("2026-09-18").julian_offset(), 13);
|
|
let j = d("2026-09-18").to_julian();
|
|
assert_eq!((j.year, j.month, j.day), (2026, 9, 5));
|
|
}
|
|
|
|
#[test]
|
|
fn the_julian_offset_widens_the_day_after_a_julian_leap_day() {
|
|
for (before, after) in [
|
|
("1900-03-13", "1900-03-14"),
|
|
("2100-03-14", "2100-03-15"),
|
|
("2200-03-15", "2200-03-16"),
|
|
] {
|
|
assert_eq!(
|
|
d(after).julian_offset(),
|
|
d(before).julian_offset() + 1,
|
|
"offset should widen at {after}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn old_calendar_nativity_falls_on_7_january() {
|
|
assert_eq!(
|
|
JulianDate { year: 2026, month: 12, day: 25 }.to_civil().iso(),
|
|
"2027-01-07"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn old_and_new_disagree_about_the_day_not_the_date() {
|
|
let old = at("2027-01-07", "rocor");
|
|
let new = at("2026-12-25", "goarch");
|
|
assert_eq!(old.church_date, "25 December");
|
|
assert_eq!(new.church_date, "25 December");
|
|
assert_eq!(old.feast, new.feast);
|
|
assert_eq!(old.rank, Some(Rank::Great));
|
|
assert_eq!(new.rank, Some(Rank::Great));
|
|
}
|
|
|
|
// ───────────────────────── the Orthodox fast ──────────────────────────────
|
|
|
|
#[test]
|
|
fn great_fast_weekdays_are_xerophagy_and_weekends_wine_and_oil() {
|
|
assert_eq!(at("2026-02-25", "new").fast, Level::Xerophagy);
|
|
assert_eq!(at("2026-02-28", "new").fast, Level::WineOil);
|
|
assert_eq!(at("2026-03-01", "new").fast, Level::WineOil);
|
|
}
|
|
|
|
#[test]
|
|
fn the_two_strictest_days_of_the_year() {
|
|
assert_eq!(at("2026-02-23", "new").fast, Level::Strict);
|
|
assert_eq!(at("2026-04-10", "new").fast, Level::Strict);
|
|
// The one Saturday kept without oil.
|
|
assert_eq!(at("2026-04-11", "new").fast, Level::Xerophagy);
|
|
}
|
|
|
|
#[test]
|
|
fn annunciation_and_palm_sunday_admit_fish_inside_the_great_fast() {
|
|
let a = at("2026-03-25", "new");
|
|
assert_eq!(a.season, Some(Season::GreatFast));
|
|
assert_eq!(a.fast, Level::Fish);
|
|
assert_eq!(at("2026-04-05", "new").fast, Level::Fish);
|
|
}
|
|
|
|
#[test]
|
|
fn the_annunciation_is_never_lost_to_great_friday() {
|
|
for (when, j) in [
|
|
("2016-03-25", "western"), ("2005-03-25", "western"),
|
|
("2029-04-07", "rocor"), ("2018-04-07", "rocor"),
|
|
] {
|
|
let day = at(when, j);
|
|
assert_eq!(day.church_date, "25 March", "{when}");
|
|
assert!(day.feast.as_deref().unwrap_or("").contains("Annunciation"), "{when}");
|
|
assert_eq!(day.rank, Some(Rank::Great), "{when}");
|
|
assert_eq!(day.fast, Level::WineOil, "{when}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn kyriopascha_and_the_annunciation_in_bright_week_stay_fast_free() {
|
|
assert_eq!(at("2035-03-25", "western").fast, Level::None);
|
|
assert_eq!(at("2075-04-07", "rocor").fast, Level::None);
|
|
}
|
|
|
|
#[test]
|
|
fn palm_sunday_and_lazarus_saturday_are_still_the_fast() {
|
|
for j in ["ruthenian", "ugcc", "melkite", "goarch", "rocor"] {
|
|
for year in [2025, 2026, 2027] {
|
|
let jur = resolve(Some(j)).unwrap();
|
|
let (mv, _) = year_of(year, jur);
|
|
for when in [mv.lazarus_saturday, mv.palm_sunday] {
|
|
let day = describe(when, jur);
|
|
assert!(day.season.is_some(), "{j} {when}: no season");
|
|
assert_ne!(day.fast, Level::None, "{j} {when}");
|
|
assert!(!day.reason.is_empty(), "{j} {when}: no reason");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_ranked_saint_relaxes_a_weekday_of_a_fasting_season() {
|
|
// The Nativity of the Forerunner, a great feast inside the Apostles' Fast.
|
|
assert_eq!(at("2020-06-24", "goarch").fast, Level::Fish);
|
|
// The Forty Martyrs of Sebaste, the textbook Lenten relaxation.
|
|
assert_eq!(at("2022-03-09", "goarch").fast, Level::WineOil);
|
|
// St Nicholas, a vigil saint on a Wednesday of the Nativity Fast.
|
|
assert_eq!(at("2023-12-06", "goarch").fast, Level::WineOil);
|
|
}
|
|
|
|
#[test]
|
|
fn no_saint_brings_fish_into_the_great_fast_or_lifts_great_week() {
|
|
for day in week(d("2022-03-07"), 40, resolve(Some("goarch")).unwrap()) {
|
|
if day.church_date == "25 March" {
|
|
continue;
|
|
}
|
|
if day.season == Some(Season::GreatFast) {
|
|
assert!(day.fast >= Level::Strict, "{}", day.date);
|
|
assert!(day.fast <= Level::WineOil, "{} got {:?}", day.date, day.fast);
|
|
}
|
|
if day.season == Some(Season::GreatWeek) {
|
|
assert!(day.fast <= Level::WineOil, "{} got {:?}", day.date, day.fast);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_eves_relax_when_they_fall_on_a_weekend() {
|
|
assert_eq!(at("2022-12-24", "goarch").fast, Level::WineOil);
|
|
assert_eq!(at("2023-12-24", "goarch").fast, Level::WineOil);
|
|
assert_eq!(at("2025-01-05", "goarch").fast, Level::WineOil);
|
|
// A weekday eve is still xerophagy.
|
|
assert_eq!(at("2024-12-24", "goarch").fast, Level::Xerophagy);
|
|
}
|
|
|
|
#[test]
|
|
fn cheesefare_week_keeps_dairy_on_wednesday_and_friday() {
|
|
let day = at("2026-02-18", "new");
|
|
assert_eq!(day.season, Some(Season::CheesefareWeek));
|
|
assert_eq!(day.fast, Level::Dairy);
|
|
}
|
|
|
|
#[test]
|
|
fn the_fast_free_stretches() {
|
|
assert_eq!(at("2026-04-17", "new").fast, Level::None); // Bright Friday
|
|
assert_eq!(at("2026-01-02", "new").fast, Level::None); // Christmastide
|
|
assert_eq!(at("2026-06-03", "new").fast, Level::None); // after Pentecost
|
|
// Fast-free precisely because the Pharisee boasted of fasting twice a week.
|
|
assert_eq!(at("2026-02-06", "new").fast, Level::None);
|
|
}
|
|
|
|
#[test]
|
|
fn nativity_fast_tightens_for_its_last_five_days() {
|
|
assert_eq!(at("2026-12-12", "new").fast, Level::Fish);
|
|
assert_eq!(at("2026-12-19", "new").fast, Level::Fish);
|
|
assert_eq!(at("2026-12-22", "new").fast, Level::WineOil);
|
|
assert_eq!(at("2026-12-24", "new").fast, Level::Xerophagy);
|
|
}
|
|
|
|
#[test]
|
|
fn ordinary_wednesday_and_friday_are_xerophagy() {
|
|
let day = at("2026-09-04", "new");
|
|
assert!(day.season.is_none());
|
|
assert!(day.rank.is_none());
|
|
assert_eq!(day.fast, Level::Xerophagy);
|
|
}
|
|
|
|
#[test]
|
|
fn a_polyeleos_saint_lifts_an_ordinary_friday_to_wine_and_oil() {
|
|
let day = at("2026-10-09", "new");
|
|
assert_eq!(day.weekday, Weekday::Friday);
|
|
assert_eq!(day.rank, Some(Rank::Polyeleos));
|
|
assert_eq!(day.fast, Level::WineOil);
|
|
}
|
|
|
|
#[test]
|
|
fn the_apostles_fast_is_empty_when_pascha_is_late() {
|
|
let goarch: Vec<_> = week(d("2024-05-01"), 120, resolve(Some("goarch")).unwrap())
|
|
.into_iter().filter(|x| x.season == Some(Season::Apostles)).collect();
|
|
let rocor: Vec<_> = week(d("2024-05-01"), 120, resolve(Some("rocor")).unwrap())
|
|
.into_iter().filter(|x| x.season == Some(Season::Apostles)).collect();
|
|
assert!(goarch.is_empty(), "goarch 2024 should have no Apostles' Fast");
|
|
assert!(!rocor.is_empty(), "rocor 2024 should still have one");
|
|
}
|
|
|
|
#[test]
|
|
fn the_orthodox_rule_never_reports_an_obligation() {
|
|
for day in week(d("2026-01-01"), 365, resolve(Some("new")).unwrap()) {
|
|
assert_ne!(day.obligation, Some(Obligation::Obligatory), "{}", day.date);
|
|
}
|
|
}
|
|
|
|
// ──────────────────── Byzantine Catholic particular law ───────────────────
|
|
|
|
#[test]
|
|
fn ruthenian_pascha_is_the_gregorian_one() {
|
|
assert_eq!(at("2026-06-01", "ruthenian").pascha.iso(), "2026-04-05");
|
|
}
|
|
|
|
#[test]
|
|
fn ruthenian_clean_monday_is_16_february_2026() {
|
|
assert!(at("2026-02-16", "ruthenian").reason.starts_with("the first day"));
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_great_fast_binds_under_ruthenian_law() {
|
|
let obligatory: Vec<_> = week(d("2026-01-01"), 365, resolve(Some("ruthenian")).unwrap())
|
|
.into_iter().filter(|x| x.is_obligatory()).collect();
|
|
let dates: Vec<String> = obligatory.iter().map(|x| x.date.iso()).collect();
|
|
assert!(dates.contains(&"2026-02-16".to_string()));
|
|
assert!(dates.contains(&"2026-04-03".to_string()));
|
|
for day in &obligatory {
|
|
assert!(
|
|
matches!(day.season, Some(Season::GreatFast) | Some(Season::GreatWeek)),
|
|
"{} binds outside the Fast", day.date
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_voluntary_seasons_never_report_as_binding() {
|
|
for when in ["2026-11-20", "2026-08-05"] {
|
|
assert_eq!(at(when, "ruthenian").obligation, Some(Obligation::Customary));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn christmas_on_a_friday_does_not_ask_you_to_abstain() {
|
|
let day = at("2026-12-25", "ruthenian");
|
|
assert_eq!(day.weekday, Weekday::Friday);
|
|
assert_eq!(day.fast, Level::None);
|
|
}
|
|
|
|
#[test]
|
|
fn ruthenian_strict_days_bind_strictly_and_the_rest_bind_to_meat() {
|
|
assert_eq!(at("2026-02-16", "ruthenian").binds, Level::WineOil);
|
|
assert_eq!(at("2026-04-03", "ruthenian").binds, Level::WineOil);
|
|
assert_eq!(at("2026-02-18", "ruthenian").binds, Level::Dairy);
|
|
assert_eq!(at("2026-10-09", "ruthenian").binds, Level::None);
|
|
}
|
|
|
|
// ──────────────────────────── the menologion ──────────────────────────────
|
|
|
|
#[test]
|
|
fn every_day_of_a_leap_year_has_a_commemoration() {
|
|
for day in week(d("2024-01-01"), 366, resolve(Some("new")).unwrap()) {
|
|
assert!(!day.commemorations.is_empty(), "{}", day.date);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_great_feasts_are_all_present() {
|
|
for (month, day) in [(9, 8), (9, 14), (11, 21), (12, 25), (1, 6), (2, 2), (3, 25), (8, 6), (8, 15)] {
|
|
let x = describe(Date::from_ymd(2026, month, day).unwrap(), resolve(Some("goarch")).unwrap());
|
|
assert_eq!(x.rank, Some(Rank::Great), "{month}-{day}");
|
|
}
|
|
for when in ["2026-04-05", "2026-05-21", "2026-05-31"] {
|
|
assert_eq!(at(when, "goarch").rank, Some(Rank::Great), "{when}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_movable_feast_outranks_the_fixed_calendar() {
|
|
assert!(at("2026-04-12", "new").feast.as_deref().unwrap_or("").contains("Pascha"));
|
|
}
|
|
|
|
// ──────────────────────────────── output ──────────────────────────────────
|
|
|
|
#[test]
|
|
fn ics_content_lines_stay_within_seventy_five_octets() {
|
|
for j in ["goarch", "rocor", "ruthenian"] {
|
|
let days = week(d("2026-01-01"), 365, resolve(Some(j)).unwrap());
|
|
let text = to_ics(&days, "Typikon", "20260101T000000Z");
|
|
for line in text.split("\r\n") {
|
|
assert!(line.len() <= 75, "{j}: {} octets: {line}", line.len());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ics_is_well_formed() {
|
|
let days = week(d("2026-04-10"), 7, resolve(Some("new")).unwrap());
|
|
let text = to_ics(&days, "Typikon", "20260101T000000Z");
|
|
assert!(text.starts_with("BEGIN:VCALENDAR"));
|
|
assert!(text.trim_end().ends_with("END:VCALENDAR"));
|
|
assert_eq!(text.matches("BEGIN:VEVENT").count(), text.matches("END:VEVENT").count());
|
|
}
|
|
|
|
// ─────────────────────────── every jurisdiction ───────────────────────────
|
|
|
|
#[test]
|
|
fn every_jurisdiction_holds_together_for_twenty_years() {
|
|
for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) {
|
|
for day in week(d("2020-01-01"), 366 * 21 / 4, jur) {
|
|
assert!(!day.commemorations.is_empty(), "{} {}", jur.key, day.date);
|
|
assert!(!day.title().is_empty(), "{} {}", jur.key, day.date);
|
|
// `binds` may never be stricter than the level it sits under.
|
|
assert!(day.binds >= day.fast, "{} {}: binds {:?} > fast {:?}",
|
|
jur.key, day.date, day.binds, day.fast);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_calendar_name_resolves_without_naming_a_church() {
|
|
assert_eq!(resolve(Some("old")).unwrap().menologion, MenologionKind::Julian);
|
|
assert_eq!(resolve(Some("new")).unwrap().menologion, MenologionKind::RevisedJulian);
|
|
assert_eq!(resolve(Some("western")).unwrap().paschalion, Paschalion::Gregorian);
|
|
assert_eq!(resolve(None).unwrap().key, "new");
|
|
assert!(resolve(Some("atlantis")).is_err());
|
|
assert_eq!(resolve(Some("rocor")).unwrap().calendar(), Calendar::Old);
|
|
}
|
|
|
|
// ─────────────────────── the levels are an ordered set ────────────────────
|
|
|
|
#[test]
|
|
fn a_feast_can_only_relax_a_fast_never_tighten_it() {
|
|
// Strictest to laxest, so `<` means stricter.
|
|
assert!(Level::Strict < Level::Xerophagy);
|
|
assert!(Level::Xerophagy < Level::WineOil);
|
|
assert!(Level::Dairy < Level::None);
|
|
// Relaxing to fish inside the Great Fast stops at wine and oil.
|
|
assert_eq!(Level::Xerophagy.relaxed_to(Level::Fish, Level::WineOil), Level::WineOil);
|
|
// A cap stricter than the day never tightens the day.
|
|
assert_eq!(Level::Fish.relaxed_to(Level::WineOil, Level::WineOil), Level::Fish);
|
|
// No rank means no change.
|
|
assert_eq!(Level::Xerophagy.relaxed_to(Level::Xerophagy, Level::Fish), Level::Xerophagy);
|
|
}
|
|
|
|
// ───────────────────────── the delimited formats ──────────────────────────
|
|
|
|
#[test]
|
|
fn csv_has_a_header_and_one_row_per_day() {
|
|
use typikon::export::to_delimited;
|
|
let days = week(d("2026-01-01"), 5, resolve(Some("rocor")).unwrap());
|
|
let text = to_delimited(&days, ',', &[]);
|
|
let lines: Vec<&str> = text.trim_end().split('\n').collect();
|
|
assert_eq!(lines.len(), 6, "header plus five days");
|
|
assert!(lines[0].starts_with("date,weekday,"));
|
|
assert!(lines[1].starts_with("2026-01-01,Thursday,rocor,old,"));
|
|
}
|
|
|
|
#[test]
|
|
fn csv_quotes_the_fields_that_need_it() {
|
|
use typikon::export::to_delimited;
|
|
// Commemorations are full of commas, and joined with semicolons besides.
|
|
let days = week(d("2026-09-18"), 1, resolve(Some("rocor")).unwrap());
|
|
let text = to_delimited(&days, ',', &["title".to_string()]);
|
|
let row = text.trim_end().split('\n').nth(1).unwrap();
|
|
assert!(row.starts_with('"'), "a title with a comma must be quoted: {row}");
|
|
assert!(row.ends_with('"'));
|
|
// A quote inside a field doubles rather than escaping.
|
|
assert_eq!(row.matches("\"\"").count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn tsv_uses_tabs_and_a_chosen_column_order() {
|
|
use typikon::export::to_delimited;
|
|
let days = week(d("2026-04-12"), 1, resolve(Some("new")).unwrap());
|
|
let cols = vec!["fast".to_string(), "date".to_string()];
|
|
let text = to_delimited(&days, '\t', &cols);
|
|
let lines: Vec<&str> = text.trim_end().split('\n').collect();
|
|
assert_eq!(lines[0], "fast\tdate");
|
|
assert_eq!(lines[1], "none\t2026-04-12");
|
|
}
|
|
|
|
#[test]
|
|
fn an_unknown_column_is_caught_before_anything_is_printed() {
|
|
use typikon::export::csv::unknown_column;
|
|
assert!(unknown_column(&["date".to_string(), "fast".to_string()]).is_none());
|
|
assert_eq!(
|
|
unknown_column(&["date".to_string(), "nonsense".to_string()]).map(String::as_str),
|
|
Some("nonsense")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_advertised_column_actually_renders() {
|
|
use typikon::export::{csv::COLUMNS, to_delimited};
|
|
let days = week(d("2026-02-16"), 1, resolve(Some("ruthenian")).unwrap());
|
|
let cols: Vec<String> = COLUMNS.iter().map(|c| c.to_string()).collect();
|
|
let text = to_delimited(&days, '\t', &cols);
|
|
let lines: Vec<&str> = text.trim_end().split('\n').collect();
|
|
// Every column produces a cell, even the empty ones.
|
|
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::<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);
|
|
}
|