1use crate::parser::Span;
2use nom::{
3 bytes::complete::{tag, take_while_m_n},
4 character::complete::one_of,
5 combinator::{map_res, opt},
6 sequence::preceded,
7 IResult,
8};
9use std::{fmt::Display, str::FromStr};
10
11use super::DateTimeParseError;
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct TimeStampOffset {
17 pub hours: i8,
20 pub minutes: u8,
22}
23
24impl Display for TimeStampOffset {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "{:+03}{:02}", self.hours, self.minutes)
27 }
28}
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct TimeStamp {
35 pub year: u16,
37 pub month: Option<u8>,
39 pub day: Option<u8>,
41 pub hour: Option<u8>,
43 pub minute: Option<u8>,
45 pub second: Option<u8>,
47 pub microsecond: Option<u32>,
49 pub offset: Option<TimeStampOffset>,
51}
52
53pub fn parse_timestamp<'s>(
80 s: &'s str,
81 lenient_trailing_chars: bool,
82) -> Result<TimeStamp, DateTimeParseError> {
83 fn is_decimal_digit(c: char) -> bool {
84 c.is_ascii_digit()
85 }
86
87 fn from_digits<F: FromStr>(i: Span) -> Result<F, F::Err> {
88 i.input.parse::<F>()
89 }
90
91 fn digit2<F: FromStr>(input: Span) -> IResult<Span, F> {
92 map_res(take_while_m_n(2, 2, is_decimal_digit), from_digits::<F>)(input)
93 }
94
95 fn digit4<F: FromStr>(input: Span) -> IResult<Span, F> {
96 map_res(take_while_m_n(4, 4, is_decimal_digit), from_digits::<F>)(input)
97 }
98
99 let s = Span::new(s);
100 let (s, year): (Span, u16) =
101 digit4(s).map_err(|_| DateTimeParseError::ParsingFailed("year"))?;
102 let (s, month): (Span, Option<u8>) =
103 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("month"))?;
104 let (s, day): (Span, Option<u8>) =
105 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("day"))?;
106 let (s, hour): (Span, Option<u8>) =
107 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("hour"))?;
108 let (s, minute): (Span, Option<u8>) =
109 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("minute"))?;
110 let (s, second): (Span, Option<u8>) =
111 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("second"))?;
112 let (s, second_fracs) = opt(preceded(tag("."), take_while_m_n(1, 4, is_decimal_digit)))(s)
113 .map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
114 DateTimeParseError::ParsingFailed("fractional seconds")
115 })?;
116 let (s, offset_dir) =
117 opt(one_of("+-"))(s).map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
118 DateTimeParseError::ParsingFailed("offset direction")
119 })?;
120
121 let offset_dir = match offset_dir.unwrap_or('+') {
122 '-' => -1i8,
123 _ => 1i8,
124 };
125 let (s, offset_hours): (Span, Option<i8>) =
126 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset hours"))?;
127 let offset_hours = offset_hours.map(|h| h * offset_dir);
128 let (s, offset_minutes): (Span, Option<u8>) =
129 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset minutes"))?;
130
131 if !lenient_trailing_chars && !s.is_empty() {
132 return Err(DateTimeParseError::UnexpectedCharacter(
133 s.offset,
134 s.input.chars().next().unwrap_or_default(),
135 ));
136 }
137
138 let microsecond = match second_fracs {
139 Some(fracs) => {
140 let fracs_multiplier = match fracs.len() {
141 1 => 100_000,
142 2 => 10_000,
143 3 => 1_000,
144 4 => 100,
145 _ => panic!("second_fracs.len() not in 1..=4"),
146 };
147 Some(
148 fracs
149 .input
150 .parse::<u32>()
151 .expect("can parse fractional seconds as number")
152 * fracs_multiplier,
153 )
154 }
155 None => None,
156 };
157
158 let offset = match (offset_hours, offset_minutes) {
159 (Some(hours), Some(minutes)) => Some(TimeStampOffset { hours, minutes }),
160 _ => None,
161 };
162
163 Ok(TimeStamp {
164 year,
165 month,
166 day,
167 hour,
168 minute,
169 second,
170 microsecond,
171 offset,
172 })
173}
174
175impl FromStr for TimeStamp {
177 type Err = DateTimeParseError;
178
179 fn from_str(s: &str) -> Result<Self, Self::Err> {
181 parse_timestamp(s, false)
182 }
183}
184
185impl Display for TimeStamp {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 write!(f, "{:04}", self.year)?;
189 if let Some(month) = self.month {
190 write!(f, "{:02}", month)?;
191 if let Some(day) = self.day {
192 write!(f, "{:02}", day)?;
193 if let Some(hour) = self.hour {
194 write!(f, "{:02}", hour)?;
195 if let Some(minute) = self.minute {
196 write!(f, "{:02}", minute)?;
197 if let Some(second) = self.second {
198 write!(f, "{:02}", second)?;
199 if let Some(microsecond) = self.microsecond {
200 let microsecond = format!("{:06}", microsecond);
201 write!(f, ".{}", µsecond[..4])?;
202 }
203 }
204 }
205 }
206 }
207 }
208 if let Some(offset) = &self.offset {
209 write!(f, "{}", offset)?;
210 }
211 Ok(())
212 }
213}
214
215#[cfg(test)]
216mod test {
217 use super::*;
218 use pretty_assertions_sorted::assert_eq;
219
220 #[test]
221 fn can_parse_time_with_offsets() {
222 let ts = "20230312195905.1234-0700";
223 let ts = parse_timestamp(ts, false).expect("can parse timestamp");
224
225 assert_eq!(ts.year, 2023);
226 assert_eq!(ts.month, Some(3));
227 assert_eq!(ts.day, Some(12));
228 assert_eq!(ts.hour, Some(19));
229 assert_eq!(ts.minute, Some(59));
230 assert_eq!(ts.second, Some(5));
231 assert_eq!(ts.microsecond, Some(123_400));
232 assert_eq!(
233 ts.offset,
234 Some(TimeStampOffset {
235 hours: -7,
236 minutes: 0,
237 })
238 );
239 }
240
241 #[test]
242 fn can_parse_time_without_offsets() {
243 let ts = "20230312195905.1234";
244 let ts = parse_timestamp(ts, false).expect("can parse timestamp");
245
246 assert_eq!(ts.year, 2023);
247 assert_eq!(ts.month, Some(3));
248 assert_eq!(ts.day, Some(12));
249 assert_eq!(ts.hour, Some(19));
250 assert_eq!(ts.minute, Some(59));
251 assert_eq!(ts.second, Some(5));
252 assert_eq!(ts.microsecond, Some(123_400));
253 assert_eq!(ts.offset, None);
254 }
255
256 #[test]
257 fn can_parse_time_without_offsets_or_fractional_seconds() {
258 let ts = "20230312195905";
259 let ts = parse_timestamp(ts, false).expect("can parse timestamp");
260
261 assert_eq!(ts.year, 2023);
262 assert_eq!(ts.month, Some(3));
263 assert_eq!(ts.day, Some(12));
264 assert_eq!(ts.hour, Some(19));
265 assert_eq!(ts.minute, Some(59));
266 assert_eq!(ts.second, Some(5));
267 assert_eq!(ts.microsecond, None);
268 assert_eq!(ts.offset, None);
269 }
270
271 #[test]
272 fn can_parse_time_with_offsets_without_fractional_seconds() {
273 let ts = "20230312195905-0700";
274 let ts = parse_timestamp(ts, false).expect("can parse timestamp");
275
276 assert_eq!(ts.year, 2023);
277 assert_eq!(ts.month, Some(3));
278 assert_eq!(ts.day, Some(12));
279 assert_eq!(ts.hour, Some(19));
280 assert_eq!(ts.minute, Some(59));
281 assert_eq!(ts.second, Some(5));
282 assert_eq!(ts.microsecond, None);
283 assert_eq!(
284 ts.offset,
285 Some(TimeStampOffset {
286 hours: -7,
287 minutes: 0,
288 })
289 );
290 }
291
292 #[test]
293 fn can_parse_time_with_only_year() {
294 let ts = "2023";
295 let ts = parse_timestamp(ts, false).expect("can parse timestamp");
296
297 assert_eq!(ts.year, 2023);
298 assert_eq!(ts.month, None);
299 assert_eq!(ts.day, None);
300 assert_eq!(ts.hour, None);
301 assert_eq!(ts.minute, None);
302 assert_eq!(ts.second, None);
303 assert_eq!(ts.microsecond, None);
304 assert_eq!(ts.offset, None);
305 }
306
307 #[test]
308 fn cant_parse_bad_timestamps() {
309 assert!(parse_timestamp("23", false).is_err());
310 assert!(parse_timestamp("abcd", false).is_err());
311 assert!(parse_timestamp("202303121959051", false).is_err());
312 }
313
314 #[test]
315 fn can_parse_timestamp_fromstr() {
316 let ts: TimeStamp = "20230312195905.1234-0700"
317 .parse()
318 .expect("can parse timestamp");
319
320 assert_eq!(ts.year, 2023);
321 assert_eq!(ts.month, Some(3));
322 assert_eq!(ts.day, Some(12));
323 assert_eq!(ts.hour, Some(19));
324 assert_eq!(ts.minute, Some(59));
325 assert_eq!(ts.second, Some(5));
326 assert_eq!(ts.microsecond, Some(123_400));
327 assert_eq!(
328 ts.offset,
329 Some(TimeStampOffset {
330 hours: -7,
331 minutes: 0,
332 })
333 );
334 }
335
336 #[test]
337 fn can_format_timestamp() {
338 let ts = TimeStamp {
339 year: 2023,
340 month: Some(3),
341 day: Some(12),
342 hour: Some(19),
343 minute: Some(59),
344 second: Some(5),
345 microsecond: Some(123_400),
346 offset: Some(TimeStampOffset {
347 hours: -7,
348 minutes: 0,
349 }),
350 };
351 assert_eq!(ts.to_string(), "20230312195905.1234-0700");
352
353 let ts = TimeStamp {
354 year: 2023,
355 month: Some(3),
356 day: Some(12),
357 hour: Some(19),
358 minute: None,
359 second: None,
360 microsecond: None,
361 offset: Some(TimeStampOffset {
362 hours: -7,
363 minutes: 0,
364 }),
365 };
366 assert_eq!(ts.to_string(), "2023031219-0700");
367 }
368}