1use crate::types::*;
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 pub fn bounds(&self) -> Bounds {
77 match self {
78 Edtf::Date(d) => date_bounds(d),
79 Edtf::DateTime(dt) => date_bounds(&dt.date),
80 Edtf::Interval(iv) => interval_bounds(iv),
81 Edtf::Set(s) => set_bounds(s),
82 }
83 }
84}
85
86pub(crate) fn date_bounds(d: &Date) -> Bounds {
87 if !matches!(d.year.kind, YearKind::Standard { .. }) {
89 let Some(value) = d.year.value() else {
90 return Bounds {
91 earliest: Bound::Unknown,
92 latest: Bound::Unknown,
93 };
94 };
95 let Some((lo, hi)) =
96 significant_range(value, d.year.significant_digits, big_width(&d.year.kind))
97 else {
98 return Bounds {
99 earliest: Bound::Unknown,
100 latest: Bound::Unknown,
101 };
102 };
103 return Bounds {
104 earliest: Bound::Date(BoundDate {
105 year: lo,
106 month: 1,
107 day: 1,
108 }),
109 latest: Bound::Date(BoundDate {
110 year: hi,
111 month: 12,
112 day: 31,
113 }),
114 };
115 }
116
117 if d.year.significant_digits.is_some() {
119 let value = d.year.value().expect("S excludes X digits");
120 let (lo, hi) = significant_range(value, d.year.significant_digits, 4)
122 .expect("four-digit sweep fits in i64");
123 return Bounds {
124 earliest: Bound::Date(BoundDate {
125 year: lo,
126 month: 1,
127 day: 1,
128 }),
129 latest: Bound::Date(BoundDate {
130 year: hi,
131 month: 12,
132 day: 31,
133 }),
134 };
135 }
136
137 if let Some(code) = d.month.as_ref().and_then(DateField::value) {
139 if (21..=41).contains(&code) {
140 return season_bounds(d, code);
141 }
142 }
143
144 let earliest = extremum(d, true);
145 let latest = extremum(d, false);
146 match (earliest, latest) {
147 (Some(a), Some(b)) => Bounds {
148 earliest: Bound::Date(a),
149 latest: Bound::Date(b),
150 },
151 _ => Bounds {
154 earliest: Bound::Unknown,
155 latest: Bound::Unknown,
156 },
157 }
158}
159
160fn big_width(kind: &YearKind) -> u32 {
162 match kind {
163 YearKind::Standard { .. } => 4,
164 YearKind::Big { value } => decimal_digits(value.unsigned_abs()),
165 YearKind::Exponential {
166 significand,
167 exponent,
168 } => decimal_digits(significand.unsigned_abs()) + exponent,
169 }
170}
171
172fn decimal_digits(mut v: u64) -> u32 {
173 let mut n = 1;
174 while v >= 10 {
175 v /= 10;
176 n += 1;
177 }
178 n
179}
180
181fn significant_range(value: i64, precision: Option<u32>, width: u32) -> Option<(i64, i64)> {
187 let Some(p) = precision else {
188 return Some((value, value));
189 };
190 let sweep = width.saturating_sub(p);
191 let Some(modulus) = 10i64.checked_pow(sweep) else {
192 return Some((value, value));
193 };
194 let mag = value.unsigned_abs() as i64;
195 let lo_mag = mag - mag.rem_euclid(modulus);
196 let hi_mag = lo_mag.checked_add(modulus - 1)?;
197 Some(if value < 0 {
198 (-hi_mag, -lo_mag)
199 } else {
200 (lo_mag, hi_mag)
201 })
202}
203
204fn season_months(code: u8) -> (u8, u8, bool) {
207 match code {
208 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),
213 34 => (4, 6, false),
214 35 => (7, 9, false),
215 36 => (10, 12, false),
216 37 => (1, 4, false),
217 38 => (5, 8, false),
218 39 => (9, 12, false),
219 40 => (1, 6, false),
220 41 => (7, 12, false),
221 _ => unreachable!("validated season code"),
222 }
223}
224
225fn season_bounds(d: &Date, code: u8) -> Bounds {
226 let (first, last, wraps) = season_months(code);
227 let (y_lo, y_hi) = year_range(d);
228 let end_year = if wraps { y_hi + 1 } else { y_hi };
229 Bounds {
230 earliest: Bound::Date(BoundDate {
231 year: y_lo,
232 month: first,
233 day: 1,
234 }),
235 latest: Bound::Date(BoundDate {
236 year: end_year,
237 month: last,
238 day: last_day(last, is_leap(end_year)),
239 }),
240 }
241}
242
243fn year_range(d: &Date) -> (i64, i64) {
247 match d.year.kind {
248 YearKind::Standard { negative, digits } => {
249 if let Some(v) = d.year.value() {
250 (v, v)
251 } else {
252 debug_assert!(!negative);
253 (year_value(digits, 0), year_value(digits, 9))
254 }
255 }
256 _ => unreachable!("caller checked Standard"),
257 }
258}
259
260fn year_value(digits: [Option<u8>; 4], fill: u8) -> i64 {
262 digits
263 .iter()
264 .fold(0, |acc, d| acc * 10 + i64::from(d.unwrap_or(fill)))
265}
266
267fn year_completions(digits: [Option<u8>; 4], ascending: bool) -> impl Iterator<Item = i64> {
271 let mut masked = [0usize; 4];
272 let mut n = 0;
273 for (i, d) in digits.iter().enumerate() {
274 if d.is_none() {
275 masked[n] = i;
276 n += 1;
277 }
278 }
279 let count = 10u32.pow(n as u32);
280 (0..count).map(move |i| {
281 let mut rem = if ascending { i } else { count - 1 - i };
282 let mut filled = digits.map(|d| d.unwrap_or(0));
283 for pos in masked[..n].iter().rev() {
284 filled[*pos] = (rem % 10) as u8;
285 rem /= 10;
286 }
287 filled.iter().fold(0, |acc, d| acc * 10 + i64::from(*d))
288 })
289}
290
291fn extremum(d: &Date, ascending: bool) -> Option<BoundDate> {
298 if let YearKind::Standard { digits, .. } = d.year.kind {
299 if d.year.value().is_none() {
300 return year_completions(digits, ascending)
301 .find_map(|y| extremum_in_year(d, y, ascending));
302 }
303 }
304 let (y, _) = year_range(d);
305 extremum_in_year(d, y, ascending)
306}
307
308fn extremum_in_year(d: &Date, y: i64, ascending: bool) -> Option<BoundDate> {
311 let Some(month) = &d.month else {
312 return Some(if ascending {
313 BoundDate {
314 year: y,
315 month: 1,
316 day: 1,
317 }
318 } else {
319 BoundDate {
320 year: y,
321 month: 12,
322 day: 31,
323 }
324 });
325 };
326 let mut months = month_candidates_of(month);
327 if !ascending {
328 months.reverse();
329 }
330 for m in months {
331 let Some(day) = &d.day else {
332 return Some(if ascending {
333 BoundDate {
334 year: y,
335 month: m,
336 day: 1,
337 }
338 } else {
339 BoundDate {
340 year: y,
341 month: m,
342 day: last_day(m, is_leap(y)),
343 }
344 });
345 };
346 let mut days = day_candidates_of(day);
347 if !ascending {
348 days.reverse();
349 }
350 for dd in days {
351 if dd <= last_day(m, is_leap(y)) {
352 return Some(BoundDate {
353 year: y,
354 month: m,
355 day: dd,
356 });
357 }
358 }
359 }
360 None
361}
362
363fn month_candidates_of(f: &DateField) -> alloc::vec::Vec<u8> {
364 match f.value() {
365 Some(v) => alloc::vec![v],
366 None => (1..=12).filter(|v| field_matches(f, *v)).collect(),
367 }
368}
369
370fn day_candidates_of(f: &DateField) -> alloc::vec::Vec<u8> {
371 match f.value() {
372 Some(v) => alloc::vec![v],
373 None => (1..=31).filter(|v| field_matches(f, *v)).collect(),
374 }
375}
376
377fn field_matches(f: &DateField, v: u8) -> bool {
378 f.digits[0].is_none_or(|p| p == v / 10) && f.digits[1].is_none_or(|p| p == v % 10)
379}
380
381pub(crate) fn is_leap(y: i64) -> bool {
383 y.rem_euclid(4) == 0 && (y.rem_euclid(100) != 0 || y.rem_euclid(400) == 0)
384}
385
386pub(crate) fn last_day(month: u8, leap: bool) -> u8 {
387 match month {
388 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
389 4 | 6 | 9 | 11 => 30,
390 2 => {
391 if leap {
392 29
393 } else {
394 28
395 }
396 }
397 _ => unreachable!("month is 1-12"),
398 }
399}
400
401fn interval_bounds(iv: &Interval) -> Bounds {
402 let earliest = match &iv.start {
403 IntervalEndpoint::Open | IntervalEndpoint::OnOrBefore(_) => Bound::NegativeInfinity,
404 IntervalEndpoint::Unknown => Bound::Unknown,
405 IntervalEndpoint::Date(d) | IntervalEndpoint::OnOrAfter(d) => date_bounds(d).earliest,
406 };
407 let latest = match &iv.end {
408 IntervalEndpoint::Open | IntervalEndpoint::OnOrAfter(_) => Bound::PositiveInfinity,
409 IntervalEndpoint::Unknown => Bound::Unknown,
410 IntervalEndpoint::Date(d) | IntervalEndpoint::OnOrBefore(d) => date_bounds(d).latest,
411 };
412 Bounds { earliest, latest }
413}
414
415fn set_bounds(s: &Set) -> Bounds {
416 let mut earliest: Option<Bound> = None;
417 let mut latest: Option<Bound> = None;
418 for element in &s.elements {
419 let (e, l) = match element {
420 SetElement::Date(d) => {
421 let b = date_bounds(d);
422 (b.earliest, b.latest)
423 }
424 SetElement::OnOrBefore(d) => (Bound::NegativeInfinity, date_bounds(d).latest),
425 SetElement::OnOrAfter(d) => (date_bounds(d).earliest, Bound::PositiveInfinity),
426 SetElement::Range(a, b) => (date_bounds(a).earliest, date_bounds(b).latest),
427 };
428 earliest = Some(match earliest {
429 None => e,
430 Some(cur) => min_bound(cur, e),
431 });
432 latest = Some(match latest {
433 None => l,
434 Some(cur) => max_bound(cur, l),
435 });
436 }
437 Bounds {
438 earliest: earliest.unwrap_or(Bound::Unknown),
439 latest: latest.unwrap_or(Bound::Unknown),
440 }
441}
442
443fn min_bound(a: Bound, b: Bound) -> Bound {
444 match (a, b) {
445 (Bound::NegativeInfinity, _) | (_, Bound::NegativeInfinity) => Bound::NegativeInfinity,
446 (Bound::Unknown, _) | (_, Bound::Unknown) => Bound::Unknown,
447 (Bound::PositiveInfinity, x) | (x, Bound::PositiveInfinity) => x,
448 (Bound::Date(x), Bound::Date(y)) => Bound::Date(x.min(y)),
449 }
450}
451
452fn max_bound(a: Bound, b: Bound) -> Bound {
453 match (a, b) {
454 (Bound::PositiveInfinity, _) | (_, Bound::PositiveInfinity) => Bound::PositiveInfinity,
455 (Bound::Unknown, _) | (_, Bound::Unknown) => Bound::Unknown,
456 (Bound::NegativeInfinity, x) | (x, Bound::NegativeInfinity) => x,
457 (Bound::Date(x), Bound::Date(y)) => Bound::Date(x.max(y)),
458 }
459}