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