mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
The Sundays the calendar was not naming, and a calendar that keeps every day
Nine Sundays went unnamed in every jurisdiction. Three were missing from the movable cycle outright: the Paralytic, the Samaritan Woman and the Blind Man, so the fourth, fifth and sixth Sundays of Pascha reported only the saint of the fixed date. Six more are reckoned from a fixed feast rather than from Pascha — before and after the Exaltation, the Forefathers, before and after the Nativity, before and after the Theophany — and were not modelled at all. The three Pentecostarion Sundays rank with Thomas Sunday and the Myrrh-bearing Women, so a vigil-rank saint still keeps the day and the Sunday is commemorated on it. The six fixed-cycle ones never name a day: they are found in ecclesiastical space, so the old calendar and the new get them on the right morning, and they join the commemorations. Where a short window holds no Sunday the Typikon says what to do and this follows it; where the books disagree, the day goes unmarked rather than carrying an answer this calendar cannot defend. `week` says where a day falls in the count from Pentecost, which is how the books head the long ordinary stretch. It runs on across the new year and stops at the Triodion, whose Sundays have names instead of numbers. `--every-day` gives ics an event for every day, ferias included. The default still thins the calendar, which is right for a subscription and wrong for a reader who wants the saint of each morning; and the description now leads with the commemorations rather than burying them under the church date. Checked against 1.1.2 over every jurisdiction from 2015 to 2035: rank, feast and title move on the three new Sundays and nowhere else. No fast, obligation, binding or season changes on any day.
This commit is contained in:
@@ -4,6 +4,64 @@ Notable changes, in the terms [docs/versioning.md](docs/versioning.md) sets out.
|
|||||||
**Calendar** entries change what a day reports, whatever the version number
|
**Calendar** entries change what a day reports, whatever the version number
|
||||||
does.
|
does.
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
### Calendar
|
||||||
|
|
||||||
|
- **The Pentecostarion keeps all of its Sundays.** The Sunday of the Paralytic,
|
||||||
|
the Sunday of the Samaritan Woman and the Sunday of the Blind Man were absent
|
||||||
|
from the movable cycle altogether, so the third, fourth and fifth Sundays
|
||||||
|
after Thomas Sunday reported only the saint of the fixed date. Every
|
||||||
|
jurisdiction was affected. In 2027 that is 18 and 25 April and 2 May by the
|
||||||
|
Gregorian paschalion, 23 and 30 May and 6 June by the Julian one. They rank
|
||||||
|
with Thomas Sunday and the Myrrh-bearing Women, so a fixed feast of vigil
|
||||||
|
rank still names the day and the Sunday is commemorated on it: 25 April 2027
|
||||||
|
stays St Mark and gains the Samaritan Woman.
|
||||||
|
|
||||||
|
- **The Sundays reckoned from a fixed feast are kept.** Six more that the
|
||||||
|
calendar never named: the Sundays before and after the Exaltation, the Sunday
|
||||||
|
of the Holy Forefathers, the Sundays before and after the Nativity, and the
|
||||||
|
Sundays before and after the Theophany. They are found in ecclesiastical
|
||||||
|
space, so the old calendar and the new both get them on the right morning,
|
||||||
|
and they join the day's commemorations rather than displacing its saint.
|
||||||
|
|
||||||
|
Two years in the cycle leave a window without a Sunday in it, and the Typikon
|
||||||
|
says where those go: a Nativity on a Sunday puts the Sunday after it on 26
|
||||||
|
December, and a 1 January on a Sunday keeps the Sunday before the Theophany
|
||||||
|
with the Circumcision. The two remaining cases, the Theophany itself or the
|
||||||
|
Exaltation on a Sunday, go unmarked, because the books disagree about where
|
||||||
|
what belongs in them should land.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `week`: where the day falls in the count from Pentecost, worded as the books
|
||||||
|
head it — `the 32nd Sunday after Pentecost`, or `the 32nd week after
|
||||||
|
Pentecost` on the days between. Null through the Triodion and the
|
||||||
|
Pentecostarion, whose Sundays carry names instead of numbers. The count runs
|
||||||
|
on across the new year from the Pentecost that opened it, and stops at the
|
||||||
|
next Sunday of the Publican and the Pharisee. It is in the JSON, a column in
|
||||||
|
`csv` and `tsv`, a line in the single-day view, and a getter on every
|
||||||
|
binding.
|
||||||
|
|
||||||
|
- `--every-day`: one event per day in `--format ics`, ferias included. The
|
||||||
|
default still thins the calendar to the days carrying a feast or a fast,
|
||||||
|
which is right for a subscription and wrong for a reader who wants the saint
|
||||||
|
of each morning.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The iCalendar description leads with the day's commemorations, one to a line,
|
||||||
|
then the fast and its reason, then the season, the week and the church date.
|
||||||
|
They were last before, after the church date, and a client showing three
|
||||||
|
lines of a popover showed everything except what the day commemorates. The
|
||||||
|
summary now carries the rank's mark beside the fasting glyph, as the text
|
||||||
|
view has always done, and the fast is stated on every event rather than only
|
||||||
|
on the days that keep one.
|
||||||
|
|
||||||
|
- `typikon::export::to_ics_with` and `Ics` are the way to reach the new option
|
||||||
|
from the library. `to_ics` is unchanged and still writes the thinned
|
||||||
|
calendar.
|
||||||
|
|
||||||
## 1.1.2
|
## 1.1.2
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+1
-1
@@ -404,7 +404,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typikon"
|
name = "typikon"
|
||||||
version = "1.1.2"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "typikon"
|
name = "typikon"
|
||||||
version = "1.1.2"
|
version = "1.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic churches, across the Julian, Revised Julian and Gregorian reckonings."
|
description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic churches, across the Julian, Revised Julian and Gregorian reckonings."
|
||||||
|
|||||||
@@ -277,6 +277,16 @@ $ typikon --format json --days 7 # for a bar, a widget, a script
|
|||||||
$ typikon --format ics --days 365 > orthodox.ics
|
$ typikon --format ics --days 365 > orthodox.ics
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The calendar is thinned to the days worth marking: a subscription that fills
|
||||||
|
all 365 squares is one nobody keeps. `--every-day` is for the other kind of
|
||||||
|
reader, who wants the saint of each morning and does not mind a full year.
|
||||||
|
Either way the description carries the day's commemorations, the fast and its
|
||||||
|
reason, and where the day sits in the year.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ typikon -j ruthenian --days 467 --ics --every-day > byzantine.ics
|
||||||
|
```
|
||||||
|
|
||||||
Delimited output, for a spreadsheet or a pipeline. RFC 4180 quoting, one header
|
Delimited output, for a spreadsheet or a pipeline. RFC 4180 quoting, one header
|
||||||
row, one row per day:
|
row, one row per day:
|
||||||
|
|
||||||
@@ -309,6 +319,11 @@ $ typikon --year 2027 -j goarch
|
|||||||
…
|
…
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Outside the Triodion and the Pentecostarion, whose Sundays carry names, `week`
|
||||||
|
says where the day falls in the count from Pentecost: `the 32nd Sunday after
|
||||||
|
Pentecost`, or `the 32nd week after Pentecost` on the days between. It is a
|
||||||
|
column in the delimited formats and a line in the single-day view.
|
||||||
|
|
||||||
Every day of vigil rank or above carries the mark its books print beside it:
|
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`
|
⊕ 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.
|
field in the JSON and a column in the delimited formats.
|
||||||
@@ -332,6 +347,7 @@ $ typikon -j rocor --date 2026-03-18 --json --indent 2
|
|||||||
"churchDate": "5 March",
|
"churchDate": "5 March",
|
||||||
"season": "Great Fast",
|
"season": "Great Fast",
|
||||||
"cycle": null,
|
"cycle": null,
|
||||||
|
"week": null,
|
||||||
"feast": null,
|
"feast": null,
|
||||||
"rank": null,
|
"rank": null,
|
||||||
"sign": null,
|
"sign": null,
|
||||||
|
|||||||
+4
-1
@@ -21,13 +21,16 @@ tradition title commemorations fast fastLabel emoji reason obligation binds
|
|||||||
pascha daysToPascha julianOffset
|
pascha daysToPascha julianOffset
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`week` was added in 1.2.0.
|
||||||
|
|
||||||
`fastLabel`, `emoji` and `reason` are **presentation**: the fields are always
|
`fastLabel`, `emoji` and `reason` are **presentation**: the fields are always
|
||||||
there, and their wording is not stable. Key on `fast`, `season`, `rank`,
|
there, and their wording is not stable. Key on `fast`, `season`, `rank`,
|
||||||
`obligation` and `binds`.
|
`obligation` and `binds`.
|
||||||
|
|
||||||
Adding a field is a **minor** change, so read the object by key and ignore what
|
Adding a field is a **minor** change, so read the object by key and ignore what
|
||||||
you do not recognise. `season`, `cycle`, `feast`, `rank`, `sign` and
|
you do not recognise. `season`, `cycle`, `feast`, `rank`, `sign` and
|
||||||
`obligation` are null on ordinary days.
|
`obligation` are null on ordinary days, and `week` is null through the Triodion
|
||||||
|
and the Pentecostarion.
|
||||||
|
|
||||||
Two semantics worth stating because the names do not carry them:
|
Two semantics worth stating because the names do not carry them:
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const char *typikon_day_emoji(const typikon_day *day);
|
|||||||
const char *typikon_day_reason(const typikon_day *day);
|
const char *typikon_day_reason(const typikon_day *day);
|
||||||
const char *typikon_day_season(const typikon_day *day);
|
const char *typikon_day_season(const typikon_day *day);
|
||||||
const char *typikon_day_cycle(const typikon_day *day);
|
const char *typikon_day_cycle(const typikon_day *day);
|
||||||
|
const char *typikon_day_week(const typikon_day *day);
|
||||||
const char *typikon_day_feast(const typikon_day *day);
|
const char *typikon_day_feast(const typikon_day *day);
|
||||||
const char *typikon_day_rank(const typikon_day *day);
|
const char *typikon_day_rank(const typikon_day *day);
|
||||||
const char *typikon_day_sign(const typikon_day *day);
|
const char *typikon_day_sign(const typikon_day *day);
|
||||||
@@ -79,7 +80,7 @@ end
|
|||||||
local FIELDS = {
|
local FIELDS = {
|
||||||
"iso", "weekday", "jurisdiction", "calendar", "tradition", "church_date",
|
"iso", "weekday", "jurisdiction", "calendar", "tradition", "church_date",
|
||||||
"title", "fast", "fast_label", "emoji", "reason",
|
"title", "fast", "fast_label", "emoji", "reason",
|
||||||
"season", "cycle", "feast", "rank", "sign", "obligation", "binds",
|
"season", "cycle", "week", "feast", "rank", "sign", "obligation", "binds",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Copy the day into a plain Lua table and free it. The C day owns its strings
|
-- Copy the day into a plain Lua table and free it. The C day owns its strings
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ const char *typikon_day_reason(const typikon_day *day); /* why the day fas
|
|||||||
/* NULL where the day has no such thing, which is not an error. */
|
/* NULL where the day has no such thing, which is not an error. */
|
||||||
const char *typikon_day_season(const typikon_day *day); /* NULL in ordinary time */
|
const char *typikon_day_season(const typikon_day *day); /* NULL in ordinary time */
|
||||||
const char *typikon_day_cycle(const typikon_day *day); /* NULL outside the movable cycle */
|
const char *typikon_day_cycle(const typikon_day *day); /* NULL outside the movable cycle */
|
||||||
|
const char *typikon_day_week(const typikon_day *day); /* NULL outside the count from Pentecost */
|
||||||
const char *typikon_day_feast(const typikon_day *day); /* NULL if the day has none */
|
const char *typikon_day_feast(const typikon_day *day); /* NULL if the day has none */
|
||||||
const char *typikon_day_rank(const typikon_day *day); /* NULL if the day has no feast */
|
const char *typikon_day_rank(const typikon_day *day); /* NULL if the day has no feast */
|
||||||
const char *typikon_day_sign(const typikon_day *day); /* the mark the books print, else NULL */
|
const char *typikon_day_sign(const typikon_day *day); /* the mark the books print, else NULL */
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class Day:
|
|||||||
@property
|
@property
|
||||||
def cycle(self) -> str | None: ...
|
def cycle(self) -> str | None: ...
|
||||||
@property
|
@property
|
||||||
|
def week(self) -> str | None: ...
|
||||||
|
@property
|
||||||
def feast(self) -> str | None: ...
|
def feast(self) -> str | None: ...
|
||||||
@property
|
@property
|
||||||
def rank(self) -> str | None: ...
|
def rank(self) -> str | None: ...
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ pub struct typikon_day {
|
|||||||
reason: CString,
|
reason: CString,
|
||||||
season: Option<CString>,
|
season: Option<CString>,
|
||||||
cycle: Option<CString>,
|
cycle: Option<CString>,
|
||||||
|
week: Option<CString>,
|
||||||
feast: Option<CString>,
|
feast: Option<CString>,
|
||||||
rank: Option<CString>,
|
rank: Option<CString>,
|
||||||
sign: Option<CString>,
|
sign: Option<CString>,
|
||||||
@@ -223,6 +224,7 @@ fn build(
|
|||||||
reason: cs(&inner.reason),
|
reason: cs(&inner.reason),
|
||||||
season: inner.season.map(|s| cs(s.name())),
|
season: inner.season.map(|s| cs(s.name())),
|
||||||
cycle: inner.cycle.as_deref().map(&cs),
|
cycle: inner.cycle.as_deref().map(&cs),
|
||||||
|
week: inner.week.as_deref().map(&cs),
|
||||||
feast: inner.feast.as_deref().map(&cs),
|
feast: inner.feast.as_deref().map(&cs),
|
||||||
rank: inner.rank.map(|r| cs(r.key())),
|
rank: inner.rank.map(|r| cs(r.key())),
|
||||||
sign: inner.rank.map(|r| cs(r.sign())),
|
sign: inner.rank.map(|r| cs(r.sign())),
|
||||||
@@ -326,6 +328,7 @@ accessor!(typikon_day_emoji, "The glyph for the fasting level.", |d| Some(&d.emo
|
|||||||
accessor!(typikon_day_reason, "Why the day fasts as it does.", |d| Some(&d.reason));
|
accessor!(typikon_day_reason, "Why the day fasts as it does.", |d| Some(&d.reason));
|
||||||
accessor!(typikon_day_season, "The season, or NULL in ordinary time.", |d| d.season.as_ref());
|
accessor!(typikon_day_season, "The season, or NULL in ordinary time.", |d| d.season.as_ref());
|
||||||
accessor!(typikon_day_cycle, "The place in the movable cycle, or NULL outside it.", |d| d.cycle.as_ref());
|
accessor!(typikon_day_cycle, "The place in the movable cycle, or NULL outside it.", |d| d.cycle.as_ref());
|
||||||
|
accessor!(typikon_day_week, "The count of weeks from Pentecost, or NULL outside it.", |d| d.week.as_ref());
|
||||||
accessor!(typikon_day_feast, "The feast, or NULL if the day has none.", |d| d.feast.as_ref());
|
accessor!(typikon_day_feast, "The feast, or NULL if the day has none.", |d| d.feast.as_ref());
|
||||||
accessor!(typikon_day_rank, "The rank of the feast, or NULL if the day has none.", |d| d.rank.as_ref());
|
accessor!(typikon_day_rank, "The rank of the feast, or NULL if the day has none.", |d| d.rank.as_ref());
|
||||||
accessor!(typikon_day_sign, "The mark the books print beside a day of this rank, or NULL.", |d| d.sign.as_ref());
|
accessor!(typikon_day_sign, "The mark the books print beside a day of this rank, or NULL.", |d| d.sign.as_ref());
|
||||||
|
|||||||
+18
-1
@@ -11,7 +11,7 @@ use crate::calendars::{Date, Weekday, MONTHS};
|
|||||||
use crate::fasting::{Fast, Level, Obligation};
|
use crate::fasting::{Fast, Level, Obligation};
|
||||||
use crate::jurisdictions::Jurisdiction;
|
use crate::jurisdictions::Jurisdiction;
|
||||||
use crate::menologion::{self, Menologion, Rank};
|
use crate::menologion::{self, Menologion, Rank};
|
||||||
use crate::paschalion::{movable_cycle, Movables};
|
use crate::paschalion::{movable_cycle, week_after_pentecost_label, Movables};
|
||||||
use crate::rules::{self, Context};
|
use crate::rules::{self, Context};
|
||||||
use crate::seasons::season_for;
|
use crate::seasons::season_for;
|
||||||
|
|
||||||
@@ -25,6 +25,10 @@ pub struct Day {
|
|||||||
pub church_date: String,
|
pub church_date: String,
|
||||||
pub season: Option<crate::seasons::Season>,
|
pub season: Option<crate::seasons::Season>,
|
||||||
pub cycle: Option<String>,
|
pub cycle: Option<String>,
|
||||||
|
/// Where the day falls in the count of weeks from Pentecost, worded: "the
|
||||||
|
/// 32nd Sunday after Pentecost". `None` through the Triodion and the
|
||||||
|
/// Pentecostarion, whose Sundays have names instead of numbers.
|
||||||
|
pub week: Option<String>,
|
||||||
pub feast: Option<String>,
|
pub feast: Option<String>,
|
||||||
pub rank: Option<Rank>,
|
pub rank: Option<Rank>,
|
||||||
pub tradition: &'static str,
|
pub tradition: &'static str,
|
||||||
@@ -81,6 +85,7 @@ pub struct DayJson<'a> {
|
|||||||
pub church_date: &'a str,
|
pub church_date: &'a str,
|
||||||
pub season: Option<&'a str>,
|
pub season: Option<&'a str>,
|
||||||
pub cycle: Option<&'a str>,
|
pub cycle: Option<&'a str>,
|
||||||
|
pub week: 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.
|
/// The mark the books print beside a day of this rank.
|
||||||
@@ -109,6 +114,7 @@ impl Day {
|
|||||||
church_date: &self.church_date,
|
church_date: &self.church_date,
|
||||||
season: self.season.map(|s| s.name()),
|
season: self.season.map(|s| s.name()),
|
||||||
cycle: self.cycle.as_deref(),
|
cycle: self.cycle.as_deref(),
|
||||||
|
week: self.week.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()),
|
sign: self.rank.map(|r| r.sign()),
|
||||||
@@ -182,6 +188,16 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) -
|
|||||||
(None, fixed) => fixed.clone(),
|
(None, fixed) => fixed.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The Sundays counted from the Exaltation, the Nativity and the Theophany
|
||||||
|
// join the day rather than naming it: the books print the saint and the
|
||||||
|
// Sunday both, and the saint keeps the title.
|
||||||
|
if let Some(sunday) = menologion::fixed_cycle_sunday((fm, fd), date.weekday()) {
|
||||||
|
let sunday = sunday.to_string();
|
||||||
|
if !commemorations.contains(&sunday) && feast.as_deref() != Some(sunday.as_str()) {
|
||||||
|
commemorations.insert(1.min(commemorations.len()), sunday);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
civil: date,
|
civil: date,
|
||||||
fixed: (fm, fd),
|
fixed: (fm, fd),
|
||||||
@@ -228,6 +244,7 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) -
|
|||||||
church_date: format!("{} {}", fd, MONTHS[(fm - 1) as usize]),
|
church_date: format!("{} {}", fd, MONTHS[(fm - 1) as usize]),
|
||||||
season,
|
season,
|
||||||
cycle: entry.cycle,
|
cycle: entry.cycle,
|
||||||
|
week: week_after_pentecost_label(date, date.weekday(), jur.paschalion),
|
||||||
feast,
|
feast,
|
||||||
rank,
|
rank,
|
||||||
tradition: jur.tradition.key(),
|
tradition: jur.tradition.key(),
|
||||||
|
|||||||
+2
-1
@@ -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", "sign", "tradition", "title", "fast", "fast_label", "emoji",
|
"cycle", "week", "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",
|
||||||
];
|
];
|
||||||
@@ -33,6 +33,7 @@ fn cell(day: &Day, column: &str) -> String {
|
|||||||
"church_date" => day.church_date.clone(),
|
"church_date" => day.church_date.clone(),
|
||||||
"season" => day.season.map(|s| s.name()).unwrap_or("").to_string(),
|
"season" => day.season.map(|s| s.name()).unwrap_or("").to_string(),
|
||||||
"cycle" => day.cycle.clone().unwrap_or_default(),
|
"cycle" => day.cycle.clone().unwrap_or_default(),
|
||||||
|
"week" => day.week.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(),
|
"sign" => day.rank.map(|r| r.sign()).unwrap_or("").to_string(),
|
||||||
|
|||||||
+76
-31
@@ -1,14 +1,29 @@
|
|||||||
//! iCalendar, so the calendar can live where the rest of your year does.
|
//! iCalendar, so the calendar can live where the rest of your year does.
|
||||||
//!
|
//!
|
||||||
//! Each day becomes one all-day event. The summary is the feast or the
|
//! Each day becomes one all-day event. The summary is the feast or the
|
||||||
//! principal commemoration; the description carries the fast, the season and
|
//! principal commemoration; the description lists every commemoration, then the
|
||||||
//! the rest of the day's commemorations. Days with nothing to say, no feast and
|
//! fast, then where the day sits in the year.
|
||||||
//! no fast, are left out, because a subscribed calendar that marks all 365 days
|
//!
|
||||||
//! is one nobody keeps.
|
//! By default the days with nothing to say, no feast and no fast, are left out,
|
||||||
|
//! because a subscribed calendar that marks all 365 days is one nobody keeps.
|
||||||
|
//! `Ics::every_day` is for the other kind of reader, who wants the saint of
|
||||||
|
//! each morning and does not mind a full calendar: it gives every day an event,
|
||||||
|
//! ferial ones included.
|
||||||
|
|
||||||
use crate::day::Day;
|
use crate::day::Day;
|
||||||
use crate::fasting::{Level, Obligation};
|
use crate::fasting::{Level, Obligation};
|
||||||
|
|
||||||
|
/// How to write the calendar.
|
||||||
|
pub struct Ics<'a> {
|
||||||
|
/// What a client shows the subscription as.
|
||||||
|
pub name: &'a str,
|
||||||
|
/// `DTSTAMP`, as `YYYYMMDDTHHMMSSZ`.
|
||||||
|
pub stamp: &'a str,
|
||||||
|
/// One event for every day, rather than only the days carrying a feast or
|
||||||
|
/// a fast.
|
||||||
|
pub every_day: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// A count of octets, which is what RFC 5545 measures content lines in.
|
/// A count of octets, which is what RFC 5545 measures content lines in.
|
||||||
///
|
///
|
||||||
/// A newtype because the Python this was ported from folded on *characters*
|
/// A newtype because the Python this was ported from folded on *characters*
|
||||||
@@ -61,54 +76,75 @@ fn fold(line: &str) -> String {
|
|||||||
out.join("\r\n")
|
out.join("\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The calendar as it has always been written: a subscription, thinned to the
|
||||||
|
/// days worth marking.
|
||||||
pub fn to_ics(days: &[Day], name: &str, stamp: &str) -> String {
|
pub fn to_ics(days: &[Day], name: &str, stamp: &str) -> String {
|
||||||
|
to_ics_with(days, &Ics { name, stamp, every_day: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_ics_with(days: &[Day], opts: &Ics<'_>) -> String {
|
||||||
let mut lines: Vec<String> = vec![
|
let mut lines: Vec<String> = vec![
|
||||||
"BEGIN:VCALENDAR".into(),
|
"BEGIN:VCALENDAR".into(),
|
||||||
"VERSION:2.0".into(),
|
"VERSION:2.0".into(),
|
||||||
"PRODID:-//typikon//EN".into(),
|
"PRODID:-//typikon//EN".into(),
|
||||||
"CALSCALE:GREGORIAN".into(),
|
"CALSCALE:GREGORIAN".into(),
|
||||||
"METHOD:PUBLISH".into(),
|
"METHOD:PUBLISH".into(),
|
||||||
format!("X-WR-CALNAME:{}", escape(name)),
|
format!("X-WR-CALNAME:{}", escape(opts.name)),
|
||||||
];
|
];
|
||||||
|
|
||||||
for d in days {
|
for d in days {
|
||||||
if d.feast.is_none() && d.fast == Level::None {
|
if !opts.every_day && d.feast.is_none() && d.fast == Level::None {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let start = d.date.iso().replace('-', "");
|
let start = d.date.iso().replace('-', "");
|
||||||
let end = (d.date + 1).iso().replace('-', "");
|
let end = (d.date + 1).iso().replace('-', "");
|
||||||
let summary = if d.emoji.is_empty() {
|
// The glyph for the fast and the mark for the rank, in the order the
|
||||||
d.title().to_string()
|
// text view prints them, so one is scannable down a month view and the
|
||||||
} else {
|
// other says at a glance whether the day is kept.
|
||||||
format!("{} {}", d.emoji, d.title())
|
let sign = d.rank.map(|r| r.sign()).unwrap_or("");
|
||||||
};
|
let summary = [d.emoji, sign, d.title()]
|
||||||
|
.iter()
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
// Paragraphs, joined by a blank line. A calendar client shows this in
|
||||||
|
// a popover a few lines tall, and the saints are what the reader came
|
||||||
|
// for, so they go first.
|
||||||
let mut body: Vec<String> = Vec::new();
|
let mut body: Vec<String> = Vec::new();
|
||||||
if d.fast != Level::None {
|
if !d.commemorations.is_empty() {
|
||||||
body.push(if d.reason.is_empty() {
|
body.push(
|
||||||
format!("Fast: {}", d.fast_label)
|
d.commemorations
|
||||||
} else {
|
.iter()
|
||||||
format!("Fast: {} ({})", d.fast_label, d.reason)
|
.map(|c| format!("\u{2022} {c}"))
|
||||||
});
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut fast = if d.reason.is_empty() {
|
||||||
|
format!("Fast: {}", d.fast_label)
|
||||||
|
} else {
|
||||||
|
format!("Fast: {} ({})", d.fast_label, d.reason)
|
||||||
|
};
|
||||||
if d.obligation == Some(Obligation::Obligatory) {
|
if d.obligation == Some(Obligation::Obligatory) {
|
||||||
body.push("Obligatory.".into());
|
fast.push_str("\nObligatory.");
|
||||||
|
}
|
||||||
|
body.push(fast);
|
||||||
|
|
||||||
|
let mut where_ = Vec::new();
|
||||||
|
if let Some(w) = &d.week {
|
||||||
|
where_.push(capitalised(w));
|
||||||
}
|
}
|
||||||
if let Some(s) = d.season {
|
if let Some(s) = d.season {
|
||||||
body.push(format!("Season: {}", s.name()));
|
where_.push(format!("Season: {}", s.name()));
|
||||||
}
|
}
|
||||||
if let Some(c) = &d.cycle {
|
if let Some(c) = &d.cycle {
|
||||||
let mut chars = c.chars();
|
where_.push(capitalised(c));
|
||||||
let capped = match chars.next() {
|
|
||||||
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
body.push(capped);
|
|
||||||
}
|
|
||||||
body.push(format!("Church date: {}", d.church_date));
|
|
||||||
for c in &d.commemorations {
|
|
||||||
body.push(c.clone());
|
|
||||||
}
|
}
|
||||||
|
where_.push(format!("Church date: {}", d.church_date));
|
||||||
|
body.push(where_.join("\n"));
|
||||||
|
|
||||||
lines.extend([
|
lines.extend([
|
||||||
"BEGIN:VEVENT".into(),
|
"BEGIN:VEVENT".into(),
|
||||||
@@ -123,11 +159,11 @@ pub fn to_ics(days: &[Day], name: &str, stamp: &str) -> String {
|
|||||||
d.tradition,
|
d.tradition,
|
||||||
if d.shown_at_typikon { "typikon" } else { "particular" }
|
if d.shown_at_typikon { "typikon" } else { "particular" }
|
||||||
),
|
),
|
||||||
format!("DTSTAMP:{stamp}"),
|
format!("DTSTAMP:{}", opts.stamp),
|
||||||
format!("DTSTART;VALUE=DATE:{start}"),
|
format!("DTSTART;VALUE=DATE:{start}"),
|
||||||
format!("DTEND;VALUE=DATE:{end}"),
|
format!("DTEND;VALUE=DATE:{end}"),
|
||||||
fold(&format!("SUMMARY:{}", escape(&summary))),
|
fold(&format!("SUMMARY:{}", escape(&summary))),
|
||||||
fold(&format!("DESCRIPTION:{}", escape(&body.join("\n")))),
|
fold(&format!("DESCRIPTION:{}", escape(&body.join("\n\n")))),
|
||||||
"TRANSP:TRANSPARENT".into(),
|
"TRANSP:TRANSPARENT".into(),
|
||||||
"END:VEVENT".into(),
|
"END:VEVENT".into(),
|
||||||
]);
|
]);
|
||||||
@@ -138,3 +174,12 @@ pub fn to_ics(days: &[Day], name: &str, stamp: &str) -> String {
|
|||||||
s.push_str("\r\n");
|
s.push_str("\r\n");
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A phrase written to sit inside a sentence, promoted to the start of a line.
|
||||||
|
fn capitalised(phrase: &str) -> String {
|
||||||
|
let mut chars = phrase.chars();
|
||||||
|
match chars.next() {
|
||||||
|
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,5 +3,5 @@ pub mod ics;
|
|||||||
pub mod json;
|
pub mod json;
|
||||||
|
|
||||||
pub use csv::to_delimited;
|
pub use csv::to_delimited;
|
||||||
pub use ics::to_ics;
|
pub use ics::{to_ics, to_ics_with, Ics};
|
||||||
pub use json::to_json;
|
pub use json::to_json;
|
||||||
|
|||||||
+15
-2
@@ -5,7 +5,7 @@ use clap::{Parser, ValueEnum};
|
|||||||
use typikon::calendars::Date;
|
use typikon::calendars::Date;
|
||||||
use typikon::config::{self, Source};
|
use typikon::config::{self, Source};
|
||||||
use typikon::day::{week, year_of, Day};
|
use typikon::day::{week, year_of, Day};
|
||||||
use typikon::export::{to_delimited, to_ics, to_json};
|
use typikon::export::{to_delimited, to_ics_with, to_json, Ics};
|
||||||
use typikon::jurisdictions::{self, Jurisdiction, CALENDARS, JURISDICTIONS};
|
use typikon::jurisdictions::{self, Jurisdiction, CALENDARS, JURISDICTIONS};
|
||||||
use typikon::menaia;
|
use typikon::menaia;
|
||||||
use typikon::rules;
|
use typikon::rules;
|
||||||
@@ -67,6 +67,11 @@ struct Args {
|
|||||||
#[arg(long, conflicts_with = "format")]
|
#[arg(long, conflicts_with = "format")]
|
||||||
ics: bool,
|
ics: bool,
|
||||||
|
|
||||||
|
/// Give every day an event in ics, ferias included, rather than only the
|
||||||
|
/// days carrying a feast or a fast
|
||||||
|
#[arg(long)]
|
||||||
|
every_day: bool,
|
||||||
|
|
||||||
/// Leave the glyphs out, for a terminal or a bar without them
|
/// Leave the glyphs out, for a terminal or a bar without them
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
no_emoji: bool,
|
no_emoji: bool,
|
||||||
@@ -222,6 +227,9 @@ fn print_days(days: &[Day], jur: &Jurisdiction, source: Source, verbose: bool) {
|
|||||||
} else {
|
} else {
|
||||||
println!(" {} {sign}{}", d.emoji, d.title());
|
println!(" {} {sign}{}", d.emoji, d.title());
|
||||||
}
|
}
|
||||||
|
if let Some(w) = &d.week {
|
||||||
|
println!(" {w}");
|
||||||
|
}
|
||||||
if let Some(c) = &d.cycle {
|
if let Some(c) = &d.cycle {
|
||||||
println!(" {c}");
|
println!(" {c}");
|
||||||
}
|
}
|
||||||
@@ -443,7 +451,12 @@ fn run() -> Result<i32, String> {
|
|||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
Format::Ics => {
|
Format::Ics => {
|
||||||
print!("{}", to_ics(&days, &format!("{} \u{2014} Typikon", jur.name), &stamp()));
|
let opts = Ics {
|
||||||
|
name: &format!("{} \u{2014} Typikon", jur.name),
|
||||||
|
stamp: &stamp(),
|
||||||
|
every_day: args.every_day,
|
||||||
|
};
|
||||||
|
print!("{}", to_ics_with(&days, &opts));
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
Format::Csv | Format::Tsv => {
|
Format::Csv | Format::Tsv => {
|
||||||
|
|||||||
+48
-2
@@ -14,7 +14,7 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::calendars::Date;
|
use crate::calendars::{Date, Weekday};
|
||||||
use crate::jurisdictions::Tradition;
|
use crate::jurisdictions::Tradition;
|
||||||
use crate::paschalion::Movables;
|
use crate::paschalion::Movables;
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ pub fn for_jurisdiction(tradition: Tradition, menaia: &[&str]) -> Menologion {
|
|||||||
/// The movable feasts, named by their offset from Pascha. These outrank
|
/// The movable feasts, named by their offset from Pascha. These outrank
|
||||||
/// whatever the fixed calendar holds for the day.
|
/// whatever the fixed calendar holds for the day.
|
||||||
pub fn movable_feast_for(civil: Date, mv: &Movables) -> Option<(&'static str, Option<Rank>)> {
|
pub fn movable_feast_for(civil: Date, mv: &Movables) -> Option<(&'static str, Option<Rank>)> {
|
||||||
let table: [(Date, &'static str, Option<Rank>); 24] = [
|
let table: [(Date, &'static str, Option<Rank>); 27] = [
|
||||||
(mv.pascha, "The Radiant and Life-giving Resurrection of our Lord \u{2014} Holy Pascha", Some(Rank::Great)),
|
(mv.pascha, "The Radiant and Life-giving Resurrection of our Lord \u{2014} Holy Pascha", Some(Rank::Great)),
|
||||||
(mv.palm_sunday, "The Entry of our Lord into Jerusalem", Some(Rank::Great)),
|
(mv.palm_sunday, "The Entry of our Lord into Jerusalem", Some(Rank::Great)),
|
||||||
(mv.ascension, "The Ascension of our Lord, God and Saviour Jesus Christ", Some(Rank::Great)),
|
(mv.ascension, "The Ascension of our Lord, God and Saviour Jesus Christ", Some(Rank::Great)),
|
||||||
@@ -223,6 +223,9 @@ pub fn movable_feast_for(civil: Date, mv: &Movables) -> Option<(&'static str, Op
|
|||||||
(mv.holy_saturday, "Great and Holy Saturday", Some(Rank::Vigil)),
|
(mv.holy_saturday, "Great and Holy Saturday", Some(Rank::Vigil)),
|
||||||
(mv.thomas_sunday, "Thomas Sunday \u{2014} the Antipascha", Some(Rank::Polyeleos)),
|
(mv.thomas_sunday, "Thomas Sunday \u{2014} the Antipascha", Some(Rank::Polyeleos)),
|
||||||
(mv.myrrhbearers, "The Sunday of the Holy Myrrh-bearing Women", Some(Rank::Polyeleos)),
|
(mv.myrrhbearers, "The Sunday of the Holy Myrrh-bearing Women", Some(Rank::Polyeleos)),
|
||||||
|
(mv.paralytic, "The Sunday of the Paralytic", Some(Rank::Polyeleos)),
|
||||||
|
(mv.samaritan_woman, "The Sunday of the Samaritan Woman", Some(Rank::Polyeleos)),
|
||||||
|
(mv.blind_man, "The Sunday of the Blind Man", Some(Rank::Polyeleos)),
|
||||||
(mv.midfeast, "The Midfeast of Pentecost", Some(Rank::Polyeleos)),
|
(mv.midfeast, "The Midfeast of Pentecost", Some(Rank::Polyeleos)),
|
||||||
(mv.all_saints, "The Sunday of All Saints", Some(Rank::Polyeleos)),
|
(mv.all_saints, "The Sunday of All Saints", Some(Rank::Polyeleos)),
|
||||||
(mv.publican_pharisee, "The Sunday of the Publican and the Pharisee", None),
|
(mv.publican_pharisee, "The Sunday of the Publican and the Pharisee", None),
|
||||||
@@ -245,3 +248,46 @@ pub fn movable_feast_for(civil: Date, mv: &Movables) -> Option<(&'static str, Op
|
|||||||
.find(|(d, _, _)| *d == civil)
|
.find(|(d, _, _)| *d == civil)
|
||||||
.map(|(_, title, rank)| (*title, *rank))
|
.map(|(_, title, rank)| (*title, *rank))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The Sundays reckoned from a fixed feast rather than from Pascha.
|
||||||
|
///
|
||||||
|
/// Six of them, hung off the Exaltation, the Nativity and the Theophany. They
|
||||||
|
/// are named by the **ecclesiastical** date, so one table serves the old
|
||||||
|
/// calendar and the new alike, and they are found by the civil weekday,
|
||||||
|
/// because a Sunday is a Sunday in both reckonings.
|
||||||
|
///
|
||||||
|
/// These are commemorations, not feasts: they carry no rank and never displace
|
||||||
|
/// the saint of the day. The books print both, and on 26 December the Synaxis
|
||||||
|
/// of the Theotokos still names the day.
|
||||||
|
///
|
||||||
|
/// Three of the windows are shorter than a week and so can miss their Sunday
|
||||||
|
/// altogether:
|
||||||
|
///
|
||||||
|
/// - The Nativity on a Sunday leaves 26–31 December without one. The Typikon
|
||||||
|
/// settles this: the Sunday after the Nativity is kept on 26 December, and
|
||||||
|
/// that is the one day here that is not a Sunday.
|
||||||
|
/// - 1 January on a Sunday leaves 2–5 January without one, and the Sunday
|
||||||
|
/// before the Theophany is kept on 1 January, with the Circumcision.
|
||||||
|
/// - The Theophany itself on a Sunday, and the Exaltation on a Sunday, leave
|
||||||
|
/// their windows empty with nowhere agreed to put what belongs in them.
|
||||||
|
/// Those two years go unmarked: an answer this calendar cannot defend is
|
||||||
|
/// worse than none.
|
||||||
|
pub fn fixed_cycle_sunday(fixed: (u32, u32), weekday: Weekday) -> Option<&'static str> {
|
||||||
|
let sunday = weekday == Weekday::Sunday;
|
||||||
|
let title = match fixed {
|
||||||
|
(9, 8..=13) if sunday => "The Sunday before the Exaltation",
|
||||||
|
(9, 15..=21) if sunday => "The Sunday after the Exaltation",
|
||||||
|
(12, 11..=17) if sunday => "The Sunday of the Holy Forefathers",
|
||||||
|
(12, 18..=24) if sunday => "The Sunday before the Nativity \u{2014} the Holy Fathers",
|
||||||
|
(12, 26..=31) if sunday => "The Sunday after the Nativity",
|
||||||
|
// 26 December falls on a Monday only in a year whose Nativity was a
|
||||||
|
// Sunday, which is exactly the year the window above is empty.
|
||||||
|
(12, 26) if weekday == Weekday::Monday => "The Sunday after the Nativity",
|
||||||
|
// 2 to 5 January is the window. 1 January is a Sunday only in a year
|
||||||
|
// where that window holds none, so the two fold into one range.
|
||||||
|
(1, 1..=5) if sunday => "The Sunday before the Theophany",
|
||||||
|
(1, 7..=13) if sunday => "The Sunday after the Theophany",
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(title)
|
||||||
|
}
|
||||||
|
|||||||
+59
-1
@@ -9,7 +9,7 @@
|
|||||||
//! agree in some years, 2025 falls on 20 April by both, and can diverge by as
|
//! agree in some years, 2025 falls on 20 April by both, and can diverge by as
|
||||||
//! much as five weeks.
|
//! much as five weeks.
|
||||||
|
|
||||||
use crate::calendars::{Date, JulianDate};
|
use crate::calendars::{Date, JulianDate, Weekday, MAX_YEAR, MIN_YEAR};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum Paschalion {
|
pub enum Paschalion {
|
||||||
@@ -95,7 +95,10 @@ pub struct Movables {
|
|||||||
pub bright_week_end: Date,
|
pub bright_week_end: Date,
|
||||||
pub thomas_sunday: Date,
|
pub thomas_sunday: Date,
|
||||||
pub myrrhbearers: Date,
|
pub myrrhbearers: Date,
|
||||||
|
pub paralytic: Date,
|
||||||
pub midfeast: Date,
|
pub midfeast: Date,
|
||||||
|
pub samaritan_woman: Date,
|
||||||
|
pub blind_man: Date,
|
||||||
pub ascension: Date,
|
pub ascension: Date,
|
||||||
pub fathers_first_council: Date,
|
pub fathers_first_council: Date,
|
||||||
pub pentecost: Date,
|
pub pentecost: Date,
|
||||||
@@ -131,7 +134,10 @@ pub fn movable_cycle(year: i32, p: Paschalion) -> Movables {
|
|||||||
bright_week_end: d + 6,
|
bright_week_end: d + 6,
|
||||||
thomas_sunday: d + 7,
|
thomas_sunday: d + 7,
|
||||||
myrrhbearers: d + 14,
|
myrrhbearers: d + 14,
|
||||||
|
paralytic: d + 21,
|
||||||
midfeast: d + 24,
|
midfeast: d + 24,
|
||||||
|
samaritan_woman: d + 28,
|
||||||
|
blind_man: d + 35,
|
||||||
ascension: d + 39,
|
ascension: d + 39,
|
||||||
fathers_first_council: d + 42,
|
fathers_first_council: d + 42,
|
||||||
pentecost: d + 49,
|
pentecost: d + 49,
|
||||||
@@ -140,3 +146,55 @@ pub fn movable_cycle(year: i32, p: Paschalion) -> Movables {
|
|||||||
apostles_start: d + 57,
|
apostles_start: d + 57,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a day falls in the count of weeks from Pentecost, which is how the
|
||||||
|
/// books head the long ordinary stretch of the year: "the 32nd Sunday after
|
||||||
|
/// Pentecost".
|
||||||
|
///
|
||||||
|
/// The count runs from the Monday after Pentecost to the eve of the next
|
||||||
|
/// Sunday of the Publican and the Pharisee, and it is `None` outside that,
|
||||||
|
/// through the Triodion and the Pentecostarion, where the Sundays have names
|
||||||
|
/// of their own instead of a number.
|
||||||
|
///
|
||||||
|
/// The governing Pentecost is not always the one in this day's civil year: a
|
||||||
|
/// day in January is counted from the Pentecost of the year before. That is
|
||||||
|
/// the one place this differs from `days_to_pascha`, which is always reckoned
|
||||||
|
/// against its own year's Pascha.
|
||||||
|
pub fn week_after_pentecost(date: Date, p: Paschalion) -> Option<i32> {
|
||||||
|
let year = date.year();
|
||||||
|
// A day at either end of the range this calendar answers for would need a
|
||||||
|
// Pascha outside it. One week unlabelled beats a panic.
|
||||||
|
if !(MIN_YEAR + 1..=MAX_YEAR - 1).contains(&year) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let this = movable_cycle(year, p);
|
||||||
|
let (pentecost, next_publican) = if date > this.pentecost {
|
||||||
|
(this.pentecost, movable_cycle(year + 1, p).publican_pharisee)
|
||||||
|
} else {
|
||||||
|
(movable_cycle(year - 1, p).pentecost, this.publican_pharisee)
|
||||||
|
};
|
||||||
|
if date <= pentecost || date >= next_publican {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((date - pentecost + 6) / 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same count, worded. Lowercase and article-first, like `cycle`, because
|
||||||
|
/// both are phrases a front end drops into a sentence of its own.
|
||||||
|
pub fn week_after_pentecost_label(date: Date, weekday: Weekday, p: Paschalion) -> Option<String> {
|
||||||
|
let n = week_after_pentecost(date, p)?;
|
||||||
|
let unit = if weekday == Weekday::Sunday { "Sunday" } else { "week" };
|
||||||
|
Some(format!("the {} {unit} after Pentecost", ordinal(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// English ordinals, for the one place this calendar has to count out loud.
|
||||||
|
fn ordinal(n: i32) -> String {
|
||||||
|
let suffix = match (n % 100, n % 10) {
|
||||||
|
(11..=13, _) => "th",
|
||||||
|
(_, 1) => "st",
|
||||||
|
(_, 2) => "nd",
|
||||||
|
(_, 3) => "rd",
|
||||||
|
_ => "th",
|
||||||
|
};
|
||||||
|
format!("{n}{suffix}")
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ impl PyDay {
|
|||||||
self.inner.cycle.as_deref()
|
self.inner.cycle.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn week(&self) -> Option<&str> {
|
||||||
|
self.inner.week.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
#[getter]
|
#[getter]
|
||||||
fn feast(&self) -> Option<&str> {
|
fn feast(&self) -> Option<&str> {
|
||||||
self.inner.feast.as_deref()
|
self.inner.feast.as_deref()
|
||||||
@@ -203,6 +208,7 @@ impl PyDay {
|
|||||||
d.set_item("church_date", j.church_date)?;
|
d.set_item("church_date", j.church_date)?;
|
||||||
d.set_item("season", j.season)?;
|
d.set_item("season", j.season)?;
|
||||||
d.set_item("cycle", j.cycle)?;
|
d.set_item("cycle", j.cycle)?;
|
||||||
|
d.set_item("week", j.week)?;
|
||||||
d.set_item("feast", j.feast)?;
|
d.set_item("feast", j.feast)?;
|
||||||
d.set_item("rank", j.rank)?;
|
d.set_item("rank", j.rank)?;
|
||||||
d.set_item("sign", j.sign)?;
|
d.set_item("sign", j.sign)?;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export interface Day {
|
|||||||
season: string | null;
|
season: string | null;
|
||||||
/** The place in the movable cycle, or null outside it. */
|
/** The place in the movable cycle, or null outside it. */
|
||||||
cycle: string | null;
|
cycle: string | null;
|
||||||
|
/** "the 32nd Sunday after Pentecost", or null outside that count. */
|
||||||
|
week: string | null;
|
||||||
feast: string | null;
|
feast: string | null;
|
||||||
/** The rank of the feast, or null where there is none. */
|
/** The rank of the feast, or null where there is none. */
|
||||||
rank: string | null;
|
rank: string | null;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import sys
|
|||||||
|
|
||||||
FIELDS = [
|
FIELDS = [
|
||||||
"date", "weekday", "jurisdiction", "calendar", "churchDate", "season",
|
"date", "weekday", "jurisdiction", "calendar", "churchDate", "season",
|
||||||
"cycle", "feast", "rank", "sign", "tradition", "title", "commemorations",
|
"cycle", "week", "feast", "rank", "sign", "tradition", "title", "commemorations",
|
||||||
"fast", "fastLabel", "emoji", "reason", "obligation", "binds", "pascha",
|
"fast", "fastLabel", "emoji", "reason", "obligation", "binds", "pascha",
|
||||||
"daysToPascha", "julianOffset",
|
"daysToPascha", "julianOffset",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ int main(int argc, char **argv) {
|
|||||||
field(f, "churchDate", typikon_day_church_date(day), &first);
|
field(f, "churchDate", typikon_day_church_date(day), &first);
|
||||||
field(f, "season", typikon_day_season(day), &first);
|
field(f, "season", typikon_day_season(day), &first);
|
||||||
field(f, "cycle", typikon_day_cycle(day), &first);
|
field(f, "cycle", typikon_day_cycle(day), &first);
|
||||||
|
field(f, "week", typikon_day_week(day), &first);
|
||||||
field(f, "feast", typikon_day_feast(day), &first);
|
field(f, "feast", typikon_day_feast(day), &first);
|
||||||
field(f, "rank", typikon_day_rank(day), &first);
|
field(f, "rank", typikon_day_rank(day), &first);
|
||||||
field(f, "sign", typikon_day_sign(day), &first);
|
field(f, "sign", typikon_day_sign(day), &first);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ for (const [date, jurisdiction, typikon] of JSON.parse(readFileSync(probesPath,
|
|||||||
out[`${date}|${jurisdiction}|${typikon ? 1 : 0}`] = {
|
out[`${date}|${jurisdiction}|${typikon ? 1 : 0}`] = {
|
||||||
date: d.date, weekday: d.weekday, jurisdiction: d.jurisdiction,
|
date: d.date, weekday: d.weekday, jurisdiction: d.jurisdiction,
|
||||||
calendar: d.calendar, churchDate: d.churchDate, season: d.season,
|
calendar: d.calendar, churchDate: d.churchDate, season: d.season,
|
||||||
cycle: d.cycle, feast: d.feast, rank: d.rank, sign: d.sign,
|
cycle: d.cycle, week: d.week, feast: d.feast, rank: d.rank, sign: d.sign,
|
||||||
tradition: d.tradition, title: d.title, commemorations: d.commemorations,
|
tradition: d.tradition, title: d.title, commemorations: d.commemorations,
|
||||||
fast: d.fast, fastLabel: d.fastLabel, emoji: d.emoji, reason: d.reason,
|
fast: d.fast, fastLabel: d.fastLabel, emoji: d.emoji, reason: d.reason,
|
||||||
obligation: d.obligation, binds: d.binds, pascha: d.pascha,
|
obligation: d.obligation, binds: d.binds, pascha: d.pascha,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ local probes = {
|
|||||||
local fields = {
|
local fields = {
|
||||||
{"date","iso"}, {"weekday","weekday"}, {"jurisdiction","jurisdiction"},
|
{"date","iso"}, {"weekday","weekday"}, {"jurisdiction","jurisdiction"},
|
||||||
{"calendar","calendar"}, {"churchDate","church_date"}, {"season","season"},
|
{"calendar","calendar"}, {"churchDate","church_date"}, {"season","season"},
|
||||||
{"cycle","cycle"}, {"feast","feast"}, {"rank","rank"}, {"sign","sign"},
|
{"cycle","cycle"}, {"week","week"}, {"feast","feast"}, {"rank","rank"}, {"sign","sign"},
|
||||||
{"tradition","tradition"}, {"title","title"}, {"fast","fast"},
|
{"tradition","tradition"}, {"title","title"}, {"fast","fast"},
|
||||||
{"fastLabel","fast_label"}, {"emoji","emoji"}, {"reason","reason"},
|
{"fastLabel","fast_label"}, {"emoji","emoji"}, {"reason","reason"},
|
||||||
{"obligation","obligation"}, {"binds","binds"},
|
{"obligation","obligation"}, {"binds","binds"},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ for date, jurisdiction, tk in json.load(open(probes_path)):
|
|||||||
"churchDate": day.church_date,
|
"churchDate": day.church_date,
|
||||||
"season": day.season,
|
"season": day.season,
|
||||||
"cycle": day.cycle,
|
"cycle": day.cycle,
|
||||||
|
"week": day.week,
|
||||||
"feast": day.feast,
|
"feast": day.feast,
|
||||||
"rank": day.rank,
|
"rank": day.rank,
|
||||||
"sign": day.sign,
|
"sign": day.sign,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ for (const [date, jurisdiction, typikon] of JSON.parse(readFileSync(probesPath,
|
|||||||
out[`${date}|${jurisdiction}|${typikon ? 1 : 0}`] = {
|
out[`${date}|${jurisdiction}|${typikon ? 1 : 0}`] = {
|
||||||
date: d.date, weekday: d.weekday, jurisdiction: d.jurisdiction,
|
date: d.date, weekday: d.weekday, jurisdiction: d.jurisdiction,
|
||||||
calendar: d.calendar, churchDate: d.churchDate, season,
|
calendar: d.calendar, churchDate: d.churchDate, season,
|
||||||
cycle: d.cycle, feast: d.feast, rank: d.rank, sign: d.sign,
|
cycle: d.cycle, week: d.week, feast: d.feast, rank: d.rank, sign: d.sign,
|
||||||
tradition: d.tradition, title: d.title, commemorations,
|
tradition: d.tradition, title: d.title, commemorations,
|
||||||
fast: d.fast, fastLabel: d.fastLabel, emoji: d.emoji, reason: d.reason,
|
fast: d.fast, fastLabel: d.fastLabel, emoji: d.emoji, reason: d.reason,
|
||||||
obligation: d.obligation, binds: d.binds, pascha: d.pascha,
|
obligation: d.obligation, binds: d.binds, pascha: d.pascha,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function shape(d) {
|
|||||||
return {
|
return {
|
||||||
date: d.date, weekday: d.weekday, jurisdiction: d.jurisdiction,
|
date: d.date, weekday: d.weekday, jurisdiction: d.jurisdiction,
|
||||||
calendar: d.calendar, churchDate: d.churchDate, season: d.season,
|
calendar: d.calendar, churchDate: d.churchDate, season: d.season,
|
||||||
cycle: d.cycle, feast: d.feast, rank: d.rank, sign: d.sign,
|
cycle: d.cycle, week: d.week, feast: d.feast, rank: d.rank, sign: d.sign,
|
||||||
tradition: d.tradition, title: d.title, commemorations: d.commemorations,
|
tradition: d.tradition, title: d.title, commemorations: d.commemorations,
|
||||||
fast: d.fast, fastLabel: d.fastLabel, emoji: d.emoji, reason: d.reason,
|
fast: d.fast, fastLabel: d.fastLabel, emoji: d.emoji, reason: d.reason,
|
||||||
obligation: d.obligation, binds: d.binds, pascha: d.pascha,
|
obligation: d.obligation, binds: d.binds, pascha: d.pascha,
|
||||||
|
|||||||
+196
-2
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
use typikon::calendars::{Date, JulianDate, Weekday};
|
use typikon::calendars::{Date, JulianDate, Weekday};
|
||||||
use typikon::day::{describe, week, year_of};
|
use typikon::day::{describe, week, year_of};
|
||||||
use typikon::export::to_ics;
|
use typikon::export::{to_ics, to_ics_with, Ics};
|
||||||
use typikon::fasting::{Level, Obligation};
|
use typikon::fasting::{Level, Obligation};
|
||||||
use typikon::jurisdictions::{resolve, Calendar, MenologionKind, CALENDARS, JURISDICTIONS};
|
use typikon::jurisdictions::{resolve, Calendar, MenologionKind, CALENDARS, JURISDICTIONS};
|
||||||
use typikon::menologion::Rank;
|
use typikon::menologion::Rank;
|
||||||
use typikon::paschalion::{gregorian_pascha, julian_pascha, Paschalion};
|
use typikon::paschalion::{gregorian_pascha, julian_pascha, week_after_pentecost, Paschalion};
|
||||||
use typikon::seasons::Season;
|
use typikon::seasons::Season;
|
||||||
|
|
||||||
fn d(s: &str) -> Date {
|
fn d(s: &str) -> Date {
|
||||||
@@ -366,6 +366,200 @@ fn ics_is_well_formed() {
|
|||||||
assert_eq!(text.matches("BEGIN:VEVENT").count(), text.matches("END:VEVENT").count());
|
assert_eq!(text.matches("BEGIN:VEVENT").count(), text.matches("END:VEVENT").count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_day_gives_every_day_an_event() {
|
||||||
|
let days = week(d("2026-09-21"), 60, resolve(Some("ruthenian")).unwrap());
|
||||||
|
let thin = to_ics(&days, "Typikon", "20260101T000000Z");
|
||||||
|
let full = to_ics_with(
|
||||||
|
&days,
|
||||||
|
&Ics { name: "Typikon", stamp: "20260101T000000Z", every_day: true },
|
||||||
|
);
|
||||||
|
assert!(thin.matches("BEGIN:VEVENT").count() < 60, "the thinned calendar drops days");
|
||||||
|
assert_eq!(full.matches("BEGIN:VEVENT").count(), 60);
|
||||||
|
for line in full.split("\r\n") {
|
||||||
|
assert!(line.len() <= 75, "{} octets: {line}", line.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_event_carries_the_saints_and_the_fast() {
|
||||||
|
// A ferial Wednesday of the Great Fast: no feast, a fast that binds, and
|
||||||
|
// a saint the thinned calendar would keep but the old body did not name
|
||||||
|
// until after the church date.
|
||||||
|
let days = week(d("2027-03-17"), 1, resolve(Some("ruthenian")).unwrap());
|
||||||
|
let text = to_ics_with(
|
||||||
|
&days,
|
||||||
|
&Ics { name: "Typikon", stamp: "20260101T000000Z", every_day: true },
|
||||||
|
);
|
||||||
|
let unfolded = text.replace("\r\n ", "");
|
||||||
|
let description = unfolded
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.starts_with("DESCRIPTION:"))
|
||||||
|
.expect("an event with a description");
|
||||||
|
assert!(description.contains("\u{2022} Our Venerable Father Alexis the Man of God"),
|
||||||
|
"{description}");
|
||||||
|
assert!(description.contains("Fast: no meat"), "{description}");
|
||||||
|
assert!(description.contains("Obligatory."), "{description}");
|
||||||
|
assert!(description.contains("Church date: 17 March"), "{description}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────── the Sundays of the movable year ──────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_pentecostarion_names_all_of_its_sundays() {
|
||||||
|
// Pascha 2027 is 28 March by the Gregorian paschalion and 2 May by the
|
||||||
|
// Julian one, so the same Sundays are asked of both reckonings.
|
||||||
|
for (jur, pascha) in [("ruthenian", "2027-03-28"), ("rocor", "2027-05-02")] {
|
||||||
|
let pascha = d(pascha);
|
||||||
|
for (offset, name) in [
|
||||||
|
(7, "Thomas Sunday"),
|
||||||
|
(14, "The Sunday of the Holy Myrrh-bearing Women"),
|
||||||
|
(21, "The Sunday of the Paralytic"),
|
||||||
|
(28, "The Sunday of the Samaritan Woman"),
|
||||||
|
(35, "The Sunday of the Blind Man"),
|
||||||
|
(42, "The Sunday of the Fathers of the First Council"),
|
||||||
|
] {
|
||||||
|
let day = describe(pascha + offset, resolve(Some(jur)).unwrap());
|
||||||
|
assert_eq!(day.weekday, Weekday::Sunday, "{jur} +{offset}");
|
||||||
|
let named = day.title().starts_with(name)
|
||||||
|
|| day.commemorations.iter().any(|c| c.starts_with(name));
|
||||||
|
assert!(named, "{jur} {} ({}) does not name {name}: {:?}",
|
||||||
|
day.date, offset, day.commemorations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_vigil_rank_saint_keeps_the_day_and_the_sunday_joins_it() {
|
||||||
|
// 25 April 2027 is both the Sunday of the Samaritan Woman and St Mark,
|
||||||
|
// whose feast is of vigil rank. The apostle names the day; the Sunday is
|
||||||
|
// commemorated on it.
|
||||||
|
let day = at("2027-04-25", "ruthenian");
|
||||||
|
assert_eq!(day.title(), "The Holy Apostle and Evangelist Mark");
|
||||||
|
assert_eq!(day.rank, Some(Rank::Vigil));
|
||||||
|
assert!(day.commemorations.iter().any(|c| c == "The Sunday of the Samaritan Woman"),
|
||||||
|
"{:?}", day.commemorations);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_sundays_reckoned_from_a_fixed_feast_are_kept() {
|
||||||
|
for (date, jur, name) in [
|
||||||
|
("2026-09-13", "ruthenian", "The Sunday before the Exaltation"),
|
||||||
|
("2026-09-20", "ruthenian", "The Sunday after the Exaltation"),
|
||||||
|
("2026-12-13", "ruthenian", "The Sunday of the Holy Forefathers"),
|
||||||
|
("2026-12-20", "ruthenian", "The Sunday before the Nativity \u{2014} the Holy Fathers"),
|
||||||
|
("2026-12-27", "ruthenian", "The Sunday after the Nativity"),
|
||||||
|
("2027-01-03", "ruthenian", "The Sunday before the Theophany"),
|
||||||
|
("2027-01-10", "ruthenian", "The Sunday after the Theophany"),
|
||||||
|
// The old calendar keeps the same windows thirteen days later, which
|
||||||
|
// is the whole point of reckoning them in ecclesiastical space.
|
||||||
|
("2027-01-03", "moscow", "The Sunday before the Nativity \u{2014} the Holy Fathers"),
|
||||||
|
("2027-01-10", "moscow", "The Sunday after the Nativity"),
|
||||||
|
("2027-01-17", "moscow", "The Sunday before the Theophany"),
|
||||||
|
] {
|
||||||
|
let day = at(date, jur);
|
||||||
|
assert!(day.commemorations.iter().any(|c| c == name),
|
||||||
|
"{jur} {date}: {name} missing from {:?}", day.commemorations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_nativity_on_a_sunday_moves_the_sunday_after_it_to_the_26th() {
|
||||||
|
// 25 December 2033 is a Sunday, so 26–31 December holds none and the
|
||||||
|
// Typikon keeps the Sunday after the Nativity on the Monday.
|
||||||
|
assert_eq!(at("2033-12-25", "ruthenian").weekday, Weekday::Sunday);
|
||||||
|
let monday = at("2033-12-26", "ruthenian");
|
||||||
|
assert_eq!(monday.weekday, Weekday::Monday);
|
||||||
|
assert!(monday.commemorations.iter().any(|c| c == "The Sunday after the Nativity"),
|
||||||
|
"{:?}", monday.commemorations);
|
||||||
|
// And it is kept once, not twice.
|
||||||
|
for n in 1..=6 {
|
||||||
|
let day = describe(d("2033-12-26") + n, resolve(Some("ruthenian")).unwrap());
|
||||||
|
assert!(!day.commemorations.iter().any(|c| c == "The Sunday after the Nativity"),
|
||||||
|
"{}", day.date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_new_year_on_a_sunday_keeps_the_sunday_before_the_theophany_with_it() {
|
||||||
|
let day = at("2034-01-01", "ruthenian");
|
||||||
|
assert_eq!(day.weekday, Weekday::Sunday);
|
||||||
|
assert!(day.commemorations.iter().any(|c| c == "The Sunday before the Theophany"),
|
||||||
|
"{:?}", day.commemorations);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_sunday_of_the_year_is_named_twice() {
|
||||||
|
for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) {
|
||||||
|
for day in week(d("2024-01-01"), 366 * 12, jur) {
|
||||||
|
let mut seen = day.commemorations.clone();
|
||||||
|
seen.sort();
|
||||||
|
let before = seen.len();
|
||||||
|
seen.dedup();
|
||||||
|
assert_eq!(seen.len(), before, "{} {}: {:?}", jur.key, day.date, day.commemorations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── the count of weeks from Pentecost ─────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_weeks_after_pentecost_are_counted_from_all_saints() {
|
||||||
|
let p = Paschalion::Gregorian;
|
||||||
|
// Pascha 2026 is 5 April, so Pentecost is 24 May and All Saints 31 May.
|
||||||
|
assert_eq!(week_after_pentecost(d("2026-05-24"), p), None, "Pentecost itself");
|
||||||
|
assert_eq!(week_after_pentecost(d("2026-05-25"), p), Some(1), "the Monday after");
|
||||||
|
assert_eq!(week_after_pentecost(d("2026-05-31"), p), Some(1), "All Saints");
|
||||||
|
assert_eq!(week_after_pentecost(d("2026-06-07"), p), Some(2));
|
||||||
|
assert_eq!(week_after_pentecost(d("2026-09-27"), p), Some(18));
|
||||||
|
// The count does not restart at the new year: it runs on from last
|
||||||
|
// year's Pentecost until the Triodion opens.
|
||||||
|
assert_eq!(week_after_pentecost(d("2027-01-10"), p), Some(33));
|
||||||
|
assert_eq!(week_after_pentecost(d("2027-01-16"), p), Some(34));
|
||||||
|
// 17 January 2027 is the Publican and the Pharisee, and the Triodion
|
||||||
|
// names its own Sundays.
|
||||||
|
assert_eq!(week_after_pentecost(d("2027-01-17"), p), None);
|
||||||
|
assert_eq!(week_after_pentecost(d("2027-03-28"), p), None, "Pascha");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_week_is_worded_for_the_day_it_falls_on() {
|
||||||
|
assert_eq!(at("2026-09-27", "ruthenian").week.as_deref(),
|
||||||
|
Some("the 18th Sunday after Pentecost"));
|
||||||
|
assert_eq!(at("2026-09-28", "ruthenian").week.as_deref(),
|
||||||
|
Some("the 19th week after Pentecost"));
|
||||||
|
// The Pentecostarion and the Triodion carry names instead of numbers.
|
||||||
|
assert_eq!(at("2027-04-18", "ruthenian").week, None);
|
||||||
|
assert_eq!(at("2027-02-14", "ruthenian").week, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_week_count_never_skips_or_repeats_a_number() {
|
||||||
|
for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) {
|
||||||
|
// The walk starts mid-count, so the first run it sees is not a run
|
||||||
|
// that opened. Every later one is.
|
||||||
|
let mut last: Option<i32> = None;
|
||||||
|
let mut first_run = true;
|
||||||
|
for day in week(d("2025-01-01"), 366 * 6, jur) {
|
||||||
|
let Some(n) = week_after_pentecost(day.date, jur.paschalion) else {
|
||||||
|
if last.is_some() {
|
||||||
|
first_run = false;
|
||||||
|
}
|
||||||
|
last = None;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match last {
|
||||||
|
Some(prev) => assert!(n == prev || n == prev + 1,
|
||||||
|
"{} {}: {prev} then {n}", jur.key, day.date),
|
||||||
|
None if !first_run => assert_eq!(n, 1,
|
||||||
|
"{} {}: the count opens at one", jur.key, day.date),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
last = Some(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────── every jurisdiction ───────────────────────────
|
// ─────────────────────────── every jurisdiction ───────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user