Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interop

Each crate ddust works with is behind a feature, so a program that does not use it builds none of it. Serialization covers serde, and Testing proptest, arbitrary and rand.

Numeric Traits

With num-traits-02, a decimal implements num-traits 0.2’s traits, so generic numeric code takes it:

TraitFor
CheckedAdd, CheckedSub, CheckedDiv, CheckedRemevery decimal
CheckedNeg, WrappingNegevery signed decimal
SaturatingAdd, SaturatingSub, WrappingAdd, WrappingSuba static scale, and Dynamic
CheckedMulDynamic, whose product is its own type
Zero, ConstZero, Bounded, FromPrimitivea static scale
ToPrimitiveevery decimal
fn generic_numeric_code_takes_a_decimal() {
    use ddust::{D64, dec};
    use num_traits::{CheckedAdd, FromPrimitive, ToPrimitive, Zero};

    fn total<T: Zero + CheckedAdd>(values: &[T]) -> Option<T> {
        values.iter().try_fold(T::zero(), |sum, value| sum.checked_add(value))
    }

    let fills: [D64<2>; 3] = [dec!(1.25), dec!(2.50), dec!(0.75)];
    assert_eq!(total(&fills), Some(dec!(4.5)));
    assert_eq!(total(&[D64::<2>::MAX, dec!(0.01)]), None);
    let rate: Option<D64<2>> = FromPrimitive::from_f64(0.125);
    assert_eq!(rate, Some(dec!(0.12)), "the nearest step, a tie to even");
    assert_eq!(dec!(-2.75: D64<2>).to_i64(), Some(-2), "the whole part, as a float's");
}

Each trait’s method is the decimal’s own method of that name. A product of a static scale is at the sum of its factors’ scales, another type, so CheckedMul is Dynamic’s alone; and the saturating and wrapping traits never panic, so a scale of another crate, whose values may refuse to mix, has none of them. FromPrimitive reads a whole number exactly and a float at the nearest step, a tie to the even one, and returns None past the range or for a float that is not finite. ToPrimitive gives the whole part, truncated toward zero as a float’s as is, or None past the integer’s range, and the nearest f64, from which an f32 is then rounded: rounded twice, so in rare cases a step from the f32 nearest the decimal.

bytemuck and zerocopy

A decimal of a Fixed scale is its integer and nothing else, so its bytes are its steps’. With bytemuck, it is bytemuck’s Pod, and a slice of decimals casts to bytes and back with no copy:

fn a_static_scale_decimal_is_its_steps_bytes() {
    use bytemuck::{cast, cast_slice};
    use ddust::{D64, dec};

    let prices: [D64<2>; 2] = [dec!(1.25), dec!(-0.01)];
    let bytes: &[u8] = cast_slice(&prices);
    assert_eq!(bytes.len(), 16, "eight bytes a price, and nothing else");
    assert_eq!(cast_slice::<u8, D64<2>>(bytes), prices);
    assert_eq!(cast::<D64<2>, i64>(prices[0]), 125, "the steps");
}

With zerocopy-08, it is zerocopy 0.8’s FromBytes and IntoBytes, and every decimal is KnownLayout and Immutable:

fn a_static_scale_decimal_is_read_from_bytes_in_place() {
    use ddust::{D64, dec};
    use zerocopy::{FromBytes, IntoBytes};

    let price: D64<2> = dec!(64250.75);
    let bytes = price.as_bytes();
    assert_eq!(bytes, 6_425_075_i64.as_bytes());
    assert_eq!(D64::<2>::read_from_bytes(bytes), Ok(price));
}

The bytes are the steps alone, so the scale comes from the type: a reader of the bytes knows them as a D64<2> because its schema says so. A decimal of a run-time scale is never read from bytes, since its scale’s byte must be 0 to 38, nor written as bytes, since its padding holds no value; bytemuck can zero it, and a Dynamic alone is written as its byte. The compiler refuses the rest:

//! A run-time scale's byte may be past 38 decimals, so neither it nor a decimal
//! of it reads from bytes.

use ddust::{Decimal, Dynamic};
use zerocopy::FromBytes;

fn main() {
    let _scale = Dynamic::read_from_bytes(&[4]);
    let _price = Decimal::<i64, Dynamic>::read_from_bytes(&[0; 16]);
}
error[E0599]: no associated function or constant named `read_from_bytes` found for struct `Dynamic` in the current scope
 --> tests/compile_fail_bytes/a_run_time_scale_is_never_read_from_bytes_by_zerocopy.rs:8:27
  |
8 |     let _scale = Dynamic::read_from_bytes(&[4]);
  |                           ^^^^^^^^^^^^^^^ associated function or constant not found in `Dynamic`
  |
note: if you're trying to build a new `Dynamic`, consider using `Dynamic::new` which returns `Option<Dynamic>`
 --> src/scale.rs
  |
  |     pub const fn new(decimals: u8) -> Option<Self> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: there is a method `as_bytes` with a similar name, but with different arguments
 --> $CARGO/zerocopy-$VERSION/src/lib.rs
  |
  | /     fn as_bytes(&self) -> &[u8]
  | |     where
  | |         Self: Immutable,
  | |________________________^

error[E0599]: the associated function or constant `read_from_bytes` exists for struct `Decimal<i64, Dynamic>`, but its trait bounds were not satisfied
 --> tests/compile_fail_bytes/a_run_time_scale_is_never_read_from_bytes_by_zerocopy.rs:9:43
  |
9 |     let _price = Decimal::<i64, Dynamic>::read_from_bytes(&[0; 16]);
  |                                           ^^^^^^^^^^^^^^^ associated function or constant cannot be called on `Decimal<i64, Dynamic>` due to unsatisfied trait bounds
  |
 ::: src/scale.rs
  |
  | pub struct Dynamic {
  | ------------------ doesn't satisfy `Dynamic: FromBytes`
  |
 ::: src/decimal.rs
  |
  | pub struct Decimal<I, S> {
  | ------------------------ doesn't satisfy `Decimal<i64, Dynamic>: FromBytes`
  |
note: if you're trying to build a new `Decimal<i64, Dynamic>` consider using one of the following associated functions:
      ddust::convert::<impl Decimal<I, S>>::from_int
      Decimal::<I, S>::from_steps
      Decimal::<I, S>::new
      Decimal::<I, S>::new_round
      and $N others
 --> src/decimal.rs
  |
  |       pub const fn from_steps(steps: I, scale: S) -> Self {
  |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
  | /     pub const fn new(digits: I, decimals: u8, scale: S) -> Result<Self, ConvertError>
  | |     where
  | |         I: [const] Int,
  | |         S: [const] Scale,
  | |_________________________^
...
  | /     pub const fn new_round<R>(
  | |         digits: I, decimals: u8, scale: S, mode: R,
  | |     ) -> Result<Self, ConvertError>
  | |     where
  | |         I: [const] Int,
  | |         S: [const] Scale,
  | |         R: [const] RoundingMode,
  | |________________________________^
  |
 ::: src/convert.rs
  |
  | /     pub const fn from_int(n: I, scale: S) -> Result<Self, ConvertError>
  | |     where
  | |         I: [const] Int,
  | |         S: [const] Scale,
  | |_________________________^
  = note: the following trait bounds were not satisfied:
          `Dynamic: FromBytes`
          which is required by `Decimal<i64, Dynamic>: FromBytes`

defmt

With defmt, a decimal implements defmt’s Format, so firmware logs it as its shortest exact decimal, the text Display writes, without core::fmt. Fixed<D> logs as its Debug writes it, with its decimals.