edtf_core/relation.rs
1// SPDX-FileCopyrightText: Copyright (c) the edtf contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Three-valued temporal relations between EDTF expressions.
5//!
6//! [`Edtf::relation`] answers "how does A relate to B in time?" honestly
7//! under uncertainty: each of the six coarsened Allen relations (*before /
8//! after / overlaps / contains / within / equal*) is reported as
9//! [`Modality::Impossible`], [`Modality::Possible`] (holds for some
10//! completion) or [`Modality::Definite`] (holds for every completion).
11//!
12//! Semantics (decision D23 in `docs/spec-notes.md`): an expression denotes
13//! some nonempty day-interval lying within its [`Edtf::bounds`] region —
14//! the "sometime during" reading. `1985` is a value falling somewhere
15//! within the calendar year, not necessarily spanning the whole of it.
16//! Consequences, documented rather than hidden:
17//!
18//! - Qualification (`?~%`) never moves bounds (ISO 8601-2 §8.4.2 NOTE), so
19//! `1985?` relates exactly as `1985`.
20//! - Only *before*, *after* and *equal* can ever be Definite: any region wider
21//! than one day admits single-day completions, so containment or overlap can
22//! never be forced.
23//! - Interval endpoint linkage is coarsened away: `2004/2005` vs `2004-06`
24//! reports possibly-before, although every true completion of the interval
25//! straddles June 2004. Everything flows through the bounds region.
26//! - Unknown bounds propagate as possible-everything, never Definite — even
27//! where the other endpoint could in principle constrain them.
28//! - Bounds are day-granular (time of day refines within a day), so same-day
29//! datetimes are definitely equal.
30
31use crate::{
32 bounds::{Bound, BoundDate, is_leap, last_day},
33 types::Edtf,
34};
35
36/// One of the six coarsened Allen relations between two time regions.
37///
38/// The six are exhaustive and mutually exclusive over concrete
39/// day-intervals: disjoint pairs are `Before`/`After`, coincident pairs are
40/// `Equal`, proper containment (including a shared endpoint) is
41/// `Contains`/`Within`, and partial overlap is `Overlaps`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum Relation {
45 /// A ends before B starts (no shared day).
46 Before,
47 /// A starts after B ends (no shared day).
48 After,
49 /// A and B share days but each also has days the other lacks on
50 /// opposite sides (partial overlap).
51 Overlaps,
52 /// B lies within A without being equal to it.
53 Contains,
54 /// A lies within B without being equal to it.
55 Within,
56 /// A and B cover exactly the same days.
57 Equal,
58}
59
60impl Relation {
61 /// All six relations, in canonical order.
62 pub const ALL: [Self; 6] = [
63 Self::Before,
64 Self::After,
65 Self::Overlaps,
66 Self::Contains,
67 Self::Within,
68 Self::Equal,
69 ];
70
71 /// The relation that holds of (B, A) whenever `self` holds of (A, B).
72 #[must_use]
73 pub const fn converse(self) -> Self {
74 match self {
75 Self::Before => Self::After,
76 Self::After => Self::Before,
77 Self::Contains => Self::Within,
78 Self::Within => Self::Contains,
79 Self::Overlaps | Self::Equal => self,
80 }
81 }
82
83 /// Lower-case name: `"before"`, `"after"`, `"overlaps"`, `"contains"`,
84 /// `"within"` or `"equal"`.
85 #[must_use]
86 pub const fn as_str(self) -> &'static str {
87 match self {
88 Self::Before => "before",
89 Self::After => "after",
90 Self::Overlaps => "overlaps",
91 Self::Contains => "contains",
92 Self::Within => "within",
93 Self::Equal => "equal",
94 }
95 }
96
97 const fn idx(self) -> usize {
98 match self {
99 Self::Before => 0,
100 Self::After => 1,
101 Self::Overlaps => 2,
102 Self::Contains => 3,
103 Self::Within => 4,
104 Self::Equal => 5,
105 }
106 }
107}
108
109/// How firmly a relation holds across the completions of two expressions.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub enum Modality {
113 /// Holds for no pair of completions.
114 Impossible,
115 /// Holds for some pair of completions, but not all.
116 Possible,
117 /// Holds for every pair of completions.
118 Definite,
119}
120
121impl Modality {
122 /// Lower-case name: `"impossible"`, `"possible"` or `"definite"`.
123 #[must_use]
124 pub const fn as_str(self) -> &'static str {
125 match self {
126 Self::Impossible => "impossible",
127 Self::Possible => "possible",
128 Self::Definite => "definite",
129 }
130 }
131}
132
133/// The modality of every [`Relation`] between one pair of expressions, as
134/// computed by [`Edtf::relation`].
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub struct Relations {
138 modalities: [Modality; 6],
139}
140
141impl Relations {
142 /// Every relation possible, none definite — the honest answer when a
143 /// bound is unknown.
144 const ALL_POSSIBLE: Self = Self {
145 modalities: [Modality::Possible; 6],
146 };
147
148 /// A possible-set becomes modalities: the sole possible relation (when
149 /// there is exactly one) holds for *every* completion pair, because the
150 /// six relations are exhaustive and mutually exclusive.
151 fn from_possible(possible: [bool; 6]) -> Self {
152 let count = possible.iter().filter(|p| **p).count();
153 let mut modalities = [Modality::Impossible; 6];
154 for (m, p) in modalities.iter_mut().zip(possible) {
155 if p {
156 *m = if count == 1 {
157 Modality::Definite
158 } else {
159 Modality::Possible
160 };
161 }
162 }
163 Self { modalities }
164 }
165
166 /// The modality of one relation.
167 #[must_use]
168 pub const fn modality(self, r: Relation) -> Modality {
169 self.modalities[r.idx()]
170 }
171
172 /// True if the relation holds for at least one completion pair
173 /// (i.e. its modality is `Possible` or `Definite`).
174 #[must_use]
175 pub fn is_possible(self, r: Relation) -> bool {
176 self.modality(r) != Modality::Impossible
177 }
178
179 /// True if the relation holds for every completion pair.
180 #[must_use]
181 pub fn is_definite(self, r: Relation) -> bool {
182 self.modality(r) == Modality::Definite
183 }
184
185 /// True if the relation holds for no completion pair.
186 #[must_use]
187 pub fn is_impossible(self, r: Relation) -> bool {
188 self.modality(r) == Modality::Impossible
189 }
190
191 /// The one relation that definitely holds, if any. At most one relation
192 /// can be definite; only `Before`, `After` and `Equal` ever are.
193 #[must_use]
194 pub fn definite(self) -> Option<Relation> {
195 Relation::ALL.into_iter().find(|r| self.is_definite(*r))
196 }
197
198 /// The relations that hold for at least one completion pair, in
199 /// canonical order. Never empty.
200 pub fn possible(self) -> impl Iterator<Item = Relation> {
201 Relation::ALL
202 .into_iter()
203 .filter(move |r| self.is_possible(*r))
204 }
205}
206
207impl core::fmt::Display for Relations {
208 /// Comma-separated non-impossible relations, e.g. `definitely before`
209 /// or `possibly before, possibly overlaps, possibly within`.
210 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
211 let mut first = true;
212 for r in self.possible() {
213 if !first {
214 write!(f, ", ")?;
215 }
216 first = false;
217 let adverb = match self.modality(r) {
218 Modality::Definite => "definitely",
219 _ => "possibly",
220 };
221 write!(f, "{adverb} {}", r.as_str())?;
222 }
223 Ok(())
224 }
225}
226
227/// A bound on the day axis with infinities ordered around concrete days.
228/// `Unknown` is handled before conversion, so no variant is needed for it.
229#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
230enum Ext {
231 NegInf,
232 Day(BoundDate),
233 PosInf,
234}
235
236const fn ext(b: Bound) -> Option<Ext> {
237 match b {
238 Bound::NegativeInfinity => Some(Ext::NegInf),
239 Bound::Date(d) => Some(Ext::Day(d)),
240 Bound::PositiveInfinity => Some(Ext::PosInf),
241 Bound::Unknown => None,
242 }
243}
244
245/// The calendar day after `e`; year overflow saturates to `PosInf`, which
246/// only under-reports overlap feasibility at the edge of representable time.
247fn succ(e: Ext) -> Ext {
248 let Ext::Day(d) = e else { return e };
249 if d.day < last_day(d.month, is_leap(d.year)) {
250 Ext::Day(BoundDate {
251 day: d.day + 1,
252 ..d
253 })
254 } else if d.month < 12 {
255 Ext::Day(BoundDate {
256 year: d.year,
257 month: d.month + 1,
258 day: 1,
259 })
260 } else {
261 d.year.checked_add(1).map_or(Ext::PosInf, |year| {
262 Ext::Day(BoundDate {
263 year,
264 month: 1,
265 day: 1,
266 })
267 })
268 }
269}
270
271/// The calendar day before `e`; year underflow saturates to `NegInf`.
272fn pred(e: Ext) -> Ext {
273 let Ext::Day(d) = e else { return e };
274 if d.day > 1 {
275 Ext::Day(BoundDate {
276 day: d.day - 1,
277 ..d
278 })
279 } else if d.month > 1 {
280 let month = d.month - 1;
281 Ext::Day(BoundDate {
282 year: d.year,
283 month,
284 day: last_day(month, is_leap(d.year)),
285 })
286 } else {
287 d.year.checked_sub(1).map_or(Ext::NegInf, |year| {
288 Ext::Day(BoundDate {
289 year,
290 month: 12,
291 day: 31,
292 })
293 })
294 }
295}
296
297/// Can some completion of A start strictly before some completion of B and
298/// end inside it (`a1 < b1 <= a2 < b2`)? Setting `p` to the shared boundary
299/// day (A's end, B's start), feasibility is exactly
300/// `∃p: lo_a < p <= hi_a ∧ lo_b <= p < hi_b`.
301fn half_overlap(lo_a: Ext, hi_a: Ext, lo_b: Ext, hi_b: Ext) -> bool {
302 succ(lo_a).max(lo_b) <= hi_a.min(pred(hi_b))
303}
304
305impl Edtf {
306 /// The three-valued temporal relation between this expression and
307 /// `other`, computed over the two [`Edtf::bounds`] regions (see the
308 /// semantics note in this module's documentation).
309 ///
310 /// ```
311 /// use edtf_core::{Edtf, Modality, Relation};
312 ///
313 /// let a = Edtf::parse("1985~").unwrap();
314 /// let b = Edtf::parse("199X").unwrap();
315 /// assert_eq!(a.relation(&b).definite(), Some(Relation::Before));
316 ///
317 /// let c = Edtf::parse("198X").unwrap();
318 /// let d = Edtf::parse("1985").unwrap();
319 /// // 198X may fall before, on, after or around 1985 — nothing asserted.
320 /// assert_eq!(c.relation(&d).definite(), None);
321 /// assert!(c.relation(&d).is_possible(Relation::Before));
322 /// assert!(c.relation(&d).is_possible(Relation::After));
323 /// assert!(c.relation(&d).is_possible(Relation::Contains));
324 /// ```
325 #[must_use]
326 pub fn relation(&self, other: &Self) -> Relations {
327 let a = self.bounds();
328 let b = other.bounds();
329 let (Some(a1), Some(a2), Some(b1), Some(b2)) = (
330 ext(a.earliest),
331 ext(a.latest),
332 ext(b.earliest),
333 ext(b.latest),
334 ) else {
335 return Relations::ALL_POSSIBLE;
336 };
337 let intersects = a1 <= b2 && b1 <= a2;
338 Relations::from_possible([
339 a1 < b2, // Before: some a ends before some b starts.
340 b1 < a2, // After, mirrored.
341 half_overlap(a1, a2, b1, b2) || half_overlap(b1, b2, a1, a2), // Overlaps.
342 intersects && a1 < a2, // Contains: a day of B inside a wider A.
343 intersects && b1 < b2, // Within, mirrored.
344 intersects, // Equal: a shared day serves both.
345 ])
346 }
347}