1use super::{DateTimeParseError, TimeStampOffset};
2use crate::parser::Span;
3use nom::{
4 bytes::complete::{tag, take_while_m_n},
5 character::complete::one_of,
6 combinator::{map_res, opt},
7 sequence::preceded,
8 IResult,
9};
10use std::{fmt::Display, str::FromStr};
11
12#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Time {
17 pub hour: u8,
19 pub minute: Option<u8>,
21 pub second: Option<u8>,
23 pub microsecond: Option<u32>,
25 pub offset: Option<TimeStampOffset>,
27}
28
29pub fn parse_time<'s>(
53 s: &'s str,
54 lenient_trailing_chars: bool,
55) -> Result<Time, DateTimeParseError> {
56 fn is_decimal_digit(c: char) -> bool {
57 c.is_ascii_digit()
58 }
59
60 fn from_digits<F: FromStr>(i: Span) -> Result<F, F::Err> {
61 i.input.parse::<F>()
62 }
63
64 fn digit2<F: FromStr>(input: Span) -> IResult<Span, F> {
65 map_res(take_while_m_n(2, 2, is_decimal_digit), from_digits::<F>)(input)
66 }
67
68 let s = Span::new(s);
69 let (s, hour): (Span, u8) = digit2(s).map_err(|_| DateTimeParseError::ParsingFailed("hour"))?;
70 let (s, minute): (Span, Option<u8>) =
71 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("minute"))?;
72 let (s, second): (Span, Option<u8>) =
73 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("second"))?;
74 let (s, second_fracs) = opt(preceded(tag("."), take_while_m_n(1, 4, is_decimal_digit)))(s)
75 .map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
76 DateTimeParseError::ParsingFailed("fractional seconds")
77 })?;
78 let (s, offset_dir) =
79 opt(one_of("+-"))(s).map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
80 DateTimeParseError::ParsingFailed("offset direction")
81 })?;
82
83 let offset_dir = match offset_dir.unwrap_or('+') {
84 '-' => -1i8,
85 _ => 1i8,
86 };
87 let (s, offset_hours): (Span, Option<i8>) =
88 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset hours"))?;
89 let offset_hours = offset_hours.map(|h| h * offset_dir);
90 let (s, offset_minutes): (Span, Option<u8>) =
91 opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset minutes"))?;
92
93 if !lenient_trailing_chars && !s.is_empty() {
94 return Err(DateTimeParseError::UnexpectedCharacter(
95 s.offset,
96 s.input.chars().next().unwrap_or_default(),
97 ));
98 }
99
100 let microsecond = match second_fracs {
101 Some(fracs) => {
102 let fracs_multiplier = match fracs.len() {
103 1 => 100_000,
104 2 => 10_000,
105 3 => 1_000,
106 4 => 100,
107 _ => panic!("second_fracs.len() not in 1..=4"),
108 };
109 Some(
110 fracs
111 .input
112 .parse::<u32>()
113 .expect("can parse fractional seconds as number")
114 * fracs_multiplier,
115 )
116 }
117 None => None,
118 };
119
120 let offset = match (offset_hours, offset_minutes) {
121 (Some(hours), Some(minutes)) => Some(TimeStampOffset { hours, minutes }),
122 _ => None,
123 };
124
125 Ok(Time {
126 hour,
127 minute,
128 second,
129 microsecond,
130 offset,
131 })
132}
133
134impl FromStr for Time {
136 type Err = DateTimeParseError;
137
138 fn from_str(s: &str) -> Result<Self, Self::Err> {
140 parse_time(s, false)
141 }
142}
143
144impl Display for Time {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 write!(f, "{:02}", self.hour)?;
148 if let Some(minute) = self.minute {
149 write!(f, "{:02}", minute)?;
150 if let Some(second) = self.second {
151 write!(f, "{:02}", second)?;
152 if let Some(microsecond) = self.microsecond {
153 let microsecond = format!("{:06}", microsecond);
154 write!(f, ".{}", µsecond[..4])?;
155 }
156 }
157 }
158 if let Some(offset) = &self.offset {
159 write!(f, "{}", offset)?;
160 }
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166mod test {
167 use super::*;
168 use pretty_assertions_sorted::assert_eq;
169
170 #[test]
171 fn can_parse_time_with_offsets() {
172 let ts = "195905.1234-0700";
173 let ts = parse_time(ts, false).expect("can parse time");
174
175 assert_eq!(ts.hour, 19);
176 assert_eq!(ts.minute, Some(59));
177 assert_eq!(ts.second, Some(5));
178 assert_eq!(ts.microsecond, Some(123_400));
179 assert_eq!(
180 ts.offset,
181 Some(TimeStampOffset {
182 hours: -7,
183 minutes: 0,
184 })
185 );
186 }
187}