use crate::diagnostics::DiagnosticCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Offset(pub i32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeRef {
Wall,
Standard,
Universal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeOfDay {
pub seconds: i32,
pub reference: TimeRef,
}
impl TimeOfDay {
pub fn zero() -> Self {
TimeOfDay {
seconds: 0,
reference: TimeRef::Wall,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Save {
pub seconds: i32,
pub is_dst: bool,
}
fn parse_hms(input: &str) -> std::result::Result<(i32, &str), String> {
let s = input;
let (neg, body) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s.strip_prefix('+').unwrap_or(s)),
};
let split = body
.find(|c: char| !(c.is_ascii_digit() || c == ':' || c == '.'))
.unwrap_or(body.len());
let (num, rest) = body.split_at(split);
if num.is_empty() {
return Err(format!("missing time value in {input:?}"));
}
let mut parts = num.split(':');
let h: i64 = parse_u(parts.next().unwrap_or(""))?;
let m: i64 = match parts.next() {
Some(p) => parse_u(p)?,
None => 0,
};
let (sec, frac) = match parts.next() {
Some(p) => match p.split_once('.') {
Some((whole, frac)) => (parse_u(whole)?, frac),
None => (parse_u(p)?, ""),
},
None => (0, ""),
};
if parts.next().is_some() {
return Err(format!("too many ':' groups in time {input:?}"));
}
if m >= 60 || sec >= 60 {
return Err(format!("minutes/seconds out of range in {input:?}"));
}
let mut total = h * 3600 + m * 60 + sec;
if !frac.is_empty() {
if !frac.bytes().all(|b| b.is_ascii_digit()) {
return Err(format!("invalid fractional seconds in {input:?}"));
}
let half = frac.as_bytes()[0] >= b'5';
if half {
total += 1;
}
}
let total = if neg { -total } else { total };
let total = i32::try_from(total).map_err(|_| format!("time {input:?} out of range"))?;
Ok((total, rest))
}
fn parse_u(s: &str) -> std::result::Result<i64, String> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(format!("invalid number {s:?}"));
}
s.parse::<i64>()
.map_err(|_| format!("number {s:?} out of range"))
}
pub fn parse_offset(input: &str) -> std::result::Result<Offset, (DiagnosticCode, String)> {
if input == "-" {
return Ok(Offset(0));
}
let (sec, rest) = parse_hms(input).map_err(|m| (DiagnosticCode::InvalidValue, m))?;
if !rest.is_empty() {
return Err((
DiagnosticCode::InvalidValue,
format!("unexpected suffix {rest:?} in offset {input:?}"),
));
}
Ok(Offset(sec))
}
pub fn parse_time_of_day(input: &str) -> std::result::Result<TimeOfDay, (DiagnosticCode, String)> {
if input == "-" {
return Ok(TimeOfDay::zero());
}
let (sec, rest) = parse_hms(input).map_err(|m| (DiagnosticCode::InvalidValue, m))?;
let reference = match rest {
"" | "w" => TimeRef::Wall,
"s" => TimeRef::Standard,
"u" | "g" | "z" => TimeRef::Universal,
other => {
return Err((
DiagnosticCode::InvalidTimeSuffix,
format!("invalid time suffix {other:?} in {input:?}"),
))
}
};
Ok(TimeOfDay {
seconds: sec,
reference,
})
}
pub fn parse_save(input: &str) -> std::result::Result<Save, (DiagnosticCode, String)> {
if input == "-" {
return Ok(Save {
seconds: 0,
is_dst: false,
});
}
let (sec, rest) = parse_hms(input).map_err(|m| (DiagnosticCode::InvalidValue, m))?;
let is_dst = match rest {
"s" => false,
"d" => true,
"" => sec != 0,
other => {
return Err((
DiagnosticCode::InvalidTimeSuffix,
format!("invalid save suffix {other:?} in {input:?}"),
))
}
};
Ok(Save {
seconds: sec,
is_dst,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offsets() {
assert_eq!(parse_offset("0").unwrap(), Offset(0));
assert_eq!(parse_offset("-").unwrap(), Offset(0));
assert_eq!(parse_offset("-5:00").unwrap(), Offset(-18000));
assert_eq!(parse_offset("5:30:15").unwrap(), Offset(19815));
assert_eq!(parse_offset("1").unwrap(), Offset(3600));
assert!(parse_offset("5:00s").is_err());
assert!(parse_offset("1:99").is_err());
}
#[test]
fn times_with_suffix() {
assert_eq!(parse_time_of_day("2:00").unwrap().reference, TimeRef::Wall);
assert_eq!(
parse_time_of_day("2:00s").unwrap().reference,
TimeRef::Standard
);
assert_eq!(
parse_time_of_day("2:00u").unwrap().reference,
TimeRef::Universal
);
assert_eq!(parse_time_of_day("24:00").unwrap().seconds, 86400);
assert_eq!(parse_time_of_day("-2:30").unwrap().seconds, -9000);
assert!(parse_time_of_day("2:00x").is_err());
}
#[test]
fn save_dst_flag() {
assert!(!parse_save("0").unwrap().is_dst);
assert!(parse_save("1:00").unwrap().is_dst);
assert!(!parse_save("1:00s").unwrap().is_dst);
assert!(parse_save("0d").unwrap().is_dst);
assert_eq!(parse_save("-1:00").unwrap().seconds, -3600);
}
#[test]
fn fractional_rounds_to_nearest() {
assert_eq!(parse_time_of_day("0:00:00.4").unwrap().seconds, 0);
assert_eq!(parse_time_of_day("0:00:00.5").unwrap().seconds, 1);
}
}