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