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    /// Rounds `quantity` to the nearest `increment` in units of nanoseconds.
110    pub(crate) fn round(
111        self,
112        quantity: impl RInto<NoUnits128>,
113        increment: impl RInto<NoUnits128>,
114    ) -> NoUnits128 {
115        // ref: https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement
116        fn inner(
117            mode: RoundMode,
118            quantity: NoUnits128,
119            increment: NoUnits128,
120        ) -> NoUnits128 {
121            let mut quotient = quantity.div_ceil(increment);
122            let remainder = quantity.rem_ceil(increment);
123            if remainder == C(0) {
124                return quantity;
125            }
126            let sign = if remainder < C(0) { C128(-1) } else { C128(1) };
127            let tiebreaker = (remainder * C128(2)).abs();
128            let tie = tiebreaker == increment;
129            let expand_is_nearer = tiebreaker > increment;
130            // ref: https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement
131            match mode {
132                RoundMode::Ceil => {
133                    if sign > C(0) {
134                        quotient += sign;
135                    }
136                }
137                RoundMode::Floor => {
138                    if sign < C(0) {
139                        quotient += sign;
140                    }
141                }
142                RoundMode::Expand => {
143                    quotient += sign;
144                }
145                RoundMode::Trunc => {}
146                RoundMode::HalfCeil => {
147                    if expand_is_nearer || (tie && sign > C(0)) {
148                        quotient += sign;
149                    }
150                }
151                RoundMode::HalfFloor => {
152                    if expand_is_nearer || (tie && sign < C(0)) {
153                        quotient += sign;
154                    }
155                }
156                RoundMode::HalfExpand => {
157                    if expand_is_nearer || tie {
158                        quotient += sign;
159                    }
160                }
161                RoundMode::HalfTrunc => {
162                    if expand_is_nearer {
163                        quotient += sign;
164                    }
165                }
166                RoundMode::HalfEven => {
167                    if expand_is_nearer || (tie && quotient % C(2) == C(1)) {
168                        quotient += sign;
169                    }
170                }
171            }
172            // We use saturating arithmetic here because this can overflow
173            // when `quantity` is the max value. Since we're rounding, we just
174            // refuse to go over the maximum. I'm not 100% convinced this is
175            // correct, but I think the only alternative is to return an error,
176            // and I'm not sure that's ideal either.
177            quotient.saturating_mul(increment)
178        }
179        inner(self, quantity.rinto(), increment.rinto())
180    }
181
182    pub(crate) fn round_float(
183        self,
184        quantity: f64,
185        increment: NoUnits128,
186    ) -> NoUnits128 {
187        #[cfg(not(feature = "std"))]
188        use crate::util::libm::Float;
189
190        let quotient = quantity / (increment.get() as f64);
191        let rounded = match self {
192            RoundMode::Ceil => quotient.ceil(),
193            RoundMode::Floor => quotient.floor(),
194            RoundMode::Expand => {
195                if quotient < 0.0 {
196                    quotient.floor()
197                } else {
198                    quotient.ceil()
199                }
200            }
201            RoundMode::Trunc => quotient.trunc(),
202            RoundMode::HalfCeil => {
203                if quotient % 1.0 == 0.5 {
204                    quotient.ceil()
205                } else {
206                    quotient.round()
207                }
208            }
209            RoundMode::HalfFloor => {
210                if quotient % 1.0 == 0.5 {
211                    quotient.floor()
212                } else {
213                    quotient.round()
214                }
215            }
216            RoundMode::HalfExpand => {
217                quotient.signum() * quotient.abs().round()
218            }
219            RoundMode::HalfTrunc => {
220                if quotient % 1.0 == 0.5 {
221                    quotient.trunc()
222                } else {
223                    quotient.round()
224                }
225            }
226            RoundMode::HalfEven => {
227                if quotient % 1.0 == 0.5 {
228                    quotient.trunc() + (quotient % 2.0)
229                } else {
230                    quotient.round()
231                }
232            }
233        };
234        let rounded = NoUnits::new(rounded as i64).unwrap();
235        NoUnits128::rfrom(rounded.saturating_mul(increment))
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    // Some ad hoc tests I wrote while writing the rounding increment code.
244    #[test]
245    fn round_to_increment_half_expand_ad_hoc() {
246        let round = |quantity: i64, increment: i64| -> i64 {
247            let quantity = NoUnits::new(quantity).unwrap();
248            let increment = NoUnits::new(increment).unwrap();
249            i64::from(RoundMode::HalfExpand.round(quantity, increment))
250        };
251        assert_eq!(26, round(20, 13));
252
253        assert_eq!(0, round(29, 60));
254        assert_eq!(60, round(30, 60));
255        assert_eq!(60, round(31, 60));
256
257        assert_eq!(0, round(3, 7));
258        assert_eq!(7, round(4, 7));
259    }
260
261    // The Temporal tests are inspired by the table from here:
262    // https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement
263    //
264    // The main difference is that our rounding function specifically does not
265    // use floating point, so we tweak the values a bit.
266
267    #[test]
268    fn round_to_increment_temporal_table_ceil() {
269        let round = |quantity: i64, increment: i64| -> i64 {
270            let quantity = NoUnits::new(quantity).unwrap();
271            let increment = NoUnits::new(increment).unwrap();
272            RoundMode::Ceil.round(quantity, increment).into()
273        };
274        assert_eq!(-10, round(-15, 10));
275        assert_eq!(0, round(-5, 10));
276        assert_eq!(10, round(4, 10));
277        assert_eq!(10, round(5, 10));
278        assert_eq!(10, round(6, 10));
279        assert_eq!(20, round(15, 10));
280    }
281
282    #[test]
283    fn round_to_increment_temporal_table_floor() {
284        let round = |quantity: i64, increment: i64| -> i64 {
285            let quantity = NoUnits::new(quantity).unwrap();
286            let increment = NoUnits::new(increment).unwrap();
287            RoundMode::Floor.round(quantity, increment).into()
288        };
289        assert_eq!(-20, round(-15, 10));
290        assert_eq!(-10, round(-5, 10));
291        assert_eq!(0, round(4, 10));
292        assert_eq!(0, round(5, 10));
293        assert_eq!(0, round(6, 10));
294        assert_eq!(10, round(15, 10));
295    }
296
297    #[test]
298    fn round_to_increment_temporal_table_expand() {
299        let round = |quantity: i64, increment: i64| -> i64 {
300            let quantity = NoUnits::new(quantity).unwrap();
301            let increment = NoUnits::new(increment).unwrap();
302            RoundMode::Expand.round(quantity, increment).into()
303        };
304        assert_eq!(-20, round(-15, 10));
305        assert_eq!(-10, round(-5, 10));
306        assert_eq!(10, round(4, 10));
307        assert_eq!(10, round(5, 10));
308        assert_eq!(10, round(6, 10));
309        assert_eq!(20, round(15, 10));
310    }
311
312    #[test]
313    fn round_to_increment_temporal_table_trunc() {
314        let round = |quantity: i64, increment: i64| -> i64 {
315            let quantity = NoUnits::new(quantity).unwrap();
316            let increment = NoUnits::new(increment).unwrap();
317            RoundMode::Trunc.round(quantity, increment).into()
318        };
319        assert_eq!(-10, round(-15, 10));
320        assert_eq!(0, round(-5, 10));
321        assert_eq!(0, round(4, 10));
322        assert_eq!(0, round(5, 10));
323        assert_eq!(0, round(6, 10));
324        assert_eq!(10, round(15, 10));
325    }
326
327    #[test]
328    fn round_to_increment_temporal_table_half_ceil() {
329        let round = |quantity: i64, increment: i64| -> i64 {
330            let quantity = NoUnits::new(quantity).unwrap();
331            let increment = NoUnits::new(increment).unwrap();
332            RoundMode::HalfCeil.round(quantity, increment).into()
333        };
334        assert_eq!(-10, round(-15, 10));
335        assert_eq!(0, round(-5, 10));
336        assert_eq!(0, round(4, 10));
337        assert_eq!(10, round(5, 10));
338        assert_eq!(10, round(6, 10));
339        assert_eq!(20, round(15, 10));
340    }
341
342    #[test]
343    fn round_to_increment_temporal_table_half_floor() {
344        let round = |quantity: i64, increment: i64| -> i64 {
345            let quantity = NoUnits::new(quantity).unwrap();
346            let increment = NoUnits::new(increment).unwrap();
347            RoundMode::HalfFloor.round(quantity, increment).into()
348        };
349        assert_eq!(-20, round(-15, 10));
350        assert_eq!(-10, round(-5, 10));
351        assert_eq!(0, round(4, 10));
352        assert_eq!(0, round(5, 10));
353        assert_eq!(10, round(6, 10));
354        assert_eq!(10, round(15, 10));
355    }
356
357    #[test]
358    fn round_to_increment_temporal_table_half_expand() {
359        let round = |quantity: i64, increment: i64| -> i64 {
360            let quantity = NoUnits::new(quantity).unwrap();
361            let increment = NoUnits::new(increment).unwrap();
362            RoundMode::HalfExpand.round(quantity, increment).into()
363        };
364        assert_eq!(-20, round(-15, 10));
365        assert_eq!(-10, round(-5, 10));
366        assert_eq!(0, round(4, 10));
367        assert_eq!(10, round(5, 10));
368        assert_eq!(10, round(6, 10));
369        assert_eq!(20, round(15, 10));
370    }
371
372    #[test]
373    fn round_to_increment_temporal_table_half_trunc() {
374        let round = |quantity: i64, increment: i64| -> i64 {
375            let quantity = NoUnits::new(quantity).unwrap();
376            let increment = NoUnits::new(increment).unwrap();
377            RoundMode::HalfTrunc.round(quantity, increment).into()
378        };
379        assert_eq!(-10, round(-15, 10));
380        assert_eq!(0, round(-5, 10));
381        assert_eq!(0, round(4, 10));
382        assert_eq!(0, round(5, 10));
383        assert_eq!(10, round(6, 10));
384        assert_eq!(10, round(15, 10));
385    }
386
387    #[test]
388    fn round_to_increment_temporal_table_half_even() {
389        let round = |quantity: i64, increment: i64| -> i64 {
390            let quantity = NoUnits::new(quantity).unwrap();
391            let increment = NoUnits::new(increment).unwrap();
392            RoundMode::HalfEven.round(quantity, increment).into()
393        };
394        assert_eq!(-20, round(-15, 10));
395        assert_eq!(0, round(-5, 10));
396        assert_eq!(0, round(4, 10));
397        assert_eq!(0, round(5, 10));
398        assert_eq!(10, round(6, 10));
399        assert_eq!(20, round(15, 10));
400    }
401}