Skip to main content

edtf_core/
display.rs

1// SPDX-FileCopyrightText: Copyright (c) the edtf contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Canonical formatting: `Display` renders every expression in the
5//! spec-preferred form (ISO 8601-2 §8.2.4 — complete qualification over
6//! group, group over individual, no redundant qualifiers).
7//!
8//! Round-trip property: for any valid input, `parse(format(parse(input)))`
9//! yields a semantically identical value; spellings that mean the same
10//! thing (`?2004-?06-?11` vs `2004-06-11?`) normalize to one form.
11
12use alloc::{string::String, vec::Vec};
13use core::fmt::{self, Display, Formatter, Write as _};
14
15use crate::types::{
16    Date, DateField, DateTime, Edtf, Interval, IntervalEndpoint, Qualifier, Set, SetElement,
17    SetKind, Time, TimeShift, Year, YearKind,
18};
19
20impl Display for Edtf {
21    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::Date(d) => d.fmt(f),
24            Self::DateTime(dt) => dt.fmt(f),
25            Self::Interval(iv) => iv.fmt(f),
26            Self::Set(s) => s.fmt(f),
27        }
28    }
29}
30
31fn qual_symbol(q: Qualifier) -> char {
32    match (q.uncertain, q.approximate) {
33        (true, true) => '%',
34        (true, false) => '?',
35        (false, true) => '~',
36        (false, false) => unreachable!("caller checks is_qualified"),
37    }
38}
39
40fn year_body(year: &Year) -> String {
41    let mut s = String::new();
42    match year.kind {
43        YearKind::Standard { negative, digits } => {
44            if negative {
45                s.push('-');
46            }
47            for d in digits {
48                match d {
49                    Some(v) => s.push((b'0' + v) as char),
50                    None => s.push('X'),
51                }
52            }
53        },
54        YearKind::Big { value } => {
55            let _ = write!(s, "Y{value}");
56        },
57        YearKind::Exponential {
58            significand,
59            exponent,
60        } => {
61            let _ = write!(s, "Y{significand}E{exponent}");
62        },
63    }
64    if let Some(p) = year.significant_digits {
65        let _ = write!(s, "S{p}");
66    }
67    s
68}
69
70fn field_body(f: DateField) -> String {
71    let mut s = String::new();
72    for d in f.digits {
73        match d {
74            Some(v) => s.push((b'0' + v) as char),
75            None => s.push('X'),
76        }
77    }
78    s
79}
80
81impl Display for Date {
82    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
83        let mut parts: Vec<(String, Qualifier)> = Vec::new();
84        parts.push((year_body(&self.year), self.year.qualifier));
85        if let Some(m) = &self.month {
86            parts.push((field_body(*m), m.qualifier));
87        }
88        if let Some(d) = &self.day {
89            parts.push((field_body(*d), d.qualifier));
90        }
91
92        let all_equal = parts.iter().all(|(_, q)| *q == parts[0].1);
93        if all_equal {
94            // Unqualified, or complete qualification: one trailing symbol.
95            let q = parts[0].1;
96            for (i, (body, _)) in parts.iter().enumerate() {
97                if i > 0 {
98                    f.write_char('-')?;
99                }
100                f.write_str(body)?;
101            }
102            if q.is_qualified() {
103                f.write_char(qual_symbol(q))?;
104            }
105            return Ok(());
106        }
107
108        // Longest qualified prefix sharing the year's qualifier becomes one
109        // group marker; anything after it gets individual (left) qualifiers.
110        // Fully uniform dates took the branch above, so this scan always
111        // stops at an unequal part before running off the end.
112        let q0 = parts[0].1;
113        let prefix_end = if q0.is_qualified() {
114            let mut end = 0;
115            while end + 1 < parts.len() && parts[end + 1].1 == q0 {
116                end += 1;
117            }
118            Some(end)
119        } else {
120            None
121        };
122        for (i, (body, q)) in parts.iter().enumerate() {
123            if i > 0 {
124                f.write_char('-')?;
125            }
126            let in_prefix = prefix_end.is_some_and(|end| i <= end);
127            if !in_prefix && q.is_qualified() {
128                f.write_char(qual_symbol(*q))?;
129            }
130            f.write_str(body)?;
131            if prefix_end == Some(i) {
132                f.write_char(qual_symbol(q0))?;
133            }
134        }
135        Ok(())
136    }
137}
138
139impl Display for Time {
140    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
141        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
142        match self.shift {
143            None => Ok(()),
144            Some(TimeShift::Utc) => f.write_char('Z'),
145            Some(TimeShift::Offset {
146                minutes,
147                hours_only,
148            }) => {
149                let sign = if minutes < 0 { '-' } else { '+' };
150                let mag = minutes.unsigned_abs();
151                let (h, m) = (mag / 60, mag % 60);
152                // The parser only sets `hours_only` for ±hh forms, whose
153                // minutes are zero by construction.
154                if hours_only && m == 0 {
155                    write!(f, "{sign}{h:02}")
156                } else {
157                    write!(f, "{sign}{h:02}:{m:02}")
158                }
159            },
160        }
161    }
162}
163
164impl Display for DateTime {
165    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
166        write!(f, "{}T{}", self.date, self.time)
167    }
168}
169
170impl Display for Interval {
171    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
172        fn endpoint(f: &mut Formatter<'_>, e: &IntervalEndpoint) -> fmt::Result {
173            match e {
174                IntervalEndpoint::Unknown => Ok(()),
175                IntervalEndpoint::Open => f.write_str(".."),
176                IntervalEndpoint::Date(d) => d.fmt(f),
177                IntervalEndpoint::OnOrBefore(d) => write!(f, "..{d}"),
178                IntervalEndpoint::OnOrAfter(d) => write!(f, "{d}.."),
179            }
180        }
181        endpoint(f, &self.start)?;
182        f.write_char('/')?;
183        endpoint(f, &self.end)
184    }
185}
186
187impl Display for Set {
188    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
189        let (open, close) = match self.kind {
190            SetKind::AllMembers => ('{', '}'),
191            SetKind::OneMember => ('[', ']'),
192        };
193        f.write_char(open)?;
194        for (i, e) in self.elements.iter().enumerate() {
195            if i > 0 {
196                f.write_char(',')?;
197            }
198            match e {
199                SetElement::Date(d) => d.fmt(f)?,
200                SetElement::OnOrBefore(d) => write!(f, "..{d}")?,
201                SetElement::OnOrAfter(d) => write!(f, "{d}..")?,
202                SetElement::Range(a, b) => write!(f, "{a}..{b}")?,
203            }
204        }
205        f.write_char(close)
206    }
207}