use crate::types::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BoundDate {
pub year: i64,
pub month: u8,
pub day: u8,
}
impl core::fmt::Display for BoundDate {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.year < 0 {
write!(f, "-{:04}-{:02}-{:02}", -self.year, self.month, self.day)
} else {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Bound {
NegativeInfinity,
Date(BoundDate),
PositiveInfinity,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bounds {
pub earliest: Bound,
pub latest: Bound,
}
impl Edtf {
pub fn bounds(&self) -> Bounds {
match self {
Edtf::Date(d) => date_bounds(d),
Edtf::DateTime(dt) => date_bounds(&dt.date),
Edtf::Interval(iv) => interval_bounds(iv),
Edtf::Set(s) => set_bounds(s),
}
}
}
pub(crate) fn date_bounds(d: &Date) -> Bounds {
if !matches!(d.year.kind, YearKind::Standard { .. }) {
let Some(value) = d.year.value() else {
return Bounds {
earliest: Bound::Unknown,
latest: Bound::Unknown,
};
};
let Some((lo, hi)) =
significant_range(value, d.year.significant_digits, big_width(&d.year.kind))
else {
return Bounds {
earliest: Bound::Unknown,
latest: Bound::Unknown,
};
};
return Bounds {
earliest: Bound::Date(BoundDate {
year: lo,
month: 1,
day: 1,
}),
latest: Bound::Date(BoundDate {
year: hi,
month: 12,
day: 31,
}),
};
}
if d.year.significant_digits.is_some() {
let value = d.year.value().expect("S excludes X digits");
let (lo, hi) = significant_range(value, d.year.significant_digits, 4)
.expect("four-digit sweep fits in i64");
return Bounds {
earliest: Bound::Date(BoundDate {
year: lo,
month: 1,
day: 1,
}),
latest: Bound::Date(BoundDate {
year: hi,
month: 12,
day: 31,
}),
};
}
if let Some(code) = d.month.as_ref().and_then(DateField::value) {
if (21..=41).contains(&code) {
return season_bounds(d, code);
}
}
let earliest = extremum(d, true);
let latest = extremum(d, false);
match (earliest, latest) {
(Some(a), Some(b)) => Bounds {
earliest: Bound::Date(a),
latest: Bound::Date(b),
},
_ => Bounds {
earliest: Bound::Unknown,
latest: Bound::Unknown,
},
}
}
fn big_width(kind: &YearKind) -> u32 {
match kind {
YearKind::Standard { .. } => 4,
YearKind::Big { value } => decimal_digits(value.unsigned_abs()),
YearKind::Exponential {
significand,
exponent,
} => decimal_digits(significand.unsigned_abs()) + exponent,
}
}
fn decimal_digits(mut v: u64) -> u32 {
let mut n = 1;
while v >= 10 {
v /= 10;
n += 1;
}
n
}
fn significant_range(value: i64, precision: Option<u32>, width: u32) -> Option<(i64, i64)> {
let Some(p) = precision else {
return Some((value, value));
};
let sweep = width.saturating_sub(p);
let Some(modulus) = 10i64.checked_pow(sweep) else {
return Some((value, value));
};
let mag = value.unsigned_abs() as i64;
let lo_mag = mag - mag.rem_euclid(modulus);
let hi_mag = lo_mag.checked_add(modulus - 1)?;
Some(if value < 0 {
(-hi_mag, -lo_mag)
} else {
(lo_mag, hi_mag)
})
}
fn season_months(code: u8) -> (u8, u8, bool) {
match code {
21 | 25 | 31 => (3, 5, false), 22 | 26 | 32 => (6, 8, false), 23 | 27 | 29 => (9, 11, false), 24 | 28 | 30 => (12, 2, true), 33 => (1, 3, false),
34 => (4, 6, false),
35 => (7, 9, false),
36 => (10, 12, false),
37 => (1, 4, false),
38 => (5, 8, false),
39 => (9, 12, false),
40 => (1, 6, false),
41 => (7, 12, false),
_ => unreachable!("validated season code"),
}
}
fn season_bounds(d: &Date, code: u8) -> Bounds {
let (first, last, wraps) = season_months(code);
let (y_lo, y_hi) = year_range(d);
let end_year = if wraps { y_hi + 1 } else { y_hi };
Bounds {
earliest: Bound::Date(BoundDate {
year: y_lo,
month: first,
day: 1,
}),
latest: Bound::Date(BoundDate {
year: end_year,
month: last,
day: last_day(last, is_leap(end_year)),
}),
}
}
fn year_range(d: &Date) -> (i64, i64) {
match d.year.kind {
YearKind::Standard { negative, digits } => {
if let Some(v) = d.year.value() {
(v, v)
} else {
debug_assert!(!negative);
(year_value(digits, 0), year_value(digits, 9))
}
}
_ => unreachable!("caller checked Standard"),
}
}
fn year_value(digits: [Option<u8>; 4], fill: u8) -> i64 {
digits
.iter()
.fold(0, |acc, d| acc * 10 + i64::from(d.unwrap_or(fill)))
}
fn year_completions(digits: [Option<u8>; 4], ascending: bool) -> impl Iterator<Item = i64> {
let mut masked = [0usize; 4];
let mut n = 0;
for (i, d) in digits.iter().enumerate() {
if d.is_none() {
masked[n] = i;
n += 1;
}
}
let count = 10u32.pow(n as u32);
(0..count).map(move |i| {
let mut rem = if ascending { i } else { count - 1 - i };
let mut filled = digits.map(|d| d.unwrap_or(0));
for pos in masked[..n].iter().rev() {
filled[*pos] = (rem % 10) as u8;
rem /= 10;
}
filled.iter().fold(0, |acc, d| acc * 10 + i64::from(*d))
})
}
fn extremum(d: &Date, ascending: bool) -> Option<BoundDate> {
if let YearKind::Standard { digits, .. } = d.year.kind {
if d.year.value().is_none() {
return year_completions(digits, ascending)
.find_map(|y| extremum_in_year(d, y, ascending));
}
}
let (y, _) = year_range(d);
extremum_in_year(d, y, ascending)
}
fn extremum_in_year(d: &Date, y: i64, ascending: bool) -> Option<BoundDate> {
let Some(month) = &d.month else {
return Some(if ascending {
BoundDate {
year: y,
month: 1,
day: 1,
}
} else {
BoundDate {
year: y,
month: 12,
day: 31,
}
});
};
let mut months = month_candidates_of(month);
if !ascending {
months.reverse();
}
for m in months {
let Some(day) = &d.day else {
return Some(if ascending {
BoundDate {
year: y,
month: m,
day: 1,
}
} else {
BoundDate {
year: y,
month: m,
day: last_day(m, is_leap(y)),
}
});
};
let mut days = day_candidates_of(day);
if !ascending {
days.reverse();
}
for dd in days {
if dd <= last_day(m, is_leap(y)) {
return Some(BoundDate {
year: y,
month: m,
day: dd,
});
}
}
}
None
}
fn month_candidates_of(f: &DateField) -> alloc::vec::Vec<u8> {
match f.value() {
Some(v) => alloc::vec![v],
None => (1..=12).filter(|v| field_matches(f, *v)).collect(),
}
}
fn day_candidates_of(f: &DateField) -> alloc::vec::Vec<u8> {
match f.value() {
Some(v) => alloc::vec![v],
None => (1..=31).filter(|v| field_matches(f, *v)).collect(),
}
}
fn field_matches(f: &DateField, v: u8) -> bool {
f.digits[0].is_none_or(|p| p == v / 10) && f.digits[1].is_none_or(|p| p == v % 10)
}
pub(crate) fn is_leap(y: i64) -> bool {
y.rem_euclid(4) == 0 && (y.rem_euclid(100) != 0 || y.rem_euclid(400) == 0)
}
pub(crate) fn last_day(month: u8, leap: bool) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if leap {
29
} else {
28
}
}
_ => unreachable!("month is 1-12"),
}
}
fn interval_bounds(iv: &Interval) -> Bounds {
let earliest = match &iv.start {
IntervalEndpoint::Open | IntervalEndpoint::OnOrBefore(_) => Bound::NegativeInfinity,
IntervalEndpoint::Unknown => Bound::Unknown,
IntervalEndpoint::Date(d) | IntervalEndpoint::OnOrAfter(d) => date_bounds(d).earliest,
};
let latest = match &iv.end {
IntervalEndpoint::Open | IntervalEndpoint::OnOrAfter(_) => Bound::PositiveInfinity,
IntervalEndpoint::Unknown => Bound::Unknown,
IntervalEndpoint::Date(d) | IntervalEndpoint::OnOrBefore(d) => date_bounds(d).latest,
};
Bounds { earliest, latest }
}
fn set_bounds(s: &Set) -> Bounds {
let mut earliest: Option<Bound> = None;
let mut latest: Option<Bound> = None;
for element in &s.elements {
let (e, l) = match element {
SetElement::Date(d) => {
let b = date_bounds(d);
(b.earliest, b.latest)
}
SetElement::OnOrBefore(d) => (Bound::NegativeInfinity, date_bounds(d).latest),
SetElement::OnOrAfter(d) => (date_bounds(d).earliest, Bound::PositiveInfinity),
SetElement::Range(a, b) => (date_bounds(a).earliest, date_bounds(b).latest),
};
earliest = Some(match earliest {
None => e,
Some(cur) => min_bound(cur, e),
});
latest = Some(match latest {
None => l,
Some(cur) => max_bound(cur, l),
});
}
Bounds {
earliest: earliest.unwrap_or(Bound::Unknown),
latest: latest.unwrap_or(Bound::Unknown),
}
}
fn min_bound(a: Bound, b: Bound) -> Bound {
match (a, b) {
(Bound::NegativeInfinity, _) | (_, Bound::NegativeInfinity) => Bound::NegativeInfinity,
(Bound::Unknown, _) | (_, Bound::Unknown) => Bound::Unknown,
(Bound::PositiveInfinity, x) | (x, Bound::PositiveInfinity) => x,
(Bound::Date(x), Bound::Date(y)) => Bound::Date(x.min(y)),
}
}
fn max_bound(a: Bound, b: Bound) -> Bound {
match (a, b) {
(Bound::PositiveInfinity, _) | (_, Bound::PositiveInfinity) => Bound::PositiveInfinity,
(Bound::Unknown, _) | (_, Bound::Unknown) => Bound::Unknown,
(Bound::NegativeInfinity, x) | (x, Bound::NegativeInfinity) => x,
(Bound::Date(x), Bound::Date(y)) => Bound::Date(x.max(y)),
}
}