ferrobridge_fhirpath/temporal/
arithmetic.rs1use rust_decimal::Decimal;
13use rust_decimal::prelude::ToPrimitive;
14
15use super::{Clock, Date, DateTime, Precision, Seconds, Time};
16use crate::quantity::Calendar;
17
18#[derive(Debug, Clone, thiserror::Error)]
20pub enum ArithmeticError {
21 #[error("a {value} cannot be moved by a quantity in {unit}")]
24 Unit {
25 value: &'static str,
27 unit: &'static str,
29 },
30 #[error("the result lies outside the years 0001 to 9999")]
32 Range,
33 #[error("the calendar arithmetic failed")]
35 Calendar {
36 #[source]
38 source: jiff::Error,
39 },
40}
41
42const fn step(precision: Precision, fraction: bool) -> Calendar {
45 match precision {
46 Precision::Year => Calendar::Year,
47 Precision::Month => Calendar::Month,
48 Precision::Day => Calendar::Day,
49 Precision::Hour => Calendar::Hour,
50 Precision::Minute => Calendar::Minute,
51 Precision::Second if fraction => Calendar::Millisecond,
52 Precision::Second => Calendar::Second,
53 }
54}
55
56fn steps(
60 amount: Decimal,
61 unit: Calendar,
62 finest: Calendar,
63) -> Result<(i64, Calendar), ArithmeticError> {
64 let (amount, unit) = if unit == Calendar::Week {
65 (
66 amount
67 .checked_mul(Decimal::from(7))
68 .ok_or(ArithmeticError::Range)?,
69 Calendar::Day,
70 )
71 } else {
72 (amount, unit)
73 };
74 let (amount, unit) = if unit > finest {
75 (
76 unit.convert(amount, finest).ok_or(ArithmeticError::Range)?,
77 finest,
78 )
79 } else if unit == Calendar::Second && finest == Calendar::Millisecond {
80 (
81 amount
82 .checked_mul(Decimal::from(1000))
83 .ok_or(ArithmeticError::Range)?,
84 Calendar::Millisecond,
85 )
86 } else {
87 (amount, unit)
88 };
89 let whole = amount.trunc().to_i64().ok_or(ArithmeticError::Range)?;
90 Ok((whole, unit))
91}
92
93fn span(count: i64, unit: Calendar) -> Result<jiff::Span, ArithmeticError> {
95 let span = jiff::Span::new();
96 match unit {
97 Calendar::Year => span.try_years(count),
98 Calendar::Month => span.try_months(count),
99 Calendar::Week => span.try_weeks(count),
100 Calendar::Day => span.try_days(count),
101 Calendar::Hour => span.try_hours(count),
102 Calendar::Minute => span.try_minutes(count),
103 Calendar::Second => span.try_seconds(count),
104 Calendar::Millisecond => span.try_milliseconds(count),
105 }
106 .map_err(|source| ArithmeticError::Calendar { source })
107}
108
109fn move_date(date: Date, count: i64, unit: Calendar) -> Result<Date, ArithmeticError> {
111 let civil = jiff::civil::Date::new(date.year, date.month.unwrap_or(1), date.day.unwrap_or(1))
112 .map_err(|source| ArithmeticError::Calendar { source })?;
113 let moved = civil
114 .checked_add(span(count, unit)?)
115 .map_err(|source| ArithmeticError::Calendar { source })?;
116 in_range(Date {
117 year: moved.year(),
118 month: date.month.map(|_| moved.month()),
119 day: date.day.map(|_| moved.day()),
120 })
121}
122
123fn in_range(date: Date) -> Result<Date, ArithmeticError> {
125 if (1..=9999).contains(&date.year) {
126 Ok(date)
127 } else {
128 Err(ArithmeticError::Range)
129 }
130}
131
132fn nanos(clock: &Clock) -> i32 {
134 clock
135 .second
136 .and_then(|second| second.fraction)
137 .map_or(0, |(nanos, _)| nanos)
138}
139
140fn rewrite(clock: Clock, time: jiff::civil::Time, millis: bool) -> Clock {
143 Clock {
144 hour: time.hour(),
145 minute: clock.minute.map(|_| time.minute()),
146 second: clock.second.map(|second| Seconds {
147 whole: time.second(),
148 fraction: second.fraction.map(|(_, digits)| {
149 let digits = if millis { digits.max(3) } else { digits };
150 (time.subsec_nanosecond(), digits)
151 }),
152 }),
153 }
154}
155
156impl Date {
157 pub fn add(self, amount: Decimal, unit: Calendar) -> Result<Self, ArithmeticError> {
164 if unit > Calendar::Day {
165 return Err(ArithmeticError::Unit {
166 value: "date",
167 unit: unit.keyword(),
168 });
169 }
170 let (count, unit) = steps(amount, unit, step(self.precision(), false))?;
171 move_date(self, count, unit)
172 }
173}
174
175impl DateTime {
176 pub fn add(&self, amount: Decimal, unit: Calendar) -> Result<Self, ArithmeticError> {
183 let fraction = self
184 .clock
185 .and_then(|clock| clock.second)
186 .is_some_and(|second| second.fraction.is_some());
187 let (count, unit) = steps(amount, unit, step(self.precision(), fraction))?;
188 let Some(clock) = self.clock else {
189 return Ok(Self {
190 date: move_date(self.date, count, unit)?,
191 ..*self
192 });
193 };
194 let civil = jiff::civil::DateTime::new(
195 self.date.year,
196 self.date.month.unwrap_or(1),
197 self.date.day.unwrap_or(1),
198 clock.hour,
199 clock.minute.unwrap_or(0),
200 clock.second.map_or(0, |second| second.whole),
201 nanos(&clock),
202 )
203 .map_err(|source| ArithmeticError::Calendar { source })?;
204 let moved = civil
205 .checked_add(span(count, unit)?)
206 .map_err(|source| ArithmeticError::Calendar { source })?;
207 Ok(Self {
208 date: in_range(Date {
209 year: moved.year(),
210 month: Some(moved.month()),
211 day: Some(moved.day()),
212 })?,
213 clock: Some(rewrite(clock, moved.time(), unit == Calendar::Millisecond)),
214 offset: self.offset,
215 })
216 }
217}
218
219impl Time {
220 pub fn add(&self, amount: Decimal, unit: Calendar) -> Result<Self, ArithmeticError> {
229 if unit < Calendar::Hour {
230 return Err(ArithmeticError::Unit {
231 value: "time",
232 unit: unit.keyword(),
233 });
234 }
235 let fraction = self
236 .clock
237 .second
238 .is_some_and(|second| second.fraction.is_some());
239 let (count, unit) = steps(amount, unit, step(self.precision(), fraction))?;
240 let civil = jiff::civil::Time::new(
241 self.clock.hour,
242 self.clock.minute.unwrap_or(0),
243 self.clock.second.map_or(0, |second| second.whole),
244 nanos(&self.clock),
245 )
246 .map_err(|source| ArithmeticError::Calendar { source })?;
247 let moved = civil.wrapping_add(span(count, unit)?);
248 Ok(Self {
249 clock: rewrite(self.clock, moved, unit == Calendar::Millisecond),
250 })
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use rust_decimal::Decimal;
257
258 use super::super::{Date, DateTime};
259 use crate::quantity::Calendar;
260
261 fn date(text: &str) -> Date {
262 Date::parse(text).expect("the date should read")
263 }
264
265 #[test]
266 fn partial_dates_take_the_quantity_at_their_precision() {
267 let add = |text: &str, n: i64, unit| {
268 date(text)
269 .add(Decimal::from(n), unit)
270 .expect("the sum should exist")
271 .to_string()
272 };
273 assert_eq!(add("2014", 24, Calendar::Month), "2016");
274 assert_eq!(add("2014", 23, Calendar::Month), "2015");
275 assert_eq!(add("2016", 365, Calendar::Day), "2017");
276 assert_eq!(add("2019-03-01", 24, Calendar::Month), "2021-03-01");
277 assert_eq!(add("1973-12-25", 1, Calendar::Week), "1974-01-01");
278 }
279
280 #[test]
281 fn a_missing_day_becomes_the_last_of_the_month() {
282 let sum = date("2015-01-31").add(Decimal::ONE, Calendar::Month);
283 assert_eq!(
284 sum.ok().map(|d| d.to_string()),
285 Some(String::from("2015-02-28"))
286 );
287 }
288
289 #[test]
290 fn a_datetime_keeps_its_offset_and_fraction() {
291 let value = DateTime::parse("1973-12-25T00:00:00.000+10:00").expect("datetime");
292 let sum = value.add(Decimal::from(10), Calendar::Millisecond);
293 assert_eq!(
294 sum.ok().map(|d| d.to_string()),
295 Some(String::from("1973-12-25T00:00:00.010+10:00"))
296 );
297 }
298
299 #[test]
300 fn an_hour_does_not_move_a_date() {
301 assert!(
302 date("2015-01-01")
303 .add(Decimal::ONE, Calendar::Hour)
304 .is_err()
305 );
306 }
307}