edtf/level_1/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright © 2021 Corporation for Digital Scholarship
6
7#![doc = include_str!("README.md")]
8
9pub(crate) mod packed;
10mod parser;
11mod validate;
12
13mod basic;
14
15/// A set of iterators for stepping through date intervals.
16pub mod iter;
17
18#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
19#[cfg(feature = "chrono")]
20mod sorting;
21#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
22#[cfg(feature = "chrono")]
23pub use sorting::{SortOrder, SortOrderEnd, SortOrderStart};
24
25#[cfg(test)]
26mod test;
27
28use core::convert::TryInto;
29use core::fmt;
30use core::num::NonZeroU8;
31use core::str::FromStr;
32
33use crate::helpers;
34use crate::{DateComplete, DateTime, ParseError, Time, TzOffset};
35
36use self::{
37    packed::{DMMask, PackedInt, PackedU8, PackedYear, YearMask},
38    parser::{ParsedEdtf, UnvalidatedDate},
39};
40
41// TODO: wrap Certainty with one that doesn't expose the implementation detail
42pub use packed::Certainty;
43
44/// An EDTF date. Represents a standalone date or one end of a interval.
45///
46/// ### Equality and comparison
47///
48/// Presently, only the default derive-generated code is used for PartialEq/PartialOrd. At least
49/// you can have some kind of implementation, but the results won't necessarily make sense when
50/// some components are masked (unspecified). The current implementation is packed, and the
51/// comparisons are done on the packed representation. You may wish to add a wrapper type with its
52/// own PartialEq/PartialOrd/Eq/Ord implementations with more complex logic.
53///
54/// ```
55/// use edtf::level_1::{Date, Precision};
56/// let d1 = Date::from_precision(Precision::DayOfMonth(2021, 06));
57/// let d2 = Date::from_precision(Precision::Day(2021, 06, 09));
58/// // d1 is a non-specific day in June, but PartialOrd thinks d1 is "less than" d2.
59/// assert!(d1 < d2);
60/// assert!( ! (d1 > d2));
61/// ```
62#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub struct Date {
64    pub(crate) year: PackedYear,
65    pub(crate) month: Option<PackedU8>,
66    pub(crate) day: Option<PackedU8>,
67    pub(crate) certainty: Certainty,
68}
69
70/// Fully represents EDTF Level 1. The representation is lossless.
71#[derive(Clone, Copy, PartialEq, Eq, Hash)]
72pub enum Edtf {
73    /// A full timestamp. `2019-07-15T01:56:00Z`
74    DateTime(DateTime),
75    /// `2018`, `2019-07-09%`, `1973?`, `1956-XX`, etc
76    Date(Date),
77    /// `Y170000002`, `Y-170000002`
78    ///
79    /// Years within the interval -9999..=9999 are explicitly disallowed. Years must contain MORE
80    /// THAN four digits.
81    YYear(YYear),
82    /// `2018/2019`, `2019-12-31/2020-01-15`, etc
83    Interval(Date, Date),
84    /// `2019/..` (open), `2019/` (unknown)
85    IntervalFrom(Date, Terminal),
86    /// `../2019` (open), `/2019` (unknown)
87    IntervalTo(Terminal, Date),
88}
89
90/// Either empty string (unknown start/end date) or `..` in an L1 interval.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
92pub enum Terminal {
93    /// empty string before or after a slash, e.g. `/2019` or `2019/`
94    Unknown,
95    /// `..` in e.g. `../2019` or `2019/..`
96    Open,
97}
98
99/// A season in [Precision]
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
101pub enum Season {
102    /// 21
103    Spring = 21,
104    /// 22
105    Summer = 22,
106    /// 23
107    Autumn = 23,
108    /// 24
109    Winter = 24,
110}
111
112impl Season {
113    fn from_u32(value: u32) -> Self {
114        match value {
115            21 => Self::Spring,
116            22 => Self::Summer,
117            23 => Self::Autumn,
118            24 => Self::Winter,
119            _ => panic!("invalid season number {}", value),
120        }
121    }
122    fn from_u32_opt(value: u32) -> Option<Self> {
123        Some(match value {
124            21 => Self::Spring,
125            22 => Self::Summer,
126            23 => Self::Autumn,
127            24 => Self::Winter,
128            _ => return None,
129        })
130    }
131}
132
133/// An enum used to conveniently match on a [Date].
134///
135/// The i32 field in each is a year.
136///
137/// ```
138/// use edtf::level_1::{Date, Certainty, Precision};
139/// match Date::parse("2019-04-XX").unwrap().precision() {
140///     // 2019-04-XX
141///     Precision::DayOfMonth(year, m) => {
142///         assert_eq!(year, 2019);
143///         assert_eq!(m, 4);
144///     }
145///     // 2019-XX
146///     Precision::MonthOfYear(year) => {
147///         panic!("not matched");
148///     }
149///     // ...
150///     _ => panic!("not matched"),
151/// }
152/// ```
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum Precision {
155    /// `19XX` => `Century(1900)`; Ends in two zeroes.
156    Century(i32),
157    /// `193X` => `Decade(1930)`; Ends in a zero.
158    Decade(i32),
159    /// `1936` => `Year(1936)`; A particular year.
160    Year(i32),
161    /// `1933-22` => `Season(1933, Season::Summer)`; a particular season in a particular year.
162    Season(i32, Season),
163    /// `1936-08` => `Month(1936, 08)`; a particular month in a particular year. Month is `1..=12`
164    Month(i32, u32),
165    /// `1931-08-19` => `Day(1931, 08, 19)`; a full date; a particular day.
166    ///
167    /// Month `1..-12`, day is valid for that month in that year.
168    Day(i32, u32, u32),
169    /// `1931-XX` => `MonthOfYear(1931)`; a non-specific month in a particular year.
170    MonthOfYear(i32),
171    /// `1931-XX-XX` => `DayOfYear(1931)`; a non-specific day in a particular year.
172    DayOfYear(i32),
173    /// `1931-08-XX` => `DayOfMonth(1931, 08)`; a non-specific day in a particular year.
174    ///
175    /// Month is `1..=12`
176    DayOfMonth(i32, u32),
177}
178
179/// Represents a 5+ digit, signed year like `Y12345`, `Y-17000`.
180///
181#[doc = include_str!("YYear.md")]
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub struct YYear(i64);
184
185impl YYear {
186    /// Get the year this represents.
187    pub fn year(&self) -> i64 {
188        self.0
189    }
190
191    /// Gets the year. Like [YYear::year] but takes `self`.
192    ///
193    /// ```
194    /// use edtf::level_1::YYear;
195    /// assert_eq!(YYear::new_opt(12345).map(YYear::value), Some(12345));
196    /// ```
197    pub fn value(self) -> i64 {
198        self.0
199    }
200
201    pub(crate) fn raw(y: i64) -> Self {
202        Self(y)
203    }
204
205    /// Creates a YYear but **panics** if value is fewer than 5 digits.
206    pub fn new(value: i64) -> Self {
207        Self::new_opt(value).expect("value outside range for YYear, must be 5-digit year")
208    }
209
210    /// If the value is fewer than 5 digits (invalid for `Y`-years), returns None.
211    pub fn new_opt(value: i64) -> Option<Self> {
212        if helpers::inside_9999(value) {
213            return None;
214        }
215        Some(Self(value))
216    }
217
218    /// If the value is fewer than 5 digits (invalid for `Y`-years), returns an [Edtf::Date]
219    /// calendar date instead.
220    pub fn new_or_cal(value: i64) -> Result<Self, Edtf> {
221        if helpers::inside_9999(value) {
222            let date = value
223                .try_into()
224                .ok()
225                .and_then(|y| Date::from_ymd_opt(y, 0, 0))
226                .map(Edtf::Date)
227                .expect("should have already validated as within -9999..=9999");
228            return Err(date);
229        }
230        Ok(Self(value))
231    }
232}
233
234/// Specifies the number of Xs in `2019`/`201X`/`20XX`.
235#[derive(Debug, Copy, Clone, PartialEq, Eq)]
236pub enum YearDigits {
237    /// `2019`
238    NoX,
239    /// `201X`
240    X,
241    /// `20XX`
242    XX,
243}
244
245#[doc(hidden)]
246impl From<YearMask> for YearDigits {
247    fn from(ym: YearMask) -> Self {
248        match ym {
249            YearMask::None => Self::NoX,
250            YearMask::OneDigit => Self::X,
251            YearMask::TwoDigits => Self::XX,
252        }
253    }
254}
255
256#[doc(hidden)]
257impl From<YearDigits> for YearMask {
258    fn from(ym: YearDigits) -> Self {
259        match ym {
260            YearDigits::NoX => YearMask::None,
261            YearDigits::X => YearMask::OneDigit,
262            YearDigits::XX => YearMask::TwoDigits,
263        }
264    }
265}
266
267/// Represents a possibly-unspecified date component (month or day) or an -XX mask. Pass-through formatting
268///
269/// ```
270/// use edtf::level_1::Component::*;
271/// assert_eq!(format!("{}", Unspecified), "X");
272/// assert_eq!(format!("{:04}", Unspecified), "XXXX");
273/// assert_eq!(format!("{:02}", Value(5)), "05");
274/// assert_eq!(format!("{:04}", Value(5)), "0005");
275/// ```
276#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
277pub enum Component {
278    /// The component has a value that was specified in the EDTF
279    Value(u32),
280    /// An `-XX` masked out component
281    Unspecified,
282}
283
284impl fmt::Display for Component {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        match self {
287            // pass the formatter through and let people format as they wish
288            Component::Value(val) => val.fmt(f),
289            Component::Unspecified => {
290                // write as many digits as were requested
291                let precision = f.width().unwrap_or(1);
292                write!(f, "{:X<1$}", "", precision)
293            }
294        }
295    }
296}
297
298impl Component {
299    /// Get the value as an option instead of the custom `Component` type.
300    pub fn value(self) -> Option<u32> {
301        match self {
302            Component::Value(v) => Some(v),
303            Component::Unspecified => None,
304        }
305    }
306
307    fn from_packed_filter(packed: PackedU8, range: std::ops::RangeInclusive<u32>) -> Option<Self> {
308        let (val, flags) = packed.unpack();
309        let val = val as u32;
310        if flags.is_masked() {
311            Some(Component::Unspecified)
312        } else if range.contains(&val) {
313            Some(Component::Value(val as u32))
314        } else {
315            None
316        }
317    }
318    fn from_packed(packed: PackedU8) -> Self {
319        let (val, flags) = packed.unpack();
320        if flags.is_masked() {
321            Component::Unspecified
322        } else {
323            Component::Value(val as u32)
324        }
325    }
326}
327
328impl From<Date> for Edtf {
329    fn from(date: Date) -> Self {
330        Self::Date(date)
331    }
332}
333
334impl From<(Date, Date)> for Edtf {
335    fn from((a, b): (Date, Date)) -> Self {
336        Self::Interval(a, b)
337    }
338}
339
340impl FromStr for Edtf {
341    type Err = ParseError;
342
343    fn from_str(s: &str) -> Result<Self, Self::Err> {
344        Edtf::parse(s)
345    }
346}
347
348impl fmt::Display for Date {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        let Date {
351            year,
352            month,
353            day,
354            certainty,
355        } = *self;
356        let (year, yf) = year.unpack();
357        let sign = helpers::sign_str_if_neg(year);
358        let year = year.abs();
359        match yf.mask {
360            YearMask::None => write!(f, "{}{:04}", sign, year)?,
361            YearMask::OneDigit => write!(f, "{}{:03}X", sign, year / 10)?,
362            YearMask::TwoDigits => write!(f, "{}{:02}XX", sign, year / 100)?,
363        }
364        if let Some(month) = month {
365            let (m, mf) = month.unpack();
366            match mf.mask {
367                DMMask::None => write!(f, "-{:02}", m)?,
368                DMMask::Unspecified => write!(f, "-XX")?,
369            }
370            if let Some(day) = day {
371                let (d, df) = day.unpack();
372                match df.mask {
373                    DMMask::None => write!(f, "-{:02}", d)?,
374                    DMMask::Unspecified => write!(f, "-XX")?,
375                }
376            }
377        }
378        if let Some(cert) = match certainty {
379            Certainty::Certain => None,
380            Certainty::Uncertain => Some("?"),
381            Certainty::Approximate => Some("~"),
382            Certainty::ApproximateUncertain => Some("%"),
383        } {
384            write!(f, "{}", cert)?;
385        }
386        Ok(())
387    }
388}
389
390impl fmt::Debug for Date {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        write!(f, "{}", self)
393    }
394}
395
396impl fmt::Display for DateTime {
397    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398        let DateComplete { year, month, day } = self.date;
399        let Time { hh, mm, ss, tz } = self.time;
400        write!(
401            f,
402            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
403            year, month, day, hh, mm, ss
404        )?;
405        match tz {
406            TzOffset::Unspecified => {}
407            TzOffset::Utc => write!(f, "Z")?,
408            TzOffset::Hours(h) => {
409                let off_h = h % 24;
410                write!(f, "{:+03}", off_h)?;
411            }
412            TzOffset::Minutes(min) => {
413                let off_m = (min.abs()) % 60;
414                let off_h = (min / 60) % 24;
415                write!(f, "{:+03}:{:02}", off_h, off_m)?;
416            }
417        }
418        Ok(())
419    }
420}
421
422impl fmt::Display for YYear {
423    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424        write!(f, "Y{}", self.0)
425    }
426}
427
428impl fmt::Display for Terminal {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        match self {
431            Self::Open => write!(f, ".."),
432            Self::Unknown => Ok(()),
433        }
434    }
435}
436
437impl fmt::Display for Edtf {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        match self {
440            Self::Date(d) => write!(f, "{}", d),
441            Self::Interval(d, d2) => write!(f, "{}/{}", d, d2),
442            Self::IntervalFrom(d, t) => write!(f, "{}/{}", d, t),
443            Self::IntervalTo(t, d) => write!(f, "{}/{}", t, d),
444            Self::YYear(s) => write!(f, "{}", s),
445            Self::DateTime(dt) => write!(f, "{}", dt),
446        }
447    }
448}
449
450impl fmt::Debug for Edtf {
451    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452        <Self as fmt::Display>::fmt(self, f)
453    }
454}