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:

  1. Each ranged integer type corresponded to a single contiguous range (e.g., Year was -9999..=9999), and the range values were defined by const type parameters.
  2. 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).
  3. Ranged integers kept track of their minimum and maximum possible values, at runtime, but only when debug_assertions were enabled. So when you did x + y, the result wouldn't just be the actual addition, but also addition performed on the corresponding min and max values on each of x and y.

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:

  1. 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.
  2. 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):

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 RawBoundsError with the type parameter set to Self.

See BoundsError for 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::Error

Create 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