Trait Bounds
pub trait Bounds: Sized
An interface for defining boundaries on integer values.
An implementation of this trait defines a single contiguous range used
inside of Jiff. For example, the allowed range of years is -9999..=9999.
Each implementation defines its primitive representation, which is usally
the smallest signed integer type that can hold the minimum and maximum
values. This trait provides check, checked_add and checked_mul
methods that callers likely do not need to override.
Other than the associated types and constants, the only required method
that callers must implement is error().
History & Design
It took a lot of design iteration to arrive at this trait. Jiff originally started with an Ada-inspired ranged integer abstraction. At first, it worked really well and seemed to do a decent job at uncovering bugs related to values going outside of their intended range. Or worse, actual integer overflow. This particular abstraction had three key properties:
- Each ranged integer type corresponded to a single contiguous range
(e.g.,
Yearwas-9999..=9999), and the range values were defined by const type parameters. - The value inside of a ranged integer was permitted to "drift" outside
of its defined range (but not outside of its primitive representation).
A panic would only manifest when one tried to look inside the ranged
integer and access its primitive representation (e.g.,
Date::year). - Ranged integers kept track of their minimum and maximum possible values,
at runtime, but only when
debug_assertionswere enabled. So when you didx + y, the result wouldn't just be the actual addition, but also addition performed on the corresponding min and max values on each ofxandy.
When all three of these properties combined, you got an excellent bug
finding tool without necessarily needing to write tests covering all of the
edge cases. For example, if you did x + y and the result only went out
of range when x and y were some extreme values, you'd get an immediate
panic once you accessed the value even when using non-extreme values.
That's because the extreme values are computed dynamically at runtime.
Unfortunately, as Jiff grew bigger, ranged integers became more and more annoying. They have two fatal flaws as conceived above:
- If you "escape" outside of a ranged integer at any point, you lose the tracked min and max values. And thus you lose advantage of ranged integers in the first place.
- Because of (1), it was exceptionally difficult to do any sort of clever representation-based optimizations. Since a ranged integer had its own representation, trying to do, e.g., bit-packing was effectively impossible.
There were some other downsides, although I didn't perceive them as fatal on their own (but they certaintly contributed to my decision to abdandon them):
- Since these were custom types and they were generic, literally none of
them worked in a
constcontext. - Whenever there was conditional control flow involving ranged integers, it was necessary to do strange contortions to get their internal min/max values to line up correctly.
- As time pressed on, the panics surfaced by ranged integers were more and more likely to be as a result of "holding it wrong" and not because of any actual bugs in the arithmetic.
With all of that said, a datetime library has to be quite paranoid about the range of values it permits. And error messages really benefit from being able to encode information about the allowed range so that users know what is and isn't illegal. Hence, I settled on this lighter weight design that still encodes ranges as a static property of the program, but as associated types on a trait. And then that trait provides a few very primitive operations (like checking if an integer is in bounds) that produce an appropriate error message on failure.
Note also that this trait is specifically not implemented for i128.
It's only implemented for i8, i16, i32 and i64. This is because
the check function accepts an i64 as the type that can contain all
possible values for any range type. And then that value is checked before
potentially being converted to a smaller primitive representation. If this
trait supported i128, then one would want a check function where
parameters get converted to i128 and then their ranges are checked. I
did not want this because of the extra costs associated with i128.
Moreover, Jiff is using i128 in a vanishingly small number of locations.
I don't think I'll ever be able to eliminate it entirely, but the number
of locations is small enough that they are special cased and don't need to
fit into this infrastructure.
Finally, a key property of this design is that error values carry no associated data. Instead, they are instantiated as implementations of this trait, and that implementation provides all of the information necessary to craft a half-way decent error message.
Associated Types
type Primitive: Primitive;The primitive integer representation for this boundary type.
This is generally the smallest primitive integer type that fits the minimum and maximum allowed values.
type Error;The error type returned when a value is considered out of range for this particular implementation.
The intended usage is for this type to be an enum of single-field variants. The field is meant to be an instantiation of
RawBoundsErrorwith the type parameter set toSelf.See
BoundsErrorfor an example.
Associated Constants
const WHAT: &'static str;A short human readable description of the values represented by these bounds. This is used in error messages.
const MIN: Self::Primitive;The minimum boundary value.
const MAX: Self::Primitive;The maximum boundary value.
Required Methods
fn error() -> Self::ErrorCreate an error when a value is outside the bounds for this type.
Provided Methods
fn check(n: impl Into<i64>) -> Result<Self::Primitive, Self::Error>Converts the 64-bit integer provided into the primitive representation of these bounds.
Errors
This returns an error if the given integer does not fit in the bounds prescribed by this trait implementation.
fn check_self(n: Self::Primitive) -> Result<Self::Primitive, Self::Error>Checks whether the given integer, in the same primitive representation as this boundary type, is in bounds.
Errors
This returns an error if the given integer does not fit in the bounds prescribed by this trait implementation.
fn checked_add(n1: Self::Primitive, n2: Self::Primitive) -> Result<Self::Primitive, Self::Error>Performs checked addition using this boundary type's primitive representation.
Errors
If the result exceeds the boundaries of the primitive type or of the declared range for this type, then an error is returned.
fn checked_mul(n1: Self::Primitive, n2: Self::Primitive) -> Result<Self::Primitive, Self::Error>Performs checked multiplication using this boundary type's primitive representation.
Errors
If the result exceeds the boundaries of the primitive type or of the declared range for this type, then an error is returned.
Implementors
impl Bounds for CivilDayNanosecondimpl Bounds for CivilDaySecondimpl Bounds for Dayimpl Bounds for DayOfYearimpl Bounds for DayOfYearNoLeapimpl Bounds for DeltaSecondsimpl Bounds for Hourimpl Bounds for ISOWeekimpl Bounds for ISOYearimpl Bounds for Microsecondimpl Bounds for Millisecondimpl Bounds for Minuteimpl Bounds for Monthimpl Bounds for Nanosecondimpl Bounds for NthWeekdayimpl Bounds for NthWeekdayOfMonthimpl Bounds for OffsetHoursimpl Bounds for OffsetMinutesimpl Bounds for OffsetSecondsimpl Bounds for OffsetTotalSecondsimpl Bounds for Secondimpl Bounds for SignedSubsecNanosecondimpl Bounds for SubsecNanosecondimpl Bounds for TimestampArithmeticNanosecondimpl Bounds for UnixEpochDaysimpl Bounds for UnixEpochMicrosecondsimpl Bounds for UnixEpochMillisecondsimpl Bounds for UnixEpochSecondsimpl Bounds for WeekdayMondayOneimpl Bounds for WeekdayMondayZeroimpl Bounds for WeekdaySundayOneimpl Bounds for WeekdaySundayZeroimpl Bounds for Yearimpl Bounds for YearBCEimpl Bounds for YearCE