1use crate::{DataError, Fingerprint, ValidationError, emit_error};
4use core::fmt;
5use serde::{Deserialize, Deserializer, Serialize, de};
6use serde_json::Value;
7use serde_with::{DisplayFromStr, serde_as};
8use speedate::Duration;
9use std::hash::Hasher;
10use std::str::FromStr;
11use tracing::error;
12
13#[serde_as]
19#[derive(Clone, Debug, PartialEq, Serialize)]
20pub struct MyDuration(#[serde_as(as = "DisplayFromStr")] Duration);
21
22impl MyDuration {
23 pub fn new(positive: bool, day: u32, second: u32, microsecond: u32) -> Result<Self, DataError> {
25 let x = Duration::new(positive, day, second, microsecond).map_err(|x| {
26 error!("{}", x);
27 DataError::Duration(x.to_string().into())
28 })?;
29 Ok(MyDuration(x))
30 }
31
32 fn from(duration: Duration) -> Self {
33 MyDuration(duration)
34 }
35
36 pub fn truncate(&self) -> Self {
47 let inner = &self.0;
48 MyDuration::from(
49 Duration::new(
50 inner.positive,
51 inner.day,
52 inner.second,
53 (inner.microsecond / 10_000) * 10_000,
54 )
55 .expect("Failed truncating duration"),
56 )
57 }
58
59 pub fn positive(&self) -> bool {
61 self.0.positive
62 }
63
64 pub fn day(&self) -> u32 {
66 self.0.day
67 }
68
69 pub fn second(&self) -> u32 {
71 self.0.second
72 }
73
74 pub fn microsecond(&self) -> u32 {
76 self.0.microsecond
77 }
78
79 pub fn to_iso8601(&self) -> String {
81 let inner = &self.0;
82 let mut res = String::from("P");
83 if inner.day != 0 {
84 res.push_str(&inner.day.to_string());
85 res.push('D');
86 };
87 res.push('T');
88 let sec = inner.second;
89 let mu = inner.microsecond / 10_000;
91 let (h, rest) = (sec / 3600, sec % 3600);
93 res.push_str(&h.to_string());
94 res.push('H');
95 let (m, s) = (rest / 60, rest % 60);
96 res.push_str(&m.to_string());
97 res.push('M');
98 if mu == 0 {
99 res.push_str(&s.to_string());
100 } else {
101 let sec = s as f32 + (mu as f32 / 100.0);
102 res.push_str(&format!("{sec:.2}"));
103 }
104 res.push('S');
105 res
106 }
107}
108
109impl fmt::Display for MyDuration {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 write!(f, "{}", self.to_iso8601())
112 }
113}
114
115impl Fingerprint for MyDuration {
116 fn fingerprint<H: Hasher>(&self, state: &mut H) {
117 let truncated = self.truncate().0;
118 state.write_i64(truncated.signed_total_seconds());
119 state.write_i32(truncated.signed_microseconds())
120 }
121}
122
123impl FromStr for MyDuration {
124 type Err = DataError;
125
126 fn from_str(s: &str) -> Result<Self, Self::Err> {
127 let s = s.trim();
133 if s.contains('W') && !s.ends_with('W') {
134 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
135 "Only [PnnW] or [PnnYnnMnnDTnnHnnMnnS] patterns are allowed".into()
136 )))
137 } else {
138 let x = Duration::parse_str(s).map_err(|x| {
139 error!("{}", x);
140 DataError::Duration(x.to_string().into())
141 })?;
142 Ok(MyDuration::from(x))
143 }
144 }
145}
146
147impl<'de> Deserialize<'de> for MyDuration {
148 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149 where
150 D: Deserializer<'de>,
151 {
152 let value: Value = Deserialize::deserialize(deserializer)?;
153 match value {
154 Value::String(s) => MyDuration::from_str(&s).map_err(de::Error::custom),
155 _ => Err(de::Error::custom("Expected string")),
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 #[should_panic]
166 fn test_iso8601_4433_p1() {
167 MyDuration::from_str("P4W1D").unwrap();
168 }
169
170 #[test]
171 fn test_iso8601_4433_p2() {
172 assert!(MyDuration::from_str("P4W").is_ok());
173 assert!(serde_json::from_str::<MyDuration>("\"P4W\"").is_ok());
174 }
175
176 #[test]
177 fn test_truncation() {
178 const D1: &str = "P1DT12H36M0.12567S";
179 const D2: &str = "P1DT12H36M0.12S";
180
181 let d1 = MyDuration::from_str(D1).unwrap();
182 let d2 = MyDuration::from_str(D2).unwrap();
183 assert_eq!(d1.day(), d2.day());
184 assert_eq!(d1.second(), d2.second());
185 assert_eq!(d1.microsecond() / 10_000, d2.microsecond() / 10_000);
186 }
187
188 #[test]
189 #[should_panic]
190 fn test_deserialization() {
191 serde_json::from_str::<MyDuration>("\"P4W1D\"").unwrap();
192 }
193}