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