use crate::models::expression::is_strict_expr;
use serde::{de, Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
pub struct Duration {
#[serde(skip_serializing_if = "Option::is_none")]
pub days: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hours: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minutes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub milliseconds: Option<u64>,
}
impl<'de> de::Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct DurationVisitor;
impl<'de> de::Visitor<'de> for DurationVisitor {
type Value = Duration;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a duration object with at least one property")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut days: Option<u64> = None;
let mut hours: Option<u64> = None;
let mut minutes: Option<u64> = None;
let mut seconds: Option<u64> = None;
let mut milliseconds: Option<u64> = None;
let mut has_key = false;
while let Some(key) = map.next_key::<String>()? {
has_key = true;
match key.as_str() {
"days" => {
days = Some(map.next_value()?);
}
"hours" => {
hours = Some(map.next_value()?);
}
"minutes" => {
minutes = Some(map.next_value()?);
}
"seconds" => {
seconds = Some(map.next_value()?);
}
"milliseconds" => {
milliseconds = Some(map.next_value()?);
}
other => {
return Err(de::Error::custom(format!(
"unexpected key '{}' in duration object",
other
)));
}
}
}
if !has_key {
return Err(de::Error::custom(
"duration object must include at least one property",
));
}
Ok(Duration {
days,
hours,
minutes,
seconds,
milliseconds,
})
}
}
deserializer.deserialize_map(DurationVisitor)
}
}
macro_rules! from_unit {
($name:ident, $field:ident) => {
pub fn $name(v: u64) -> Self {
Self {
$field: Some(v),
..Self::default()
}
}
};
}
macro_rules! total_as {
($name:ident, $divisor:expr) => {
pub fn $name(&self) -> f64 {
self.total_milliseconds() as f64 / $divisor
}
};
}
impl Duration {
from_unit!(from_days, days);
from_unit!(from_hours, hours);
from_unit!(from_minutes, minutes);
from_unit!(from_seconds, seconds);
from_unit!(from_milliseconds, milliseconds);
total_as!(total_days, 24.0 * 60.0 * 60.0 * 1000.0);
total_as!(total_hours, 60.0 * 60.0 * 1000.0);
total_as!(total_minutes, 60.0 * 1000.0);
total_as!(total_seconds, 1000.0);
pub fn total_milliseconds(&self) -> u64 {
let total: u128 = (self.days.unwrap_or(0) as u128) * 86_400_000
+ (self.hours.unwrap_or(0) as u128) * 3_600_000
+ (self.minutes.unwrap_or(0) as u128) * 60_000
+ (self.seconds.unwrap_or(0) as u128) * 1_000
+ self.milliseconds.unwrap_or(0) as u128;
total.try_into().unwrap_or(u64::MAX)
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if let Some(days) = self.days {
parts.push(format!("{} days", days));
}
if let Some(hours) = self.hours {
parts.push(format!("{} hours", hours));
}
if let Some(minutes) = self.minutes {
parts.push(format!("{} minutes", minutes));
}
if let Some(seconds) = self.seconds {
parts.push(format!("{} seconds", seconds));
}
if let Some(milliseconds) = self.milliseconds {
parts.push(format!("{} milliseconds", milliseconds));
}
write!(f, "{}", parts.join(" "))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OneOfDurationOrIso8601Expression {
Duration(Duration),
Iso8601Expression(String),
}
impl<'de> de::Deserialize<'de> for OneOfDurationOrIso8601Expression {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OneOfDurationVisitor;
impl<'de> de::Visitor<'de> for OneOfDurationVisitor {
type Value = OneOfDurationOrIso8601Expression;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a duration object or an ISO 8601 duration string")
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let duration = Duration::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(OneOfDurationOrIso8601Expression::Duration(duration))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if is_strict_expr(v) {
return Ok(OneOfDurationOrIso8601Expression::Iso8601Expression(
v.to_string(),
));
}
if !is_iso8601_duration_valid(v) {
return Err(de::Error::custom(format!(
"invalid ISO 8601 duration expression: '{}'",
v
)));
}
Ok(OneOfDurationOrIso8601Expression::Iso8601Expression(
v.to_string(),
))
}
}
deserializer.deserialize_any(OneOfDurationVisitor)
}
}
impl serde::Serialize for OneOfDurationOrIso8601Expression {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
OneOfDurationOrIso8601Expression::Duration(d) => d.serialize(serializer),
OneOfDurationOrIso8601Expression::Iso8601Expression(s) => serializer.serialize_str(s),
}
}
}
impl Default for OneOfDurationOrIso8601Expression {
fn default() -> Self {
OneOfDurationOrIso8601Expression::Duration(Duration::default())
}
}
impl fmt::Display for OneOfDurationOrIso8601Expression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OneOfDurationOrIso8601Expression::Duration(duration) => write!(f, "{}", duration),
OneOfDurationOrIso8601Expression::Iso8601Expression(expr) => write!(f, "{}", expr),
}
}
}
impl From<Duration> for OneOfDurationOrIso8601Expression {
fn from(duration: Duration) -> Self {
OneOfDurationOrIso8601Expression::Duration(duration)
}
}
impl OneOfDurationOrIso8601Expression {
pub fn total_milliseconds(&self) -> u64 {
match self {
OneOfDurationOrIso8601Expression::Duration(d) => d.total_milliseconds(),
OneOfDurationOrIso8601Expression::Iso8601Expression(_) => 0,
}
}
pub fn is_duration(&self) -> bool {
matches!(self, OneOfDurationOrIso8601Expression::Duration(_))
}
pub fn is_iso8601(&self) -> bool {
matches!(self, OneOfDurationOrIso8601Expression::Iso8601Expression(_))
}
pub fn as_iso8601(&self) -> Option<&str> {
match self {
OneOfDurationOrIso8601Expression::Iso8601Expression(s) => Some(s),
_ => None,
}
}
}
pub fn is_iso8601_duration_valid(s: &str) -> bool {
if !s.starts_with('P') {
return false;
}
let rest = &s[1..];
if rest.is_empty() {
return false; }
let (date_part, time_part) = if let Some(t_idx) = rest.find('T') {
let date = &rest[..t_idx];
let time = &rest[t_idx + 1..];
if time.is_empty() {
return false; }
(date, Some(time))
} else {
(rest, None)
};
if !date_part.is_empty() {
let days_str = date_part.trim_end_matches('D');
if days_str.is_empty() && date_part.ends_with('D') {
return false; }
if !days_str.is_empty() {
if days_str.parse::<u64>().is_err() {
return false;
}
}
if date_part.contains('Y') || date_part.contains('W') || date_part.contains('M') {
return false;
}
}
if let Some(time) = time_part {
let remaining = parse_time_components(time);
if remaining.is_none() {
return false;
}
if let Some(remaining) = remaining {
if !remaining.is_empty() {
return false;
}
}
}
true
}
fn parse_time_components(mut s: &str) -> Option<&str> {
while !s.is_empty() {
let (num_str, rest) = split_number_prefix(s)?;
if num_str.is_empty() {
return None; }
if num_str.parse::<f64>().is_err() {
return None;
}
if let Some(rest_after_ms) = rest.strip_prefix("MS") {
s = rest_after_ms;
} else if let Some(rest_after_h) = rest.strip_prefix('H') {
s = rest_after_h;
} else if let Some(rest_after_m) = rest.strip_prefix('M') {
s = rest_after_m;
} else if let Some(rest_after_s) = rest.strip_prefix('S') {
s = rest_after_s;
} else {
return Some(s);
}
}
Some(s) }
fn split_number_prefix(s: &str) -> Option<(&str, &str)> {
let mut i = 0;
let bytes = s.as_bytes();
if i < bytes.len() && bytes[i] == b'-' {
i += 1;
}
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
}
if i == 0 {
return None;
}
Some((&s[..i], &s[i..]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_duration_from_days() {
let d = Duration::from_days(1);
assert_eq!(d.days, Some(1));
assert_eq!(d.total_milliseconds(), 86400000);
}
#[test]
fn test_duration_from_hours() {
let d = Duration::from_hours(2);
assert_eq!(d.hours, Some(2));
assert_eq!(d.total_milliseconds(), 7200000);
}
#[test]
fn test_duration_from_minutes() {
let d = Duration::from_minutes(5);
assert_eq!(d.minutes, Some(5));
assert_eq!(d.total_milliseconds(), 300000);
}
#[test]
fn test_duration_from_seconds() {
let d = Duration::from_seconds(30);
assert_eq!(d.seconds, Some(30));
assert_eq!(d.total_milliseconds(), 30000);
}
#[test]
fn test_duration_from_milliseconds() {
let d = Duration::from_milliseconds(500);
assert_eq!(d.milliseconds, Some(500));
assert_eq!(d.total_milliseconds(), 500);
}
#[test]
fn test_duration_composite() {
let d = Duration {
days: Some(1),
hours: Some(2),
minutes: Some(30),
seconds: Some(45),
milliseconds: Some(500),
};
let expected = 86400000 + 7200000 + 1800000 + 45000 + 500;
assert_eq!(d.total_milliseconds(), expected);
}
#[test]
fn test_duration_total_conversions() {
let d = Duration::from_minutes(90);
assert_eq!(d.total_hours(), 1.5);
assert_eq!(d.total_minutes(), 90.0);
assert_eq!(d.total_seconds(), 5400.0);
}
#[test]
fn test_duration_serialize() {
let d = Duration::from_seconds(30);
let json = serde_json::to_string(&d).unwrap();
assert_eq!(json, r#"{"seconds":30}"#);
}
#[test]
fn test_duration_deserialize() {
let json = r#"{"minutes": 5, "seconds": 30}"#;
let d: Duration = serde_json::from_str(json).unwrap();
assert_eq!(d.minutes, Some(5));
assert_eq!(d.seconds, Some(30));
}
#[test]
fn test_duration_empty_object_rejected() {
let json = r#"{}"#;
let result: Result<Duration, _> = serde_json::from_str(json);
assert!(result.is_err(), "empty duration object should be rejected");
let err = result.unwrap_err().to_string();
assert!(
err.contains("at least one property"),
"expected 'at least one property' error, got: {}",
err
);
}
#[test]
fn test_duration_unknown_key_rejected() {
let json = r#"{"after": "PT1S"}"#;
let result: Result<Duration, _> = serde_json::from_str(json);
assert!(
result.is_err(),
"unknown key in duration object should be rejected"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("unexpected key"),
"expected 'unexpected key' error, got: {}",
err
);
}
#[test]
fn test_duration_unknown_key_mixed_rejected() {
let json = r#"{"seconds": 30, "duration": "PT1S"}"#;
let result: Result<Duration, _> = serde_json::from_str(json);
assert!(
result.is_err(),
"unknown key mixed with valid keys should be rejected"
);
}
#[test]
fn test_duration_default() {
let d = Duration::default();
assert_eq!(d.total_milliseconds(), 0);
}
#[test]
fn test_oneof_duration_serialize_struct() {
let oneof = OneOfDurationOrIso8601Expression::Duration(Duration::from_seconds(30));
let json = serde_json::to_string(&oneof).unwrap();
assert_eq!(json, r#"{"seconds":30}"#);
}
#[test]
fn test_oneof_duration_serialize_iso8601() {
let oneof = OneOfDurationOrIso8601Expression::Iso8601Expression("PT5M".to_string());
let json = serde_json::to_string(&oneof).unwrap();
assert_eq!(json, r#""PT5M""#);
}
#[test]
fn test_oneof_duration_deserialize_struct() {
let json = r#"{"seconds": 30}"#;
let oneof: OneOfDurationOrIso8601Expression = serde_json::from_str(json).unwrap();
match oneof {
OneOfDurationOrIso8601Expression::Duration(d) => {
assert_eq!(d.seconds, Some(30));
}
_ => panic!("Expected Duration variant"),
}
}
#[test]
fn test_oneof_duration_deserialize_iso8601() {
let json = r#""PT5M""#;
let oneof: OneOfDurationOrIso8601Expression = serde_json::from_str(json).unwrap();
match oneof {
OneOfDurationOrIso8601Expression::Iso8601Expression(s) => {
assert_eq!(s, "PT5M");
}
_ => panic!("Expected Iso8601Expression variant"),
}
}
#[test]
fn test_duration_display() {
let d = Duration {
hours: Some(2),
minutes: Some(30),
..Default::default()
};
let display = format!("{}", d);
assert!(display.contains("2 hours"));
assert!(display.contains("30 minutes"));
}
#[test]
fn test_oneof_iso8601_valid_patterns() {
let valid_cases = vec![
("\"P1D\"", "P1D"),
("\"P1DT12H30M\"", "P1DT12H30M"),
("\"PT1H\"", "PT1H"),
("\"PT250MS\"", "PT250MS"),
("\"P3DT4H5M6S250MS\"", "P3DT4H5M6S250MS"),
];
for (json, expected) in valid_cases {
let oneof: OneOfDurationOrIso8601Expression = serde_json::from_str(json).unwrap();
match &oneof {
OneOfDurationOrIso8601Expression::Iso8601Expression(s) => {
assert_eq!(s, expected, "expected ISO expression {}", expected);
}
_ => panic!("Expected Iso8601Expression variant for {}", expected),
}
}
}
#[test]
fn test_oneof_iso8601_rejected_patterns() {
let rejected = vec![
"\"P2Y\"", "\"P1Y2M3D\"", "\"P1W\"", "\"1Y\"", ];
for json in rejected {
let result: Result<OneOfDurationOrIso8601Expression, _> = serde_json::from_str(json);
assert!(result.is_err(), "expected {} to fail deserialization", json);
}
}
#[test]
fn test_duration_composite_with_all_fields() {
let d = Duration {
days: Some(3),
hours: Some(4),
minutes: Some(5),
seconds: Some(6),
milliseconds: Some(250),
};
let expected = 3 * 86400000 + 4 * 3600000 + 5 * 60000 + 6 * 1000 + 250;
assert_eq!(d.total_milliseconds(), expected);
}
#[test]
fn test_iso8601_duration_valid_patterns() {
assert!(is_iso8601_duration_valid("P1D"), "P1D should be valid");
assert!(
is_iso8601_duration_valid("P1DT12H30M"),
"P1DT12H30M should be valid"
);
assert!(is_iso8601_duration_valid("PT1H"), "PT1H should be valid");
assert!(
is_iso8601_duration_valid("PT250MS"),
"PT250MS should be valid"
);
assert!(
is_iso8601_duration_valid("P3DT4H5M6S250MS"),
"P3DT4H5M6S250MS should be valid"
);
assert!(is_iso8601_duration_valid("PT30S"), "PT30S should be valid");
assert!(
is_iso8601_duration_valid("PT0.1S"),
"PT0.1S should be valid"
);
assert!(
is_iso8601_duration_valid("P1DT2H30M"),
"P1DT2H30M should be valid"
);
}
#[test]
fn test_iso8601_duration_invalid_patterns() {
assert!(!is_iso8601_duration_valid("P2Y"), "years not supported");
assert!(
!is_iso8601_duration_valid("P1Y2M3D"),
"months not supported in date part"
);
assert!(!is_iso8601_duration_valid("P1W"), "weeks not supported");
assert!(
!is_iso8601_duration_valid("P1Y2M3D4H"),
"years+months not supported"
);
assert!(
!is_iso8601_duration_valid("P1Y2M3D4H5M6S"),
"years+months not supported"
);
assert!(!is_iso8601_duration_valid("P"), "bare P is invalid");
assert!(!is_iso8601_duration_valid("P1DT"), "bare PT is invalid");
assert!(!is_iso8601_duration_valid("1Y"), "missing P prefix");
assert!(!is_iso8601_duration_valid(""), "empty string is invalid");
assert!(
!is_iso8601_duration_valid("P1.5D"),
"fractional days not supported"
);
assert!(
!is_iso8601_duration_valid("P1M"),
"months (M in date part) not supported"
);
assert!(
!is_iso8601_duration_valid("P1DT2H3M4S5MS7"),
"trailing garbage after MS not valid"
);
}
#[test]
fn test_iso8601_non_iso_rejected_patterns() {
assert!(
!is_iso8601_duration_valid("10s"),
"non-ISO '10s' should be rejected"
);
assert!(
!is_iso8601_duration_valid("150ms"),
"non-ISO '150ms' should be rejected"
);
assert!(
!is_iso8601_duration_valid("1Y"),
"non-ISO '1Y' should be rejected"
);
assert!(
!is_iso8601_duration_valid("PT"),
"bare 'PT' should be rejected"
);
}
#[test]
fn test_iso8601_p1dt1h_valid() {
assert!(
is_iso8601_duration_valid("P1DT1H"),
"P1DT1H should be valid"
);
}
#[test]
fn test_iso8601_pt1s250ms_valid() {
assert!(
is_iso8601_duration_valid("PT1S250MS"),
"PT1S250MS should be valid"
);
}
#[test]
fn test_iso8601_rejected_year() {
assert!(
!is_iso8601_duration_valid("P1Y"),
"P1Y should be rejected (years)"
);
}
#[test]
fn test_iso8601_rejected_week() {
assert!(
!is_iso8601_duration_valid("P1W"),
"P1W should be rejected (weeks)"
);
}
#[test]
fn test_iso8601_rejected_fractional_day() {
assert!(
!is_iso8601_duration_valid("P1.5D"),
"P1.5D should be rejected (fractional days)"
);
}
#[test]
fn test_iso8601_rejected_month() {
assert!(
!is_iso8601_duration_valid("P1M"),
"P1M should be rejected (months)"
);
}
#[test]
fn test_iso8601_rejected_bare_pt() {
assert!(
!is_iso8601_duration_valid("PT"),
"bare PT should be rejected"
);
}
#[test]
fn test_iso8601_rejected_invalid_expression() {
assert!(
!is_iso8601_duration_valid("1Y"),
"1Y without P prefix should be rejected"
);
}
#[test]
fn test_oneof_duration_roundtrip_struct() {
let duration = OneOfDurationOrIso8601Expression::Duration(Duration {
days: Some(1),
hours: Some(2),
minutes: Some(30),
..Default::default()
});
let serialized = serde_json::to_string(&duration).unwrap();
let deserialized: OneOfDurationOrIso8601Expression =
serde_json::from_str(&serialized).unwrap();
assert_eq!(duration, deserialized);
}
#[test]
fn test_oneof_duration_roundtrip_iso8601() {
let duration =
OneOfDurationOrIso8601Expression::Iso8601Expression("P3DT4H5M6S250MS".to_string());
let serialized = serde_json::to_string(&duration).unwrap();
let deserialized: OneOfDurationOrIso8601Expression =
serde_json::from_str(&serialized).unwrap();
assert_eq!(duration, deserialized);
}
}