use crate::errors::GraphDDBError;
pub fn normalize_datetime(value: &str) -> Result<String, GraphDDBError> {
let (y, mo, d, h, mi, s, ms) = parse_iso(value)?;
Ok(render(y, mo, d, h, mi, s, ms))
}
pub fn normalize_date(value: &str) -> Result<String, GraphDDBError> {
normalize_datetime(&format!("{value}T00:00:00.000Z"))
}
#[allow(clippy::type_complexity)]
fn parse_iso(value: &str) -> Result<(i64, u32, u32, u32, u32, u32, u32), GraphDDBError> {
let err = || GraphDDBError::hydration(format!("invalid ISO 8601 datetime: '{value}'"));
let core = value
.strip_suffix('Z')
.or_else(|| value.strip_suffix("+00:00"))
.unwrap_or(value);
let (date, time) = core.split_once('T').ok_or_else(err)?;
let mut dparts = date.split('-');
let y: i64 = dparts.next().ok_or_else(err)?.parse().map_err(|_| err())?;
let mo: u32 = dparts.next().ok_or_else(err)?.parse().map_err(|_| err())?;
let d: u32 = dparts.next().ok_or_else(err)?.parse().map_err(|_| err())?;
let (hms, frac) = match time.split_once('.') {
Some((a, b)) => (a, b),
None => (time, ""),
};
let mut tparts = hms.split(':');
let h: u32 = tparts.next().ok_or_else(err)?.parse().map_err(|_| err())?;
let mi: u32 = tparts.next().ok_or_else(err)?.parse().map_err(|_| err())?;
let s: u32 = tparts.next().unwrap_or("0").parse().map_err(|_| err())?;
let ms: u32 = if frac.is_empty() {
0
} else {
let digits: String = frac.chars().take(3).collect();
let padded = format!("{digits:0<3}");
padded.parse().map_err(|_| err())?
};
Ok((y, mo, d, h, mi, s, ms))
}
fn render(y: i64, mo: u32, d: u32, h: u32, mi: u32, s: u32, ms: u32) -> String {
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ms:03}Z")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn datetime_round_trips_to_millis_z() {
assert_eq!(
normalize_datetime("2020-01-01T00:00:00.000Z").unwrap(),
"2020-01-01T00:00:00.000Z"
);
assert_eq!(
normalize_datetime("2021-06-15T12:00:00Z").unwrap(),
"2021-06-15T12:00:00.000Z"
);
}
#[test]
fn date_becomes_midnight() {
assert_eq!(
normalize_date("2020-01-01").unwrap(),
"2020-01-01T00:00:00.000Z"
);
}
}