Skip to main content

edtf_core/
relation.rs

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