mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
harden the edges the reviews found, and stop lending Kyiv Pittsburgh's saints
The years this calendar answers for are now 1 to 9999 and enforced at every constructor. Arithmetic saturates instead of wrapping, which in a release build had put a date in antiquity and produced an ISO string that was not ten characters long; a seven-day walk stops at the boundary rather than running past it. --days took any integer and aborted the process trying to allocate for it, as did --indent. Both are bounded now. --update-menaion parsed a day out of a fetched page with no bound on it and wrote keys like 01-4294967295 that no lookup could ever match, then wrote the file even when the parse had failed, in replace mode, which would have blanked a year of commemorations in a calendar that had been working. It bounds the day by the month and refuses to write when most of the year comes back empty. today() shelled out to date(1) and fell back to UTC without a word when it was absent, so a container reported the wrong day either side of midnight. It says so now. The UGCC loaded the Ruthenian menaion. That is the Metropolia of Pittsburgh's calendar of saints, and lending it to Kyiv is the same error as lending the law, in a release whose headline is that a church's law is never borrowed. Also: has_obligation was dead API about to be frozen, and two ptr::eq comparisons stood in for asking whether the discipline was overridden, which is what they meant.
This commit is contained in:
@@ -39,6 +39,25 @@ Everything below changes what a day reports against pre-1.0 versions.
|
||||
no longer report any day as binding. They had borrowed Ruthenian particular
|
||||
law, which is not their law.
|
||||
|
||||
### Hardened
|
||||
|
||||
- The years this calendar answers for are 1 to 9999, enforced. `Date::from_ymd`
|
||||
and `from_jdn` reject anything outside them, arithmetic saturates rather than
|
||||
wrapping, and a seven-day walk stops at the boundary instead of running past
|
||||
it into dates whose ISO form was not ten characters long.
|
||||
- `--days` is bounded at a hundred years and `--indent` at sixteen. Both took
|
||||
any integer and aborted the process on allocation.
|
||||
- `--update-menaion` bounds the day it parses by the month it is in, and
|
||||
refuses to write when most of the year comes back empty. The page is fetched,
|
||||
so its contents are not this program's to trust: an unbounded parse wrote
|
||||
keys no lookup could match, and a page redesign would have blanked a year of
|
||||
commemorations in a calendar that had been working.
|
||||
- `today()` says on stderr when it cannot read the local offset, rather than
|
||||
silently reporting UTC and so the wrong day either side of midnight.
|
||||
- The Ukrainian Greek Catholic Church no longer loads the Ruthenian calendar of
|
||||
saints. It is Pittsburgh's, and lending it was the same error as lending the
|
||||
law.
|
||||
|
||||
### Interfaces
|
||||
|
||||
- `binds` is null unless the day binds, rather than the string `"none"`. Most
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
use typikon::calendars::{Date, MAX_YEAR, MIN_YEAR};
|
||||
println!("MIN {:?} = {:?}", Date::MIN, Date::MIN.ymd());
|
||||
println!("MAX {:?} = {:?}", Date::MAX, Date::MAX.ymd());
|
||||
println!("from_ymd(1,1,1) {:?}", Date::from_ymd(MIN_YEAR, 1, 1).map(|d| d.iso()));
|
||||
println!("from_ymd(9999,12,31){:?}", Date::from_ymd(MAX_YEAR, 12, 31).map(|d| d.iso()));
|
||||
println!("from_ymd(10000,1,1) {:?}", Date::from_ymd(10000, 1, 1).map(|d| d.iso()));
|
||||
println!("from_ymd(0,1,1) {:?}", Date::from_ymd(0, 1, 1).map(|d| d.iso()));
|
||||
}
|
||||
+39
-6
@@ -24,6 +24,13 @@ use std::fmt;
|
||||
///
|
||||
/// A newtype over the JDN rather than a (y, m, d) triple, so that day
|
||||
/// arithmetic cannot go wrong at a month or year boundary.
|
||||
/// The years this calendar answers for. The lower bound is the first year the
|
||||
/// Julian reckoning is meaningful for a Christian calendar; the upper is the
|
||||
/// one `datetime.date` stops at, so the Python bindings cannot be handed a day
|
||||
/// they could not return.
|
||||
pub const MIN_YEAR: i32 = 1;
|
||||
pub const MAX_YEAR: i32 = 9999;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub struct Date(i32);
|
||||
|
||||
@@ -128,18 +135,32 @@ impl Date {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// A date from a Julian Day Number.
|
||||
pub fn from_jdn(jdn: i32) -> Date {
|
||||
Date(jdn)
|
||||
/// A date from a Julian Day Number, if it lands in the supported years.
|
||||
///
|
||||
/// Range-checked on the number itself, because `jdn_to_gregorian` adds a
|
||||
/// constant to it and would overflow on the way to finding out.
|
||||
pub fn from_jdn(jdn: i32) -> Option<Date> {
|
||||
(Date::MIN.0..=Date::MAX.0).contains(&jdn).then_some(Date(jdn))
|
||||
}
|
||||
|
||||
pub fn from_ymd(year: i32, month: u32, day: u32) -> Option<Date> {
|
||||
if !(1..=12).contains(&month) || day == 0 || day > days_in_month(year, month) {
|
||||
if !(MIN_YEAR..=MAX_YEAR).contains(&year)
|
||||
|| !(1..=12).contains(&month)
|
||||
|| day == 0
|
||||
|| day > days_in_month(year, month)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(Date(gregorian_to_jdn(year, month, day)))
|
||||
}
|
||||
|
||||
/// `days` later, or `None` past the end of the supported years. Ordinary
|
||||
/// `+` saturates at the boundary instead, so a walk off the end stops
|
||||
/// rather than wrapping into a year the rest of the program cannot hold.
|
||||
pub fn checked_add(self, days: i32) -> Option<Date> {
|
||||
self.0.checked_add(days).and_then(Date::from_jdn)
|
||||
}
|
||||
|
||||
pub fn ymd(self) -> (i32, u32, u32) {
|
||||
jdn_to_gregorian(self.0)
|
||||
}
|
||||
@@ -218,20 +239,32 @@ impl JulianDate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Saturating at the supported range, because a liturgical year is walked by
|
||||
/// adding days to it and an overflow there would wrap a date into antiquity.
|
||||
impl std::ops::Add<i32> for Date {
|
||||
type Output = Date;
|
||||
fn add(self, days: i32) -> Date {
|
||||
Date(self.0 + days)
|
||||
self.checked_add(days).unwrap_or(if days < 0 { Date::MIN } else { Date::MAX })
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<i32> for Date {
|
||||
type Output = Date;
|
||||
fn sub(self, days: i32) -> Date {
|
||||
Date(self.0 - days)
|
||||
self.0
|
||||
.checked_sub(days)
|
||||
.and_then(Date::from_jdn)
|
||||
.unwrap_or(if days > 0 { Date::MIN } else { Date::MAX })
|
||||
}
|
||||
}
|
||||
|
||||
impl Date {
|
||||
/// The first day this calendar answers for.
|
||||
pub const MIN: Date = Date(1721426);
|
||||
/// The last.
|
||||
pub const MAX: Date = Date(5373484);
|
||||
}
|
||||
|
||||
/// Days between two dates, which is a count and not a date.
|
||||
impl std::ops::Sub<Date> for Date {
|
||||
type Output = i32;
|
||||
|
||||
+7
-4
@@ -202,9 +202,10 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) -
|
||||
// one tradition answers both. When `discipline` names another, the level
|
||||
// and its reason come from that one and the obligation still comes from
|
||||
// this church's own law, because that is the part only it can say.
|
||||
let overridden = jur.discipline.is_some_and(|d| d != jur.rule);
|
||||
let shown = jur.discipline.map(rules::get).unwrap_or(rule);
|
||||
let mut fast = (shown.fast_for)(&ctx);
|
||||
if !std::ptr::eq(shown, rule) {
|
||||
if overridden {
|
||||
fast.obligation = match rule.obligation_for.and_then(|f| f(&ctx)) {
|
||||
Some((reason, binds)) => {
|
||||
fast.binds = Some(binds);
|
||||
@@ -237,18 +238,20 @@ pub fn describe_with(date: Date, jur: &'static Jurisdiction, men: &Menologion) -
|
||||
reason,
|
||||
obligation,
|
||||
binds,
|
||||
shown_at_typikon: !std::ptr::eq(shown, rule),
|
||||
shown_at_typikon: overridden,
|
||||
pascha: mv.pascha,
|
||||
days_to_pascha: mv.pascha - date,
|
||||
julian_offset: date.julian_offset(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `days` consecutive days from `start`.
|
||||
/// `days` consecutive days from `start`, stopping at the end of the years this
|
||||
/// calendar answers for rather than repeating the last of them.
|
||||
pub fn week(start: Date, days: i32, jur: &'static Jurisdiction) -> Vec<Day> {
|
||||
let men = menologion::for_jurisdiction(jur.tradition, jur.menaia);
|
||||
(0..days.max(1))
|
||||
.map(|n| describe_with(start + n, jur, &men))
|
||||
.map_while(|n| start.checked_add(n))
|
||||
.map(|date| describe_with(date, jur, &men))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@ pub const JURISDICTIONS: &[Jurisdiction] = &[
|
||||
// eastern catholic
|
||||
Jurisdiction { key: "ruthenian", name: "Byzantine Catholic Metropolia of Pittsburgh", paschalion: GregP, menologion: RevJul, rule: Ruth, discipline: None, tradition: Sl, menaia: RUTHENIAN_MENAIA,
|
||||
note: "Gregorian throughout. Fasting is reported under Ruthenian particular law, which binds far less than the Typikon prescribes." },
|
||||
Jurisdiction { key: "ugcc", name: "Ukrainian Greek Catholic Church", paschalion: JulP, menologion: RevJul, rule: Orth, discipline: None, 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. Its particular law on fasting is its own and is not modelled here, so the days are shown at the Typikon and nothing is reported as binding." },
|
||||
Jurisdiction { key: "ugcc", name: "Ukrainian Greek Catholic Church", paschalion: JulP, menologion: RevJul, rule: Orth, discipline: None, tradition: Sl, menaia: NO_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. Its particular law on fasting and its own calendar of saints are not modelled here, so the common menologion is used and nothing is reported as binding." },
|
||||
Jurisdiction { key: "melkite", name: "Melkite Greek Catholic Church", paschalion: GregP, menologion: RevJul, rule: Orth, discipline: None, 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. Its particular law on fasting is its own and is not modelled here, so the days are shown at the Typikon and nothing is reported as binding." },
|
||||
Jurisdiction { key: "romanian-catholic", name: "Romanian Greek Catholic Church", paschalion: GregP, menologion: RevJul, rule: Orth, discipline: None, tradition: Gk, menaia: NO_MENAIA,
|
||||
|
||||
+36
-27
@@ -52,7 +52,7 @@ struct Args {
|
||||
date: Option<String>,
|
||||
|
||||
/// How many days from the start date, inclusive
|
||||
#[arg(short = 'n', long, default_value_t = 1)]
|
||||
#[arg(short = 'n', long, default_value_t = 1, value_parser = clap::value_parser!(i32).range(1..=36600))]
|
||||
days: i32,
|
||||
|
||||
/// How to print it
|
||||
@@ -60,11 +60,11 @@ struct Args {
|
||||
format: Format,
|
||||
|
||||
/// Shorthand for --format json
|
||||
#[arg(long, conflicts_with = "ics")]
|
||||
#[arg(long, conflicts_with_all = ["ics", "format"])]
|
||||
json: bool,
|
||||
|
||||
/// Shorthand for --format ics
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "format")]
|
||||
ics: bool,
|
||||
|
||||
/// Leave the glyphs out, for a terminal or a bar without them
|
||||
@@ -80,8 +80,8 @@ struct Args {
|
||||
list_fields: bool,
|
||||
|
||||
/// Indent JSON output
|
||||
#[arg(long)]
|
||||
indent: Option<usize>,
|
||||
#[arg(long, value_parser = clap::value_parser!(u8).range(0..=16))]
|
||||
indent: Option<u8>,
|
||||
|
||||
/// Print a year's key dates and exit
|
||||
#[arg(long)]
|
||||
@@ -189,11 +189,9 @@ pub fn elide(text: &str, width: usize) -> String {
|
||||
/// How this run's reckoning reads, for the header of either view.
|
||||
fn describe_reckoning(jur: &Jurisdiction) -> String {
|
||||
let rule = rules::get(jur.rule);
|
||||
let shown = jur.discipline.map(rules::get).unwrap_or(rule);
|
||||
if std::ptr::eq(shown, rule) {
|
||||
rule.title.to_string()
|
||||
} else {
|
||||
format!("{}, obliging under {}", shown.title, rule.title)
|
||||
match jur.discipline.filter(|d| *d != jur.rule) {
|
||||
None => rule.title.to_string(),
|
||||
Some(d) => format!("{}, obliging under {}", rules::get(d).title, rule.title),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +439,7 @@ fn run() -> Result<i32, String> {
|
||||
|
||||
match format {
|
||||
Format::Json => {
|
||||
println!("{}", to_json(&days, args.indent));
|
||||
println!("{}", to_json(&days, args.indent.map(usize::from)));
|
||||
return Ok(0);
|
||||
}
|
||||
Format::Ics => {
|
||||
@@ -475,30 +473,41 @@ fn run() -> Result<i32, String> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Today, from the system clock, without pulling in a date library for it.
|
||||
/// Today, from the system clock, in local time.
|
||||
///
|
||||
/// The local offset comes from `date +%z`, because working it out from
|
||||
/// /etc/localtime means parsing TZif and this program would rather shell out
|
||||
/// than carry a time zone database. Where `date` is absent, as in a scratch
|
||||
/// container, the answer is UTC and it says so on stderr rather than quietly
|
||||
/// reporting the wrong day either side of midnight.
|
||||
fn today() -> Date {
|
||||
let secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0);
|
||||
// Local midnight, via the offset the system reports for now.
|
||||
let local = secs + local_offset_secs(secs);
|
||||
Date::from_jdn(2440588 + (local.div_euclid(86400)) as i32)
|
||||
let local = secs + local_offset_secs();
|
||||
// Unix epoch is JDN 2440588. The clock cannot reach either bound.
|
||||
Date::from_jdn(2440588 + local.div_euclid(86400) as i32).unwrap_or(Date::MIN)
|
||||
}
|
||||
|
||||
/// Seconds east of UTC, read from /etc/localtime through the `date` command
|
||||
/// only if TZ arithmetic is needed. Falls back to UTC.
|
||||
fn local_offset_secs(_now: i64) -> i64 {
|
||||
if let Ok(out) = std::process::Command::new("date").arg("+%z").output() {
|
||||
let s = String::from_utf8_lossy(&out.stdout);
|
||||
let s = s.trim();
|
||||
if s.len() == 5 {
|
||||
if let (Ok(h), Ok(m)) = (s[1..3].parse::<i64>(), s[3..5].parse::<i64>()) {
|
||||
let mag = h * 3600 + m * 60;
|
||||
return if s.starts_with('-') { -mag } else { mag };
|
||||
}
|
||||
}
|
||||
/// Seconds east of UTC, or None when it could not be determined.
|
||||
fn local_offset_secs() -> i64 {
|
||||
let Ok(out) = std::process::Command::new("date").arg("+%z").output() else {
|
||||
eprintln!("typikon: `date` is not on PATH; reckoning today in UTC");
|
||||
return 0;
|
||||
};
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
let s = text.trim();
|
||||
// "+HHMM". Checked as bytes so a short or multi-byte reply cannot panic a
|
||||
// slice, which a str index here would.
|
||||
let b = s.as_bytes();
|
||||
if b.len() == 5 && (b[0] == b'+' || b[0] == b'-') && b[1..].iter().all(u8::is_ascii_digit) {
|
||||
let h = (s[1..3].parse::<i64>()).unwrap_or(0);
|
||||
let m = (s[3..5].parse::<i64>()).unwrap_or(0);
|
||||
let mag = h * 3600 + m * 60;
|
||||
return if b[0] == b'-' { -mag } else { mag };
|
||||
}
|
||||
eprintln!("typikon: could not read the local time offset; reckoning today in UTC");
|
||||
0
|
||||
}
|
||||
|
||||
|
||||
+25
-1
@@ -97,10 +97,17 @@ fn month_index(word: &str) -> Option<u32> {
|
||||
}
|
||||
|
||||
/// Split a line into (month, day, rest) when it opens a dated entry.
|
||||
///
|
||||
/// The day is bounded by the month it is in. This page is fetched, so its
|
||||
/// contents are not under this program's control, and an unbounded parse here
|
||||
/// wrote keys like `01-4294967295` that no lookup could ever match.
|
||||
fn dated(line: &str) -> Option<(u32, u32, String)> {
|
||||
let mut it = line.splitn(3, ' ');
|
||||
let month = month_index(it.next()?)?;
|
||||
let day: u32 = it.next()?.trim_end_matches(['.', ',']).parse().ok()?;
|
||||
if day == 0 || day > crate::calendars::days_in_month(2024, month) {
|
||||
return None;
|
||||
}
|
||||
Some((month, day, it.next().unwrap_or("").trim().to_string()))
|
||||
}
|
||||
|
||||
@@ -198,7 +205,12 @@ pub fn update(name: &str) -> Result<Fetched, String> {
|
||||
if let Some((month, mut day, rest)) = dated(line) {
|
||||
flush(&cur, &mut buf, &mut out);
|
||||
if let Some((pm, pd)) = prev {
|
||||
if pm == month && day <= pd {
|
||||
// A date going backwards inside a month is the page's own
|
||||
// typo, corrected to follow the previous entry. Only where
|
||||
// that lands on a real day of the month: the source is remote,
|
||||
// and pd + 1 must not run off the end of it.
|
||||
let last = crate::calendars::days_in_month(2024, month);
|
||||
if pm == month && day <= pd && pd < last {
|
||||
corrections.push(format!("{month:02}-{day:02} -> {month:02}-{:02}", pd + 1));
|
||||
day = pd + 1;
|
||||
}
|
||||
@@ -235,6 +247,18 @@ pub fn update(name: &str) -> Result<Fetched, String> {
|
||||
payload.insert(k.clone(), v.clone());
|
||||
}
|
||||
|
||||
// A page redesign turns into an empty parse, and this set is written with
|
||||
// mode=replace: writing it anyway would quietly blank a year of
|
||||
// commemorations in someone's calendar. Refuse instead.
|
||||
if missing.len() > 30 {
|
||||
return Err(format!(
|
||||
"{} of 366 days came back empty, so {} has probably changed shape; \
|
||||
nothing written",
|
||||
missing.len(),
|
||||
source.url
|
||||
));
|
||||
}
|
||||
|
||||
let dir = crate::menologion::data_dir();
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let path = dir.join(format!("{name}.json"));
|
||||
|
||||
+3
-2
@@ -236,8 +236,9 @@ pub fn movable_feast_for(civil: Date, mv: &Movables) -> Option<(&'static str, Op
|
||||
(mv.mary_of_egypt, "The Sunday of St Mary of Egypt", None),
|
||||
(mv.fathers_first_council, "The Sunday of the Fathers of the First Council", None),
|
||||
(mv.clean_monday, "Clean Monday \u{2014} the first day of the Great Fast", None),
|
||||
// A sentinel that can never match, keeping the array a fixed size.
|
||||
(Date::from_jdn(i32::MIN), "", None),
|
||||
// A sentinel that can never match a real day, keeping the array a
|
||||
// fixed size. Pascha is never in January.
|
||||
(Date::MIN, "", None),
|
||||
];
|
||||
table
|
||||
.iter()
|
||||
|
||||
+3
-2
@@ -59,9 +59,10 @@ pub struct Rule {
|
||||
pub fast_for: fn(&Context) -> Fast,
|
||||
/// What this tradition's particular law binds, where it has one, asked
|
||||
/// separately from how the day is kept.
|
||||
/// What this tradition's particular law binds, where it has one. `None`
|
||||
/// where the tradition legislates no binding fast at all, which is every
|
||||
/// Orthodox jurisdiction: the fast is kept as tradition, not as precept.
|
||||
pub obligation_for: Option<fn(&Context) -> Binding>,
|
||||
/// Whether this tradition legislates the fast as precept at all.
|
||||
pub has_obligation: bool,
|
||||
}
|
||||
|
||||
pub fn get(kind: RuleKind) -> &'static Rule {
|
||||
|
||||
@@ -263,8 +263,7 @@ pub static RULE: Rule = Rule {
|
||||
title: "the Typikon",
|
||||
vocabulary: Vocabulary { label, emoji },
|
||||
fast_for,
|
||||
obligation_for: None,
|
||||
// The Orthodox Church does not legislate the fast as precept, so nothing
|
||||
// here is reported as binding. See the module docs.
|
||||
has_obligation: false,
|
||||
obligation_for: None,
|
||||
};
|
||||
|
||||
@@ -170,5 +170,4 @@ pub static RULE: Rule = Rule {
|
||||
vocabulary: Vocabulary { label, emoji },
|
||||
fast_for,
|
||||
obligation_for: Some(obligation_for),
|
||||
has_obligation: true,
|
||||
};
|
||||
|
||||
@@ -664,3 +664,45 @@ fn only_the_church_whose_law_was_sourced_reports_an_obligation() {
|
||||
.count();
|
||||
assert_eq!(binding, 14);
|
||||
}
|
||||
|
||||
// ───────────────────── the edges of the supported years ───────────────────
|
||||
|
||||
#[test]
|
||||
fn dates_outside_the_supported_years_cannot_be_made() {
|
||||
use typikon::calendars::{MAX_YEAR, MIN_YEAR};
|
||||
assert!(Date::from_ymd(MIN_YEAR, 1, 1).is_some());
|
||||
assert!(Date::from_ymd(MAX_YEAR, 12, 31).is_some());
|
||||
assert!(Date::from_ymd(MIN_YEAR - 1, 12, 31).is_none());
|
||||
assert!(Date::from_ymd(MAX_YEAR + 1, 1, 1).is_none());
|
||||
assert!(Date::from_ymd(2026, 2, 30).is_none());
|
||||
assert!(Date::from_jdn(i32::MAX).is_none());
|
||||
assert!(Date::from_jdn(i32::MIN).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_walk_off_the_end_stops_rather_than_wrapping() {
|
||||
let jur = resolve(Some("new")).unwrap();
|
||||
// Every date emitted is a real one, and the year never leaves the range.
|
||||
let days = week(d("9999-12-29"), 10, jur);
|
||||
assert_eq!(days.len(), 3, "the walk should stop at 9999-12-31");
|
||||
for day in &days {
|
||||
assert_eq!(day.date.iso().len(), 10, "{}", day.date);
|
||||
assert!(day.date.year() <= 9999);
|
||||
}
|
||||
// And the other end.
|
||||
assert_eq!(week(d("0001-01-01") , 3, jur).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_arithmetic_saturates_instead_of_overflowing() {
|
||||
// In a release build an unchecked add wraps silently, which put a date in
|
||||
// antiquity and an ISO string that was not ten characters long.
|
||||
assert_eq!(Date::MAX + 1, Date::MAX);
|
||||
assert_eq!(Date::MIN - 1, Date::MIN);
|
||||
assert_eq!(Date::MAX + i32::MAX, Date::MAX);
|
||||
assert_eq!(Date::MIN - i32::MAX, Date::MIN);
|
||||
assert!(Date::MAX.checked_add(1).is_none());
|
||||
for d in [Date::MIN, Date::MAX] {
|
||||
assert_eq!(d.iso().len(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user