1use crate::types::{Date, DateField, Edtf, Interval, IntervalEndpoint, Set, SetElement, YearKind};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct BoundDate {
34 pub year: i64,
36 pub month: u8,
38 pub day: u8,
40}
41
42impl core::fmt::Display for BoundDate {
43 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44 if self.year < 0 {
45 write!(f, "-{:04}-{:02}-{:02}", -self.year, self.month, self.day)
46 } else {
47 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub enum Bound {
56 NegativeInfinity,
58 Date(BoundDate),
60 PositiveInfinity,
62 Unknown,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct Bounds {
71 pub earliest: Bound,
73 pub latest: Bound,
75}
76
77impl Edtf {
78 #[must_use]
80 pub fn bounds(&self) -> Bounds {
81 match self {
82 Self::Date(d) => date_bounds(d),
83 Self::DateTime(dt) => date_bounds(&dt.date),
84 Self::Interval(iv) => interval_bounds(iv),
85 Self::Set(s) => set_bounds(s),
86 }
87 }
88}
89
90pub(crate) fn date_bounds(d: &Date) -> Bounds {
91 if !matches!(d.year.kind, YearKind::Standard { .. }) {
93 let Some(value) = d.year.value() else {
94 return Bounds {
95 earliest: Bound::Unknown,
96 latest: Bound::Unknown,
97 };
98 };
99 let Some((lo, hi)) =
100 significant_range(value, d.year.significant_digits, big_width(&d.year.kind))
101 else {
102 return Bounds {
103 earliest: Bound::Unknown,
104 latest: Bound::Unknown,
105 };
106 };
107 return Bounds {
108 earliest: Bound::Date(BoundDate {
109 year: lo,
110 month: 1,
111 day: 1,
112 }),
113 latest: Bound::Date(BoundDate {
114 year: hi,
115 month: 12,
116 day: 31,
117 }),
118 };
119 }
120
121 if d.year.significant_digits.is_some() {
123 let Some((lo, hi)) = d
126 .year
127 .value()
128 .and_then(|value| significant_range(value, d.year.significant_digits, 4))
129 else {
130 return Bounds {
131 earliest: Bound::Unknown,
132 latest: Bound::Unknown,
133 };
134 };
135 return Bounds {
136 earliest: Bound::Date(BoundDate {
137 year: lo,
138 month: 1,
139 day: 1,
140 }),
141 latest: Bound::Date(BoundDate {
142 year: hi,
143 month: 12,
144 day: 31,
145 }),
146 };
147 }
148
149 if let Some(code) = d.month.and_then(DateField::value) {
151 if (21..=41).contains(&code) {
152 return season_bounds(d, code);
153 }
154 }
155
156 let earliest = extremum(d, true);
157 let latest = extremum(d, false);
158 match (earliest, latest) {
159 (Some(a), Some(b)) => Bounds {
160 earliest: Bound::Date(a),
161 latest: Bound::Date(b),
162 },
163 _ => Bounds {
166 earliest: Bound::Unknown,
167 latest: Bound::Unknown,
168 },
169 }
170}
171
172pub(crate) fn big_width(kind: &YearKind) -> u32 {
174 match kind {
175 YearKind::Standard { .. } => 4,
176 YearKind::Big { value } => decimal_digits(value.unsigned_abs()),
177 YearKind::Exponential {
178 significand,
179 exponent,
180 } => decimal_digits(significand.unsigned_abs()) + exponent,
181 }
182}
183
184const fn decimal_digits(mut v: u64) -> u32 {
185 let mut n = 1;
186 while v >= 10 {
187 v /= 10;
188 n += 1;
189 }
190 n
191}
192
193pub(crate) fn significant_range(
199 value: i64,
200 precision: Option<u32>,
201 width: u32,
202) -> Option<(i64, i64)> {
203 let Some(p) = precision else {
204 return Some((value, value));
205 };
206 let sweep = width.saturating_sub(p);
207 let Some(modulus) = 10i64.checked_pow(sweep) else {
210 return Some((value, value));
211 };
212 let mag = i64::try_from(value.unsigned_abs()).ok()?;
213 let lo_mag = mag - mag.rem_euclid(modulus);
214 let hi_mag = lo_mag.checked_add(modulus - 1)?;
215 Some(if value < 0 {
216 (-hi_mag, -lo_mag)
217 } else {
218 (lo_mag, hi_mag)
219 })
220}
221
222fn season_months(code: u8) -> (u8, u8, bool) {
225 match code {
226 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),
231 34 => (4, 6, false),
232 35 => (7, 9, false),
233 36 => (10, 12, false),
234 37 => (1, 4, false),
235 38 => (5, 8, false),
236 39 => (9, 12, false),
237 40 => (1, 6, false),
238 41 => (7, 12, false),
239 _ => unreachable!("validated season code"),
240 }
241}
242
243fn season_bounds(d: &Date, code: u8) -> Bounds {
244 let (first, last, wraps) = season_months(code);
245 let (y_lo, y_hi) = year_range(d);
246 let end_year = if wraps { y_hi + 1 } else { y_hi };
247 Bounds {
248 earliest: Bound::Date(BoundDate {
249 year: y_lo,
250 month: first,
251 day: 1,
252 }),
253 latest: Bound::Date(BoundDate {
254 year: end_year,
255 month: last,
256 day: last_day(last, is_leap(end_year)),
257 }),
258 }
259}
260
261fn year_range(d: &Date) -> (i64, i64) {
265 match d.year.kind {
266 YearKind::Standard { negative, digits } => d.year.value().map_or_else(
267 || {
268 debug_assert!(!negative);
269 (year_value(digits, 0), year_value(digits, 9))
270 },
271 |v| (v, v),
272 ),
273 _ => unreachable!("caller checked Standard"),
274 }
275}
276
277fn year_value(digits: [Option<u8>; 4], fill: u8) -> i64 {
279 digits
280 .iter()
281 .fold(0, |acc, d| acc * 10 + i64::from(d.unwrap_or(fill)))
282}
283
284fn year_completions(digits: [Option<u8>; 4], ascending: bool) -> impl Iterator<Item = i64> {
288 let mut masked = [0usize; 4];
289 let mut n: u32 = 0;
290 for (i, d) in digits.iter().enumerate() {
291 if d.is_none() {
292 masked[n as usize] = i;
293 n += 1;
294 }
295 }
296 let count = 10u32.pow(n);
297 (0..count).map(move |i| {
298 let mut rem = if ascending { i } else { count - 1 - i };
299 let mut filled = digits.map(|d| d.unwrap_or(0));
300 for pos in masked[..n as usize].iter().rev() {
301 filled[*pos] = (rem % 10) as u8;
302 rem /= 10;
303 }
304 filled.iter().fold(0, |acc, d| acc * 10 + i64::from(*d))
305 })
306}
307
308fn extremum(d: &Date, ascending: bool) -> Option<BoundDate> {
315 if let YearKind::Standard { digits, .. } = d.year.kind {
318 if d.year.value().is_none() {
319 return year_completions(digits, ascending)
320 .find_map(|y| extremum_in_year(d, y, ascending));
321 }
322 }
323 let (y, _) = year_range(d);
324 extremum_in_year(d, y, ascending)
325}
326
327fn extremum_in_year(d: &Date, y: i64, ascending: bool) -> Option<BoundDate> {
330 let Some(month) = &d.month else {
331 return Some(if ascending {
332 BoundDate {
333 year: y,
334 month: 1,
335 day: 1,
336 }
337 } else {
338 BoundDate {
339 year: y,
340 month: 12,
341 day: 31,
342 }
343 });
344 };
345 let mut months = month_candidates_of(*month);
346 if !ascending {
347 months.reverse();
348 }
349 for m in months {
350 let Some(day) = &d.day else {
351 return Some(if ascending {
352 BoundDate {
353 year: y,
354 month: m,
355 day: 1,
356 }
357 } else {
358 BoundDate {
359 year: y,
360 month: m,
361 day: last_day(m, is_leap(y)),
362 }
363 });
364 };
365 let mut days = day_candidates_of(*day);
366 if !ascending {
367 days.reverse();
368 }
369 for dd in days {
370 if dd <= last_day(m, is_leap(y)) {
371 return Some(BoundDate {
372 year: y,
373 month: m,
374 day: dd,
375 });
376 }
377 }
378 }
379 None
380}
381
382pub(crate) fn month_candidates_of(f: DateField) -> alloc::vec::Vec<u8> {
383 f.value().map_or_else(
384 || (1..=12).filter(|v| field_matches(f, *v)).collect(),
385 |v| alloc::vec![v],
386 )
387}
388
389pub(crate) fn day_candidates_of(f: DateField) -> alloc::vec::Vec<u8> {
390 f.value().map_or_else(
391 || (1..=31).filter(|v| field_matches(f, *v)).collect(),
392 |v| alloc::vec![v],
393 )
394}
395
396fn field_matches(f: DateField, v: u8) -> bool {
397 f.digits[0].is_none_or(|p| p == v / 10) && f.digits[1].is_none_or(|p| p == v % 10)
398}
399
400pub(crate) const fn is_leap(y: i64) -> bool {
402 y.rem_euclid(4) == 0 && (y.rem_euclid(100) != 0 || y.rem_euclid(400) == 0)
403}
404
405pub(crate) fn last_day(month: u8, leap: bool) -> u8 {
406 match month {
407 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
408 4 | 6 | 9 | 11 => 30,
409 2 => {
410 if leap {
411 29
412 } else {
413 28
414 }
415 },
416 _ => unreachable!("month is 1-12"),
417 }
418}
419
420fn interval_bounds(iv: &Interval) -> Bounds {
421 let earliest = match &iv.start {
422 IntervalEndpoint::Open | IntervalEndpoint::OnOrBefore(_) => Bound::NegativeInfinity,
423 IntervalEndpoint::Unknown => Bound::Unknown,
424 IntervalEndpoint::Date(d) | IntervalEndpoint::OnOrAfter(d) => date_bounds(d).earliest,
425 };
426 let latest = match &iv.end {
427 IntervalEndpoint::Open | IntervalEndpoint::OnOrAfter(_) => Bound::PositiveInfinity,
428 IntervalEndpoint::Unknown => Bound::Unknown,
429 IntervalEndpoint::Date(d) | IntervalEndpoint::OnOrBefore(d) => date_bounds(d).latest,
430 };
431 Bounds { earliest, latest }
432}
433
434fn set_bounds(s: &Set) -> Bounds {
435 let mut earliest: Option<Bound> = None;
436 let mut latest: Option<Bound> = None;
437 for element in &s.elements {
438 let (e, l) = match element {
439 SetElement::Date(d) => {
440 let b = date_bounds(d);
441 (b.earliest, b.latest)
442 },
443 SetElement::OnOrBefore(d) => (Bound::NegativeInfinity, date_bounds(d).latest),
444 SetElement::OnOrAfter(d) => (date_bounds(d).earliest, Bound::PositiveInfinity),
445 SetElement::Range(a, b) => (date_bounds(a).earliest, date_bounds(b).latest),
446 };
447 earliest = Some(earliest.map_or(e, |cur| min_bound(cur, e)));
448 latest = Some(latest.map_or(l, |cur| max_bound(cur, l)));
449 }
450 Bounds {
451 earliest: earliest.unwrap_or(Bound::Unknown),
452 latest: latest.unwrap_or(Bound::Unknown),
453 }
454}
455
456fn min_bound(a: Bound, b: Bound) -> Bound {
457 match (a, b) {
458 (Bound::NegativeInfinity, _) | (_, Bound::NegativeInfinity) => Bound::NegativeInfinity,
459 (Bound::Unknown, _) | (_, Bound::Unknown) => Bound::Unknown,
460 (Bound::PositiveInfinity, x) | (x, Bound::PositiveInfinity) => x,
463 (Bound::Date(x), Bound::Date(y)) => Bound::Date(x.min(y)),
464 }
465}
466
467fn max_bound(a: Bound, b: Bound) -> Bound {
468 match (a, b) {
469 (Bound::PositiveInfinity, _) | (_, Bound::PositiveInfinity) => Bound::PositiveInfinity,
470 (Bound::Unknown, _) | (_, Bound::Unknown) => Bound::Unknown,
471 (Bound::NegativeInfinity, x) | (x, Bound::NegativeInfinity) => x,
474 (Bound::Date(x), Bound::Date(y)) => Bound::Date(x.max(y)),
475 }
476}