jiff/util/round/
increment.rs

1/*!
2This module provides logic for validating rounding increments.
3
4Each of the types we support rounding for have their own logic for how the
5rounding increment is validated. For example, when rounding timestamps, only
6rounding increments up to hours are supported. But when rounding datetimes,
7rounding increments up to days are supported. Similarly, rounding increments
8for time units must divide evenly into 1 unit of the next highest unit.
9*/
10
11use crate::{
12    error::{err, Error},
13    util::{
14        rangeint::RFrom,
15        t::{self, Constant, C},
16    },
17    Unit,
18};
19
20/// Validates the given rounding increment for the given unit.
21///
22/// This validation ensures the rounding increment is valid for rounding spans.
23pub(crate) fn for_span(
24    unit: Unit,
25    increment: i64,
26) -> Result<t::NoUnits128, Error> {
27    // Indexed by `Unit`.
28    static LIMIT: &[Constant] = &[
29        t::NANOS_PER_MICRO,
30        t::MICROS_PER_MILLI,
31        t::MILLIS_PER_SECOND,
32        t::SECONDS_PER_MINUTE,
33        t::MINUTES_PER_HOUR,
34        t::HOURS_PER_CIVIL_DAY,
35    ];
36    // We allow any kind of increment for calendar units, but for time units,
37    // they have to divide evenly into the next highest unit (and also be less
38    // than that). The reason for this is that calendar units vary, where as
39    // for time units, given a balanced span, you know that time units will
40    // always spill over into days so that hours/minutes/... will never exceed
41    // 24/60/...
42    if unit >= Unit::Day {
43        // We specifically go from NoUnits to NoUnits128 here instead of
44        // directly to NoUnits128 to ensure our increment bounds match the
45        // bounds of i64 and not i128.
46        Ok(t::NoUnits128::rfrom(t::NoUnits::new_unchecked(increment)))
47    } else {
48        get_with_limit(unit, increment, "span", LIMIT)
49    }
50}
51
52/// Validates the given rounding increment for the given unit.
53///
54/// This validation ensures the rounding increment is valid for rounding
55/// datetimes (both civil and time zone aware).
56pub(crate) fn for_datetime(
57    unit: Unit,
58    increment: i64,
59) -> Result<t::NoUnits128, Error> {
60    // Indexed by `Unit`.
61    static LIMIT: &[Constant] = &[
62        t::NANOS_PER_MICRO,
63        t::MICROS_PER_MILLI,
64        t::MILLIS_PER_SECOND,
65        t::SECONDS_PER_MINUTE,
66        t::MINUTES_PER_HOUR,
67        t::HOURS_PER_CIVIL_DAY,
68        Constant(2),
69    ];
70    get_with_limit(unit, increment, "datetime", LIMIT)
71}
72
73/// Validates the given rounding increment for the given unit.
74///
75/// This validation ensures the rounding increment is valid for rounding
76/// civil times.
77pub(crate) fn for_time(
78    unit: Unit,
79    increment: i64,
80) -> Result<t::NoUnits128, Error> {
81    // Indexed by `Unit`.
82    static LIMIT: &[Constant] = &[
83        t::NANOS_PER_MICRO,
84        t::MICROS_PER_MILLI,
85        t::MILLIS_PER_SECOND,
86        t::SECONDS_PER_MINUTE,
87        t::MINUTES_PER_HOUR,
88        t::HOURS_PER_CIVIL_DAY,
89    ];
90    get_with_limit(unit, increment, "time", LIMIT)
91}
92
93/// Validates the given rounding increment for the given unit.
94///
95/// This validation ensures the rounding increment is valid for rounding
96/// timestamps.
97pub(crate) fn for_timestamp(
98    unit: Unit,
99    increment: i64,
100) -> Result<t::NoUnits128, Error> {
101    // Indexed by `Unit`.
102    static MAX: &[Constant] = &[
103        t::NANOS_PER_CIVIL_DAY,
104        t::MICROS_PER_CIVIL_DAY,
105        t::MILLIS_PER_CIVIL_DAY,
106        t::SECONDS_PER_CIVIL_DAY,
107        t::MINUTES_PER_CIVIL_DAY,
108        t::HOURS_PER_CIVIL_DAY,
109    ];
110    get_with_max(unit, increment, "timestamp", MAX)
111}
112
113fn get_with_limit(
114    unit: Unit,
115    increment: i64,
116    what: &'static str,
117    limit: &[t::Constant],
118) -> Result<t::NoUnits128, Error> {
119    // OK because `NoUnits` specifically allows any `i64` value.
120    let increment = t::NoUnits::new_unchecked(increment);
121    if increment <= C(0) {
122        return Err(err!(
123            "rounding increment {increment} for {unit} must be \
124             greater than zero",
125            unit = unit.plural(),
126        ));
127    }
128    let Some(must_divide) = limit.get(unit as usize) else {
129        return Err(err!(
130            "{what} rounding does not support {unit}",
131            unit = unit.plural()
132        ));
133    };
134    let must_divide = t::NoUnits::rfrom(*must_divide);
135    if increment >= must_divide || must_divide % increment != C(0) {
136        Err(err!(
137            "increment {increment} for rounding {what} to {unit} \
138             must be 1) less than {must_divide}, 2) divide into \
139             it evenly and 3) greater than zero",
140            unit = unit.plural(),
141        ))
142    } else {
143        Ok(t::NoUnits128::rfrom(increment))
144    }
145}
146
147fn get_with_max(
148    unit: Unit,
149    increment: i64,
150    what: &'static str,
151    max: &[t::Constant],
152) -> Result<t::NoUnits128, Error> {
153    // OK because `NoUnits` specifically allows any `i64` value.
154    let increment = t::NoUnits::new_unchecked(increment);
155    if increment <= C(0) {
156        return Err(err!(
157            "rounding increment {increment} for {unit} must be \
158             greater than zero",
159            unit = unit.plural(),
160        ));
161    }
162    let Some(must_divide) = max.get(unit as usize) else {
163        return Err(err!(
164            "{what} rounding does not support {unit}",
165            unit = unit.plural()
166        ));
167    };
168    let must_divide = t::NoUnits::rfrom(*must_divide);
169    if increment > must_divide || must_divide % increment != C(0) {
170        Err(err!(
171            "increment {increment} for rounding {what} to {unit} \
172             must be 1) less than or equal to {must_divide}, \
173             2) divide into it evenly and 3) greater than zero",
174            unit = unit.plural(),
175        ))
176    } else {
177        Ok(t::NoUnits128::rfrom(increment))
178    }
179}