use winnow::{
combinator::{alt, opt, preceded},
error::ErrMode,
ModalResult, Parser,
};
use super::{
epoch::sec_and_nsec,
offset::{timezone_offset, Offset},
primitive::{colon, ctx_err, dec_uint, s},
};
#[derive(PartialEq, Clone, Debug, Default)]
pub(crate) struct Time {
pub(crate) hour: u8,
pub(crate) minute: u8,
pub(crate) second: u8,
pub(crate) nanosecond: u32,
pub(super) offset: Option<Offset>,
}
impl TryFrom<Time> for jiff::civil::Time {
type Error = &'static str;
fn try_from(time: Time) -> Result<Self, Self::Error> {
jiff::civil::Time::new(
time.hour as i8,
time.minute as i8,
time.second as i8,
time.nanosecond as i32,
)
.map_err(|_| "time is not valid")
}
}
#[derive(Clone)]
enum Meridiem {
Am,
Pm,
}
pub(crate) fn parse(input: &mut &str) -> ModalResult<Time> {
alt((am_pm_time, iso)).parse_next(input)
}
pub(super) fn iso(input: &mut &str) -> ModalResult<Time> {
alt((
(hour24, timezone_offset).map(|(hour, offset)| Time {
hour,
minute: 0,
second: 0,
nanosecond: 0,
offset: Some(offset),
}),
(
hour24,
colon,
minute,
opt(preceded(colon, second)),
opt(timezone_offset),
)
.map(|(hour, _, minute, sec_nsec, offset)| Time {
hour,
minute,
second: sec_nsec.map_or(0, |(s, _)| s),
nanosecond: sec_nsec.map_or(0, |(_, ns)| ns),
offset,
}),
))
.parse_next(input)
}
fn am_pm_time(input: &mut &str) -> ModalResult<Time> {
let (h, m, sec_nsec, meridiem) = (
hour12,
opt(preceded(colon, minute)),
opt(preceded(colon, second)),
alt((
s("am").value(Meridiem::Am),
s("a.m.").value(Meridiem::Am),
s("pm").value(Meridiem::Pm),
s("p.m.").value(Meridiem::Pm),
)),
)
.parse_next(input)?;
if h == 0 {
return Err(ErrMode::Cut(ctx_err(
"hour must be greater than 0 when meridiem is specified",
)));
}
let mut h = h % 12;
if let Meridiem::Pm = meridiem {
h += 12;
}
Ok(Time {
hour: h,
minute: m.unwrap_or(0),
second: sec_nsec.map_or(0, |(s, _)| s),
nanosecond: sec_nsec.map_or(0, |(_, ns)| ns),
offset: None,
})
}
pub(super) fn hour24(input: &mut &str) -> ModalResult<u8> {
s(dec_uint).verify(|x| *x < 24).parse_next(input)
}
fn hour12(input: &mut &str) -> ModalResult<u8> {
s(dec_uint).verify(|x| *x <= 12).parse_next(input)
}
pub(super) fn minute(input: &mut &str) -> ModalResult<u8> {
s(dec_uint).verify(|x| *x < 60).parse_next(input)
}
fn second(input: &mut &str) -> ModalResult<(u8, u32)> {
s(sec_and_nsec)
.verify_map(|(s, ns)| if s < 60 { Some((s as u8, ns)) } else { None })
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple() {
let reference = Time {
hour: 20,
minute: 2,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in [
"20:02:00.000000",
"20:02:00",
"20:02+:00",
"20:02-:00",
"20----:02--(these hyphens are ignored)--:00",
"20++++:02++(these plusses are ignored)++:00",
"20: (A comment!) 02 (Another comment!) :00",
"20:02 (A nested (comment!)) :00",
"20:02 (So (many (nested) comments!!!!)) :00",
"20 : 02 : 00.000000",
"20:02",
"20 : 02",
"8:02pm",
"8: 02 pm",
"8:02p.m.",
"8: 02 p.m.",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn invalid() {
assert!(parse(&mut "00:00am").is_err());
assert!(parse(&mut "00:00:00am").is_err());
}
#[test]
fn hours_only() {
let reference = Time {
hour: 11,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in [
"11am",
"11 am",
"11 - am",
"11 + am",
"11 a.m.",
"11 : 00",
"11:00:00",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn nanoseconds() {
let reference = Time {
hour: 11,
minute: 0,
second: 0,
nanosecond: 123450000,
offset: None,
};
for mut s in ["11:00:00.12345", "11:00:00.12345am"] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
let reference = Time {
hour: 11,
minute: 0,
second: 0,
nanosecond: 123456789,
offset: None,
};
for mut s in ["11:00:00.123456789", "11:00:00.1234567890123"] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn noon() {
let reference = Time {
hour: 12,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in [
"12:00",
"12pm",
"12 pm",
"12 (A comment!) pm",
"12 pm",
"12 p.m.",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn midnight() {
let reference = Time {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in ["00:00", "12am"] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_hours() {
let reference = Time {
hour: 1,
minute: 23,
second: 0,
nanosecond: 0,
offset: Some((false, 5, 0).try_into().unwrap()),
};
for mut s in [
"1:23+5",
"1:23 + 5",
"1:23+05",
"1:23 + 5 : 00",
"1:23+05:00",
"1:23+05:0",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_hours_and_minutes() {
let reference = Time {
hour: 3,
minute: 45,
second: 0,
nanosecond: 0,
offset: Some((false, 5, 35).try_into().unwrap()),
};
for mut s in [
"3:45+535",
"3:45-+535",
"03:45+535",
"3 : 45 + 535",
"3:45+0535",
"3:45+5:35",
"3:45+05:35",
"3:45 + 05 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_minutes() {
let reference = Time {
hour: 3,
minute: 45,
second: 0,
nanosecond: 0,
offset: Some((false, 0, 35).try_into().unwrap()),
};
for mut s in [
"3:45+035",
"03:45+035",
"3 : 45 + 035",
"3:45+0035",
"3:45+0:35",
"3:45+00:35",
"3:45 + 00 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_negative() {
let reference = Time {
hour: 3,
minute: 45,
second: 0,
nanosecond: 0,
offset: Some((true, 5, 35).try_into().unwrap()),
};
for mut s in [
"3:45-535",
"03:45-535",
"3 : 45 - 535",
"3:45-0535",
"3:45-5:35",
"3:45-05:35",
"3:45 - 05 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
}