1/*!
2This module provides logic for validating rounding increments.
34Each 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*/
1011use crate::{
12 error::{err, Error},
13 util::{
14 rangeint::RFrom,
15 t::{self, Constant, C},
16 },
17 Unit,
18};
1920/// 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`.
28static 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/...
42if 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.
46Ok(t::NoUnits128::rfrom(t::NoUnits::new_unchecked(increment)))
47 } else {
48 get_with_limit(unit, increment, "span", LIMIT)
49 }
50}
5152/// 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`.
61static 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}
7273/// 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`.
82static 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}
9293/// 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`.
102static 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}
112113fn 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.
120let increment = t::NoUnits::new_unchecked(increment);
121if increment <= C(0) {
122return Err(err!(
123"rounding increment {increment} for {unit} must be \
124 greater than zero",
125 unit = unit.plural(),
126 ));
127 }
128let Some(must_divide) = limit.get(unit as usize) else {
129return Err(err!(
130"{what} rounding does not support {unit}",
131 unit = unit.plural()
132 ));
133 };
134let must_divide = t::NoUnits::rfrom(*must_divide);
135if increment >= must_divide || must_divide % increment != C(0) {
136Err(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 {
143Ok(t::NoUnits128::rfrom(increment))
144 }
145}
146147fn 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.
154let increment = t::NoUnits::new_unchecked(increment);
155if increment <= C(0) {
156return Err(err!(
157"rounding increment {increment} for {unit} must be \
158 greater than zero",
159 unit = unit.plural(),
160 ));
161 }
162let Some(must_divide) = max.get(unit as usize) else {
163return Err(err!(
164"{what} rounding does not support {unit}",
165 unit = unit.plural()
166 ));
167 };
168let must_divide = t::NoUnits::rfrom(*must_divide);
169if increment > must_divide || must_divide % increment != C(0) {
170Err(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 {
177Ok(t::NoUnits128::rfrom(increment))
178 }
179}