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}
402