1use crate::diagnostics::DiagnosticCode;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Offset(pub i32);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TimeRef {
19 Wall,
21 Standard,
23 Universal,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct TimeOfDay {
30 pub seconds: i32,
31 pub reference: TimeRef,
32}
33
34impl TimeOfDay {
35 pub fn zero() -> Self {
37 TimeOfDay {
38 seconds: 0,
39 reference: TimeRef::Wall,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct Save {
50 pub seconds: i32,
51 pub is_dst: bool,
52}
53
54fn parse_hms(input: &str) -> std::result::Result<(i32, &str), String> {
59 let s = input;
60 let (neg, body) = match s.strip_prefix('-') {
61 Some(rest) => (true, rest),
62 None => (false, s.strip_prefix('+').unwrap_or(s)),
63 };
64
65 let split = body
67 .find(|c: char| !(c.is_ascii_digit() || c == ':' || c == '.'))
68 .unwrap_or(body.len());
69 let (num, rest) = body.split_at(split);
70 if num.is_empty() {
71 return Err(format!("missing time value in {input:?}"));
72 }
73
74 let mut parts = num.split(':');
75 let h: i64 = parse_u(parts.next().unwrap_or(""))?;
76 let m: i64 = match parts.next() {
77 Some(p) => parse_u(p)?,
78 None => 0,
79 };
80 let (sec, frac) = match parts.next() {
82 Some(p) => match p.split_once('.') {
83 Some((whole, frac)) => (parse_u(whole)?, frac),
84 None => (parse_u(p)?, ""),
85 },
86 None => (0, ""),
87 };
88 if parts.next().is_some() {
89 return Err(format!("too many ':' groups in time {input:?}"));
90 }
91 if m >= 60 || sec >= 60 {
92 return Err(format!("minutes/seconds out of range in {input:?}"));
94 }
95
96 let mut total = h * 3600 + m * 60 + sec;
97 if !frac.is_empty() {
99 if !frac.bytes().all(|b| b.is_ascii_digit()) {
100 return Err(format!("invalid fractional seconds in {input:?}"));
101 }
102 let half = frac.as_bytes()[0] >= b'5';
103 if half {
104 total += 1;
105 }
106 }
107
108 let total = if neg { -total } else { total };
109 let total = i32::try_from(total).map_err(|_| format!("time {input:?} out of range"))?;
110 Ok((total, rest))
111}
112
113fn parse_u(s: &str) -> std::result::Result<i64, String> {
114 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
115 return Err(format!("invalid number {s:?}"));
116 }
117 s.parse::<i64>()
118 .map_err(|_| format!("number {s:?} out of range"))
119}
120
121pub fn parse_offset(input: &str) -> std::result::Result<Offset, (DiagnosticCode, String)> {
123 if input == "-" {
124 return Ok(Offset(0));
125 }
126 let (sec, rest) = parse_hms(input).map_err(|m| (DiagnosticCode::InvalidValue, m))?;
127 if !rest.is_empty() {
128 return Err((
129 DiagnosticCode::InvalidValue,
130 format!("unexpected suffix {rest:?} in offset {input:?}"),
131 ));
132 }
133 Ok(Offset(sec))
134}
135
136pub fn parse_time_of_day(input: &str) -> std::result::Result<TimeOfDay, (DiagnosticCode, String)> {
138 if input == "-" {
139 return Ok(TimeOfDay::zero());
140 }
141 let (sec, rest) = parse_hms(input).map_err(|m| (DiagnosticCode::InvalidValue, m))?;
142 let reference = match rest {
143 "" | "w" => TimeRef::Wall,
144 "s" => TimeRef::Standard,
145 "u" | "g" | "z" => TimeRef::Universal,
146 other => {
147 return Err((
148 DiagnosticCode::InvalidTimeSuffix,
149 format!("invalid time suffix {other:?} in {input:?}"),
150 ))
151 }
152 };
153 Ok(TimeOfDay {
154 seconds: sec,
155 reference,
156 })
157}
158
159pub fn parse_save(input: &str) -> std::result::Result<Save, (DiagnosticCode, String)> {
161 if input == "-" {
162 return Ok(Save {
163 seconds: 0,
164 is_dst: false,
165 });
166 }
167 let (sec, rest) = parse_hms(input).map_err(|m| (DiagnosticCode::InvalidValue, m))?;
168 let is_dst = match rest {
169 "s" => false,
171 "d" => true,
172 "" => sec != 0,
173 other => {
174 return Err((
175 DiagnosticCode::InvalidTimeSuffix,
176 format!("invalid save suffix {other:?} in {input:?}"),
177 ))
178 }
179 };
180 Ok(Save {
181 seconds: sec,
182 is_dst,
183 })
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn offsets() {
192 assert_eq!(parse_offset("0").unwrap(), Offset(0));
193 assert_eq!(parse_offset("-").unwrap(), Offset(0));
194 assert_eq!(parse_offset("-5:00").unwrap(), Offset(-18000));
195 assert_eq!(parse_offset("5:30:15").unwrap(), Offset(19815));
196 assert_eq!(parse_offset("1").unwrap(), Offset(3600));
197 assert!(parse_offset("5:00s").is_err());
198 assert!(parse_offset("1:99").is_err());
199 }
200
201 #[test]
202 fn times_with_suffix() {
203 assert_eq!(parse_time_of_day("2:00").unwrap().reference, TimeRef::Wall);
204 assert_eq!(
205 parse_time_of_day("2:00s").unwrap().reference,
206 TimeRef::Standard
207 );
208 assert_eq!(
209 parse_time_of_day("2:00u").unwrap().reference,
210 TimeRef::Universal
211 );
212 assert_eq!(parse_time_of_day("24:00").unwrap().seconds, 86400);
213 assert_eq!(parse_time_of_day("-2:30").unwrap().seconds, -9000);
214 assert!(parse_time_of_day("2:00x").is_err());
215 }
216
217 #[test]
218 fn save_dst_flag() {
219 assert!(!parse_save("0").unwrap().is_dst);
220 assert!(parse_save("1:00").unwrap().is_dst);
221 assert!(!parse_save("1:00s").unwrap().is_dst);
222 assert!(parse_save("0d").unwrap().is_dst);
223 assert_eq!(parse_save("-1:00").unwrap().seconds, -3600);
224 }
225
226 #[test]
227 fn fractional_rounds_to_nearest() {
228 assert_eq!(parse_time_of_day("0:00:00.4").unwrap().seconds, 0);
229 assert_eq!(parse_time_of_day("0:00:00.5").unwrap().seconds, 1);
230 }
231}