Skip to main content

edtf_core/
display.rs

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