jiff/util/round/
mode.rs

1use crate::{
2    util::{
3        rangeint::{RFrom, RInto},
4        t::{NoUnits, NoUnits128, C, C128},
5    },
6    Unit,
7};
8
9/// The mode for dealing with the remainder when rounding datetimes or spans.
10///
11/// This is used in APIs like [`Span::round`](crate::Span::round) for rounding
12/// spans, and APIs like [`Zoned::round`](crate::Zoned::round) for rounding
13/// datetimes.
14///
15/// In the documentation for each variant, we refer to concepts like the
16/// "smallest" unit and the "rounding increment." These are best described
17/// in the documentation for what you're rounding. For example,
18/// [`SpanRound::smallest`](crate::SpanRound::smallest)
19/// and [`SpanRound::increment`](crate::SpanRound::increment).
20///
21/// # Example
22///
23/// This shows how to round a span with a different rounding mode than the
24/// default:
25///
26/// ```
27/// use jiff::{RoundMode, SpanRound, ToSpan, Unit};
28///
29/// // The default rounds like how you were taught in school:
30/// assert_eq!(
31///     1.hour().minutes(59).round(Unit::Hour)?,
32///     2.hours().fieldwise(),
33/// );
34/// // But we can change the mode, e.g., truncation:
35/// let options = SpanRound::new().smallest(Unit::Hour).mode(RoundMode::Trunc);
36/// assert_eq!(
37///     1.hour().minutes(59).round(options)?,
38///     1.hour().fieldwise(),
39/// );
40///
41/// # Ok::<(), Box<dyn std::error::Error>>(())
42/// ```
43#[non_exhaustive]
44#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
45pub enum RoundMode {
46    /// Rounds toward positive infinity.
47    ///
48    /// For negative spans and datetimes, this option will make the value
49    /// smaller, which could be unexpected. To round away from zero, use
50    /// `Expand`.
51    Ceil,
52    /// Rounds toward negative infinity.
53    ///
54    /// This mode acts like `Trunc` for positive spans and datetimes, but
55    /// for negative values it will make the value larger, which could be
56    /// unexpected. To round towards zero, use `Trunc`.
57    Floor,
58    /// Rounds away from zero like `Ceil` for positive spans and datetimes,
59    /// and like `Floor` for negative spans and datetimes.
60    Expand,
61    /// Rounds toward zero, chopping off any fractional part of a unit.
62    ///
63    /// This is the default when rounding spans returned from
64    /// datetime arithmetic. (But it is not the default for
65    /// [`Span::round`](crate::Span::round).)
66    Trunc,
67    /// Rounds to the nearest allowed value like `HalfExpand`, but when there
68    /// is a tie, round towards positive infinity like `Ceil`.
69    HalfCeil,
70    /// Rounds to the nearest allowed value like `HalfExpand`, but when there
71    /// is a tie, round towards negative infinity like `Floor`.
72    HalfFloor,
73    /// Rounds to the nearest value allowed by the rounding increment and the
74    /// smallest unit. When there is a tie, round away from zero like `Ceil`
75    /// for positive spans and datetimes and like `Floor` for negative spans
76    /// and datetimes.
77    ///
78    /// This corresponds to how rounding is often taught in school.
79    ///
80    /// This is the default for rounding spans and datetimes.
81    HalfExpand,
82    /// Rounds to the nearest allowed value like `HalfExpand`, but when there
83    /// is a tie, round towards zero like `Trunc`.
84    HalfTrunc,
85    /// Rounds to the nearest allowed value like `HalfExpand`, but when there
86    /// is a tie, round towards the value that is an even multiple of the
87    /// rounding increment. For example, with a rounding increment of `3`,
88    /// the number `10` would round up to `12` instead of down to `9`, because
89    /// `12` is an even multiple of `3`, where as `9` is is an odd multiple.
90    HalfEven,
91}
92
93impl RoundMode {
94    /// Given a `quantity` in nanoseconds and an `increment` in units of
95    /// `unit`, this rounds it according to this mode and returns the result
96    /// in nanoseconds.
97    pub(crate) fn round_by_unit_in_nanoseconds(
98        self,
99        quantity: impl RInto<NoUnits128>,
100        unit: Unit,
101        increment: impl RInto<NoUnits128>,
102    ) -> NoUnits128 {
103        let quantity = quantity.rinto();
104        let increment = unit.nanoseconds() * increment.rinto();
105        let rounded = self.round(quantity, increment);
106        rounded
107    }
108
109    fn round(
110        self,
111        quantity: impl RInto<NoUnits128>,
112        increment: impl RInto<NoUnits128>,
113    ) -> NoUnits128 {
114        // ref: https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement
115        fn inner(
116            mode: RoundMode,
117            quantity: NoUnits128,
118            increment: NoUnits128,
119        ) -> NoUnits128 {
120            let mut quotient = quantity.div_ceil(increment);
121            let remainder = quantity.rem_ceil(increment);
122            if remainder == 0 {
123                return quantity;
124            }
125            let sign = if remainder < 0 { C128(-1) } else { C128(1) };
126            let tiebreaker = (remainder * C128(2)).abs();
127            let tie = tiebreaker == increment;
128            let expand_is_nearer = tiebreaker > increment;
129            // ref: https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement
130            match mode {
131                RoundMode::Ceil => {
132                    if sign > 0 {
133                        quotient += sign;
134                    }
135                }
136                RoundMode::Floor => {
137                    if sign < 0 {
138                        quotient += sign;
139                    }
140                }
141                RoundMode::Expand => {
142                    quotient += sign;
143                }
144                RoundMode::Trunc => {}
145                RoundMode::HalfCeil => {
146                    if expand_is_nearer || (tie && sign > 0) {
147                        quotient += sign;
148                    }
149                }
150                RoundMode::HalfFloor => {
151                    if expand_is_nearer || (tie && sign < 0) {
152                        quotient += sign;
153                    }
154                }
155                RoundMode::HalfExpand => {
156                    if expand_is_nearer || tie {
157                        quotient += sign;
158                    }
159                }
160                RoundMode::HalfTrunc => {
161                    if expand_is_nearer {
162                        quotient += sign;
163                    }
164                }
165                RoundMode::HalfEven => {
166                    if expand_is_nearer || (tie && quotient % C(2) == 1) {
167                        quotient += sign;
168                    }
169                }
170            }
171            // We use saturating arithmetic here because this can overflow
172            // when `quantity` is the max value. Since we're rounding, we just
173            // refuse to go over the maximum. I'm not 100% convinced this is
174            // correct, but I think the only alternative is to return an error,
175            // and I'm not sure that's ideal either.
176            quotient.saturating_mul(increment)
177        }
178        inner(self, quantity.rinto(), increment.rinto())
179    }
180
181    pub(crate) fn round_float(
182        self,
183        quantity: f64,
184        increment: NoUnits128,
185    ) -> NoUnits128 {
186        #[cfg(not(feature = "std"))]
187        use crate::util::libm::Float;
188
189        let quotient = quantity / (increment.get() as f64);
190        let rounded = match self {
191            RoundMode::Ceil => quotient.ceil(),
192            RoundMode::Floor => quotient.floor(),
193            RoundMode::Expand => {
194                if quotient < 0.0 {
195                    quotient.floor()
196                } else {
197                    quotient.ceil()
198                }
199            }
200            RoundMode::Trunc => quotient.trunc(),
201            RoundMode::HalfCeil => {
202                if quotient % 1.0 == 0.5 {
203                    quotient.ceil()
204                } else {
205                    quotient.round()
206                }
207            }
208            RoundMode::HalfFloor => {
209                if quotient % 1.0 == 0.5 {
210                    quotient.floor()
211                } else {
212                    quotient.round()
213                }
214            }
215            RoundMode::HalfExpand => {
216                quotient.signum() * quotient.abs().round()
217            }
218            RoundMode::HalfTrunc => {
219                if quotient % 1.0 == 0.5 {
220                    quotient.trunc()
221                } else {
222                    quotient.round()
223                }
224            }
225            RoundMode::HalfEven => {
226                if quotient % 1.0 == 0.5 {
227                    quotient.trunc() + (quotient % 2.0)
228                } else {
229                    quotient.round()
230                }
231            }
232        };
233        let rounded = NoUnits::new(rounded as i64).unwrap();
234        NoUnits128::rfrom(rounded.saturating_mul(increment))
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    // Some ad hoc tests I wrote while writing the rounding increment code.
243    #[test]
244    fn round_to_increment_half_expand_ad_hoc() {
245        let round = |quantity: i64, increment: i64| -> i64 {
246            let quantity = NoUnits::new(quantity).unwrap();
247            let increment = NoUnits::new(increment).unwrap();
248            i64::from(RoundMode::HalfExpand.round(quantity, increment))
249        };
250        assert_eq!(26, round(20, 13));
251
252        assert_eq!(0, round(29, 60));
253        assert_eq!(60, round(30, 60));
254        assert_eq!(60, round(31, 60));
255
256        assert_eq!(0, round(3, 7));
257        assert_eq!(7, round(4, 7));
258    }
259
260    // The Temporal tests are inspired by the table from here:
261    // https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement
262    //
263    // The main difference is that our rounding function specifically does not
264    // use floating point, so we tweak the values a bit.
265
266    #[test]
267    fn round_to_increment_temporal_table_ceil() {
268        let round = |quantity: i64, increment: i64| -> i64 {
269            let quantity = NoUnits::new(quantity).unwrap();
270            let increment = NoUnits::new(increment).unwrap();
271            RoundMode::Ceil.round(quantity, increment).into()
272        };
273        assert_eq!(-10, round(-15, 10));
274        assert_eq!(0, round(-5, 10));
275        assert_eq!(10, round(4, 10));
276        assert_eq!(10, round(5, 10));
277        assert_eq!(10, round(6, 10));
278        assert_eq!(20, round(15, 10));
279    }
280
281    #[test]
282    fn round_to_increment_temporal_table_floor() {
283        let round = |quantity: i64, increment: i64| -> i64 {
284            let quantity = NoUnits::new(quantity).unwrap();
285            let increment = NoUnits::new(increment).unwrap();
286            RoundMode::Floor.round(quantity, increment).into()
287        };
288        assert_eq!(-20, round(-15, 10));
289        assert_eq!(-10, round(-5, 10));
290        assert_eq!(0, round(4, 10));
291        assert_eq!(0, round(5, 10));
292        assert_eq!(0, round(6, 10));
293        assert_eq!(10, round(15, 10));
294    }
295
296    #[test]
297    fn round_to_increment_temporal_table_expand() {
298        let round = |quantity: i64, increment: i64| -> i64 {
299            let quantity = NoUnits::new(quantity).unwrap();
300            let increment = NoUnits::new(increment).unwrap();
301            RoundMode::Expand.round(quantity, increment).into()
302        };
303        assert_eq!(-20, round(-15, 10));
304        assert_eq!(-10, round(-5, 10));
305        assert_eq!(10, round(4, 10));
306        assert_eq!(10, round(5, 10));
307        assert_eq!(10, round(6, 10));
308        assert_eq!(20, round(15, 10));
309    }
310
311    #[test]
312    fn round_to_increment_temporal_table_trunc() {
313        let round = |quantity: i64, increment: i64| -> i64 {
314            let quantity = NoUnits::new(quantity).unwrap();
315            let increment = NoUnits::new(increment).unwrap();
316            RoundMode::Trunc.round(quantity, increment).into()
317        };
318        assert_eq!(-10, round(-15, 10));
319        assert_eq!(0, round(-5, 10));
320        assert_eq!(0, round(4, 10));
321        assert_eq!(0, round(5, 10));
322        assert_eq!(0, round(6, 10));
323        assert_eq!(10, round(15, 10));
324    }
325
326    #[test]
327    fn round_to_increment_temporal_table_half_ceil() {
328        let round = |quantity: i64, increment: i64| -> i64 {
329            let quantity = NoUnits::new(quantity).unwrap();
330            let increment = NoUnits::new(increment).unwrap();
331            RoundMode::HalfCeil.round(quantity, increment).into()
332        };
333        assert_eq!(-10, round(-15, 10));
334        assert_eq!(0, round(-5, 10));
335        assert_eq!(0, round(4, 10));
336        assert_eq!(10, round(5, 10));
337        assert_eq!(10, round(6, 10));
338        assert_eq!(20, round(15, 10));
339    }
340
341    #[test]
342    fn round_to_increment_temporal_table_half_floor() {
343        let round = |quantity: i64, increment: i64| -> i64 {
344            let quantity = NoUnits::new(quantity).unwrap();
345            let increment = NoUnits::new(increment).unwrap();
346            RoundMode::HalfFloor.round(quantity, increment).into()
347        };
348        assert_eq!(-20, round(-15, 10));
349        assert_eq!(-10, round(-5, 10));
350        assert_eq!(0, round(4, 10));
351        assert_eq!(0, round(5, 10));
352        assert_eq!(10, round(6, 10));
353        assert_eq!(10, round(15, 10));
354    }
355
356    #[test]
357    fn round_to_increment_temporal_table_half_expand() {
358        let round = |quantity: i64, increment: i64| -> i64 {
359            let quantity = NoUnits::new(quantity).unwrap();
360            let increment = NoUnits::new(increment).unwrap();
361            RoundMode::HalfExpand.round(quantity, increment).into()
362        };
363        assert_eq!(-20, round(-15, 10));
364        assert_eq!(-10, round(-5, 10));
365        assert_eq!(0, round(4, 10));
366        assert_eq!(10, round(5, 10));
367        assert_eq!(10, round(6, 10));
368        assert_eq!(20, round(15, 10));
369    }
370
371    #[test]
372    fn round_to_increment_temporal_table_half_trunc() {
373        let round = |quantity: i64, increment: i64| -> i64 {
374            let quantity = NoUnits::new(quantity).unwrap();
375            let increment = NoUnits::new(increment).unwrap();
376            RoundMode::HalfTrunc.round(quantity, increment).into()
377        };
378        assert_eq!(-10, round(-15, 10));
379        assert_eq!(0, round(-5, 10));
380        assert_eq!(0, round(4, 10));
381        assert_eq!(0, round(5, 10));
382        assert_eq!(10, round(6, 10));
383        assert_eq!(10, round(15, 10));
384    }
385
386    #[test]
387    fn round_to_increment_temporal_table_half_even() {
388        let round = |quantity: i64, increment: i64| -> i64 {
389            let quantity = NoUnits::new(quantity).unwrap();
390            let increment = NoUnits::new(increment).unwrap();
391            RoundMode::HalfEven.round(quantity, increment).into()
392        };
393        assert_eq!(-20, round(-15, 10));
394        assert_eq!(0, round(-5, 10));
395        assert_eq!(0, round(4, 10));
396        assert_eq!(0, round(5, 10));
397        assert_eq!(10, round(6, 10));
398        assert_eq!(20, round(15, 10));
399    }
400}