1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
use crate::{
ComponentOrder, Date, DateComponentSeparator, GregorianDate, ParsedFormat, ParsedSacOrGreg,
parse::ParsedComponent,
};
use core::{fmt::Display, num::NonZero};
/// EitherDate is either a [GregorianDate] or SAC13 [Date].
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SacOrGreg {
/// Variant wrapping a [GregorianDate].
Gregorian(GregorianDate),
/// Variant wrapping a SAC13 [Date].
Sac13(Date),
}
impl SacOrGreg {
/// If the inner variant is [SacOrGreg::Gregorian] it returns that, otherwise [None].
pub fn greg(&self) -> Option<GregorianDate> {
match self {
Self::Gregorian(x) => Some(*x),
_ => None,
}
}
/// If the inner variant is [SacOrGreg::Sac13] it returns that, otherwise [None].
pub fn sac13(&self) -> Option<Date> {
match self {
Self::Sac13(x) => Some(*x),
_ => None,
}
}
/// Parses various SAC13 and Gregorian Calendar formats.
///
/// ## Example
/// ```
/// use sac13::prelude::*;
///
/// let parsed = SacOrGreg::parse_str("2009-07-03").unwrap();
/// ```
///
/// ## Supported Formats
///
/// ### Nomenclature
/// ```text
/// ┌── leading whitespace
/// │
/// │ Separators trailing whitespace
/// ┌──┴─┐ ┌┤ │ ┌───┴───┐
/// " 23, 12 -2007 "
/// └┤ └┤ └─┬─┘
/// Components
/// ```
///
/// ### Exactly three numeric components
/// This function only allows dates that are represented by three numeric components,
/// including SAC13 years with millenium indicator (technically it's a base-26 digit).
/// In fact SAC13 years must be written with a millenium indicator because that is used
/// to disambiguate between SAC13 and Gregorian Calendar dates. Components must not
/// have thousands separators (or similar group separators).
///
/// Textual representations/components like Gregorian weekdays
/// or full month names are not supported!
///
/// ### Year first or last
/// The year component has to either be the first, or the third (and last) component.
/// It must not be the second (middle) component.
/// The allowed component orders are defined in the [ComponentOrder] enum.
///
/// If it's a SAC13 date the year must always be exactly four digits long `A000` - `Z999`.
/// For Gregorian Dates if any component is negative or zero, it is automatically assumed to be the year component.
/// Positive years must be at least three digits long so the component is clearly distinct from
/// the month and day component. If the year is positive but less than 100 it must be written with leading zeros.
///
/// Even though the actual supported formats allow for three digit Gregorian Calendar years,
/// or even one digit if the year is negative, the best practice is to pad years to at least
/// four digits.
///
/// The position of the year component implicitly defines the order of month and day,
/// because all components are either in acending or decending order.
/// There is one special case though: If it's Gregorian Calendar date and the separators between all
/// components are slashes, we assume the components to be a US formatted date with the order M/D/Y
///
/// ### Separators
/// All ASCII characters that are not letters or digits, are allowed as separators;
/// they are one or more (at most six) characters long and can be pretty arbitrary.
/// Again, even though you can use almost anything as separators doesn't mean you should.
/// You should especially avoid multi-character separators that end with dashes,
/// because they will be interpreted as a negative sign for the next component.
/// `YYYY-MM-DD` and `YYYY - MM - DD` are, of course, fine but using ` -` as a separator,
/// like in `YYYY -MM -DD`, will (obvously?) lead to said problem,
/// because it will be interpreted as three components separated by spaces, with at least two
/// of them being negative. Note that `+` is never interpreted as a sign.
/// So, something like `DD.- +MM+$%&/\YYYY` can be parsed, but please don't do that!
///
/// ### ASCII only
/// The entire input must be valid ASCII. If you have some weird format,
/// for example with emdash separators, you must replace them,
/// for example with regular hyphens, before parsing.
///
/// ### Whitespace
/// Only spaces (0x20) are considered "whitespace" by this method. Leading and trailing whitespace
/// are always trimmed, no matter how long. Whitespace in separators are considered part of the
/// separator and recorded as is in the [DateComponentSeparator].
#[must_use]
pub fn parse_str(input: &str) -> Option<ParsedSacOrGreg> {
if !input.is_ascii() {
return None;
}
let input_bytes = input.as_bytes();
let mut stream = crate::iterhelp::ByteSliceIter {
position: 0,
slice: input_bytes,
};
// trim leading spaces
stream.skip_bytes(b' ');
let c1 = ParsedComponent::parse(&mut stream)?;
let s1 = DateComponentSeparator::parse(&mut stream)?;
let c2 = ParsedComponent::parse(&mut stream)?;
let s2 = DateComponentSeparator::parse(&mut stream)?;
let c3 = ParsedComponent::parse(&mut stream)?;
// trim trailing spaces
stream.skip_bytes(b' ');
// assert end of stream
if stream.peek().is_some() {
// if it wasn't he end, something was wrong with the input
return None;
}
if c2.is_year_comp() {
// The middle part can never be the year
return None;
}
let year_first = c1.is_year_comp();
let year_last = c3.is_year_comp();
if year_first == year_last {
// either both ends or neither seem to be a year which is not allowed
return None;
}
// determine sort order
let (year, month, day, order) = if year_first {
(c1, c2, c3, ComponentOrder::YMD)
} else if c3.is_gregorian_year() && s1.is_single_slash() && s2.is_single_slash() {
// edge case to support typical Gregorian Calendar US format DD/MM/YYYY
(c3, c1, c2, ComponentOrder::MDY)
} else {
(c3, c2, c1, ComponentOrder::DMY)
};
if day.letter || month.letter {
return None;
}
if !(1..=31).contains(&day.value) || !(1..=13).contains(&month.value) {
return None;
}
let format = ParsedFormat {
separators: [s1, s2],
comp_ord: order,
len_day: NonZero::new(day.char_cnt)?,
len_month: NonZero::new(month.char_cnt)?,
len_year: NonZero::new(year.char_cnt)?,
};
let day = day.value as u8;
let month = month.value as u8;
let date = if year.is_sac13_year() {
SacOrGreg::Sac13(Date::from_ymd_untyped(year.value as u16, month, day)?)
} else {
SacOrGreg::Gregorian(GregorianDate::from_ymd(year.value, month, day)?)
};
Some(ParsedSacOrGreg { date, format })
}
}
impl Display for SacOrGreg {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SacOrGreg::Gregorian(x) => write!(f, "{x}"),
SacOrGreg::Sac13(x) => write!(f, "{x}"),
}
}
}