Skip to content

Commit c40479f

Browse files
committed
add fee-1.0 extension
1 parent 173e457 commit c40479f

File tree

17 files changed

+1858
-0
lines changed

17 files changed

+1858
-0
lines changed

src/extensions/fee/duration.rs

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
//! XSD Duration format
2+
3+
use std::ops::Div;
4+
use std::str::FromStr;
5+
6+
use instant_xml::{FromXml, ToXml};
7+
8+
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
9+
pub struct XsdDuration {
10+
months: i64,
11+
seconds: f64,
12+
}
13+
14+
impl XsdDuration {
15+
pub fn new(months: i64, seconds: f64) -> Result<Self, InvalidMinutesOrSeconds> {
16+
if months < 0 && seconds > 0.0 || months > 0 && seconds < 0.0 {
17+
return Err(InvalidMinutesOrSeconds);
18+
}
19+
Ok(Self { months, seconds })
20+
}
21+
22+
fn is_zero(&self) -> bool {
23+
self.months == 0 && self.seconds == 0.0
24+
}
25+
}
26+
27+
impl FromStr for XsdDuration {
28+
type Err = ParseError;
29+
30+
/// Parses an XSD duration string into [`XsdDuration`].
31+
///
32+
/// Algorithm based on https://www.w3.org/TR/xmlschema11-2/#f-durationMap
33+
///
34+
/// DUR consists of possibly a leading '-', followed by 'P' and then an instance Y of duYearMonthFrag and/or an instance D of duDayTimeFrag:
35+
/// Return a duration whose:
36+
/// * months value is 12 * Y + M
37+
/// * seconds value is 86400 * D + (3600 * H + 60 * M + S)
38+
///
39+
/// where Y, M, D, H, M, S are the values parsed from the string.
40+
///
41+
/// All values default to 0 if not present.
42+
fn from_str(s: &str) -> Result<Self, Self::Err> {
43+
// DUR consists of possibly a leading '-', followed by 'P' and then an instance Y of duYearMonthFrag and/or an instance D of duDayTimeFrag:
44+
let sgn = if s.starts_with('-') { -1 } else { 1 };
45+
let s = s.trim_start_matches('-');
46+
if !s.starts_with('P') {
47+
return Err(ParseError);
48+
}
49+
let s = &s[1..];
50+
51+
// duYearMonthFrag
52+
let (s, months) = {
53+
let mut y = 0;
54+
let s = match s.split_once("Y") {
55+
Some((l, r)) => {
56+
y = l.parse::<u32>().map_err(|_| ParseError)?;
57+
r
58+
}
59+
None => s,
60+
};
61+
let mut m = 0;
62+
let s = match s.split_once("M") {
63+
// skip months if the M was part of duDayTimeFrag
64+
Some((l, r)) if !l.contains('T') => {
65+
m = l.parse::<u32>().map_err(|_| ParseError)?;
66+
r
67+
}
68+
_ => s,
69+
};
70+
71+
(s, 12 * (y as i64) + (m as i64) * sgn)
72+
};
73+
74+
// duDayTimeFrag
75+
let seconds = {
76+
let mut d = 0;
77+
78+
let s = match s.split_once('D') {
79+
Some((l, r)) => {
80+
d = l.parse::<u32>().map_err(|_| ParseError)?;
81+
r
82+
}
83+
None => s,
84+
};
85+
if !s.starts_with("T") {
86+
return Ok(XsdDuration {
87+
months,
88+
seconds: (86400 * d) as f64,
89+
});
90+
}
91+
let s = &s[1..];
92+
let t = {
93+
let mut h = 0;
94+
let mut m = 0;
95+
let mut ss = 0.0;
96+
let s = match s.split_once('H') {
97+
Some((l, r)) => {
98+
h = l.parse::<u32>().map_err(|_| ParseError)?;
99+
r
100+
}
101+
None => s,
102+
};
103+
let s = match s.split_once('M') {
104+
Some((l, r)) => {
105+
m = l.parse::<u32>().map_err(|_| ParseError)?;
106+
r
107+
}
108+
None => s,
109+
};
110+
if let Some((l, _r)) = s.split_once('S') {
111+
ss = l.parse::<f64>().map_err(|_| ParseError)?;
112+
}
113+
114+
(3600 * h) as f64 + (60 * m) as f64 + ss
115+
};
116+
117+
(86400 * d) as f64 + t
118+
};
119+
120+
Ok(XsdDuration { months, seconds })
121+
}
122+
}
123+
124+
impl<'xml> FromXml<'xml> for XsdDuration {
125+
fn matches(id: instant_xml::Id<'_>, field: Option<instant_xml::Id<'_>>) -> bool {
126+
match field {
127+
Some(field) => id == field,
128+
None => false,
129+
}
130+
}
131+
132+
fn deserialize<'cx>(
133+
into: &mut Self::Accumulator,
134+
field: &'static str,
135+
deserializer: &mut instant_xml::Deserializer<'cx, 'xml>,
136+
) -> Result<(), instant_xml::Error> {
137+
if into.is_some() {
138+
return Err(instant_xml::Error::DuplicateValue(field));
139+
}
140+
141+
match deserializer.take_str()? {
142+
Some(value) => {
143+
let duration = value.parse().map_err(|_| {
144+
instant_xml::Error::Other(format!("failed to parse xsd duration: {}", value))
145+
})?;
146+
*into = Some(duration);
147+
}
148+
None => {}
149+
};
150+
151+
Ok(())
152+
}
153+
154+
type Accumulator = Option<XsdDuration>;
155+
156+
const KIND: instant_xml::Kind = instant_xml::Kind::Scalar;
157+
}
158+
159+
impl ToXml for XsdDuration {
160+
fn serialize<W: std::fmt::Write + ?Sized>(
161+
&self,
162+
_field: Option<instant_xml::Id<'_>>,
163+
serializer: &mut instant_xml::Serializer<W>,
164+
) -> Result<(), instant_xml::Error> {
165+
serializer.write_str(&format_duration_inner(self))?;
166+
Ok(())
167+
}
168+
}
169+
170+
#[derive(Debug)]
171+
pub struct InvalidMinutesOrSeconds;
172+
173+
impl std::error::Error for InvalidMinutesOrSeconds {}
174+
175+
impl std::fmt::Display for InvalidMinutesOrSeconds {
176+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177+
write!(f, "invalid minutes or seconds value")
178+
}
179+
}
180+
181+
#[derive(Debug)]
182+
pub struct ParseError;
183+
184+
impl std::error::Error for ParseError {}
185+
186+
impl std::fmt::Display for ParseError {
187+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188+
write!(f, "failed to parse xsd duration")
189+
}
190+
}
191+
192+
/// Serialize duration to XML duration string
193+
///
194+
/// See https://www.w3.org/TR/xmlschema11-2/#duration
195+
pub fn format_duration<D>(duration: D) -> Result<String, D::Error>
196+
where
197+
D: TryInto<XsdDuration>,
198+
{
199+
let duration: XsdDuration = duration.try_into()?;
200+
Ok(format_duration_inner(&duration))
201+
}
202+
/// Serialize duration to XML duration string
203+
///
204+
/// https://www.w3.org/TR/xmlschema11-2/#f-durationCanMap
205+
fn format_duration_inner(duration: &XsdDuration) -> String {
206+
if duration.is_zero() {
207+
return "P0D".to_owned();
208+
}
209+
210+
let mut buf = if duration.months < 0 || duration.seconds < 0.0 {
211+
String::from("-P")
212+
} else {
213+
String::from("P")
214+
};
215+
// https://www.w3.org/TR/xmlschema11-2/#f-duYMCan
216+
let years = (duration.months / 12) as u64;
217+
let months = (duration.months % 12) as u64;
218+
219+
if years > 0 {
220+
buf.push_str(&format!("{}Y", years));
221+
}
222+
if months > 0 {
223+
buf.push_str(&format!("{}M", months));
224+
}
225+
// https://www.w3.org/TR/xmlschema11-2/#f-duDTCan
226+
if duration.seconds == 0.0 {
227+
return buf;
228+
}
229+
230+
let days = (duration.seconds.div_euclid(86400.0)) as u64;
231+
let hours = ((duration.seconds % 86400.0).div_euclid(3600.0)) as u64;
232+
let minutes = ((duration.seconds % 3600.0).div(60.0)) as u64;
233+
let seconds = duration.seconds % 60.0;
234+
235+
if duration.seconds != 0.0 {
236+
if days > 0 {
237+
buf.push_str(&format!("{}D", days));
238+
}
239+
240+
if hours == 0 && minutes == 0 && seconds == 0.0 {
241+
return buf;
242+
}
243+
244+
buf.push('T');
245+
246+
if hours > 0 {
247+
buf.push_str(&format!("{}H", hours));
248+
}
249+
if minutes > 0 {
250+
buf.push_str(&format!("{}M", minutes));
251+
}
252+
if seconds > 0.0 {
253+
if seconds.fract() > 0.0 {
254+
buf.push_str(&format!("{:.4}S", seconds));
255+
} else {
256+
buf.push_str(&format!("{}S", seconds.trunc() as u64));
257+
}
258+
}
259+
}
260+
261+
buf
262+
}
263+
264+
#[cfg(test)]
265+
mod tests {
266+
use super::*;
267+
268+
const DAY: u64 = 86400;
269+
270+
#[test]
271+
fn construct() {
272+
let _ = XsdDuration::new(12, 3600.0).unwrap();
273+
let _ = XsdDuration::new(-12, -3600.0).unwrap();
274+
assert!(XsdDuration::new(12, -3600.0).is_err());
275+
assert!(XsdDuration::new(-12, 3600.0).is_err());
276+
}
277+
278+
#[test]
279+
fn ser() {
280+
let dur = XsdDuration::new(0, (3600 + 60 + 1) as f64).unwrap(); // 1 hour, 1 minute, 1 second
281+
let s = format_duration(dur).unwrap();
282+
assert_eq!(s, "PT1H1M1S");
283+
284+
let dur = XsdDuration::new(0, 0.0).unwrap();
285+
let s = format_duration(dur).unwrap();
286+
assert_eq!(s, "P0D");
287+
288+
// This is totally flawed but the spec demands it
289+
let dur = XsdDuration::new(13, (DAY + 3600 + 60 + 1) as f64).unwrap(); // 1 year, 1 month, 1 day, 1 hour, 1 minute, 1 second
290+
let s = format_duration(dur).unwrap();
291+
assert_eq!(s, "P1Y1M1DT1H1M1S");
292+
293+
let dur = XsdDuration::new(13, (5 * DAY) as f64).unwrap(); // 1 year, 1 month, 5 days
294+
let s = format_duration(dur).unwrap();
295+
assert_eq!(s, "P1Y1M5D");
296+
}
297+
298+
#[test]
299+
fn deser() {
300+
let s = "PT1H1M1S";
301+
let dur: XsdDuration = s.parse().unwrap();
302+
assert_eq!(dur, XsdDuration::new(0, (3600 + 60 + 1) as f64).unwrap());
303+
304+
let s = "P0D";
305+
let dur: XsdDuration = s.parse().unwrap();
306+
assert_eq!(dur, XsdDuration::new(0, 0.0).unwrap());
307+
let s = "P1Y1M1DT1H1M1S";
308+
let dur: XsdDuration = s.parse().unwrap();
309+
assert_eq!(
310+
dur,
311+
XsdDuration::new(12 * 30, (30 * DAY + DAY + 3600 + 60 + 1) as f64).unwrap()
312+
);
313+
314+
let s = "P1Y1M5DT0H0M0S";
315+
let dur: XsdDuration = s.parse().unwrap();
316+
assert_eq!(
317+
dur,
318+
XsdDuration::new(12 * 30, (30 * DAY + 5 * DAY) as f64).unwrap()
319+
);
320+
}
321+
}

0 commit comments

Comments
 (0)