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:
| Trait | For |
|---|---|
CheckedAdd, CheckedSub, CheckedDiv, CheckedRem | every decimal |
CheckedNeg, WrappingNeg | every signed decimal |
SaturatingAdd, SaturatingSub, WrappingAdd, WrappingSub | a static scale, and Dynamic |
CheckedMul | Dynamic, whose product is its own type |
Zero, ConstZero, Bounded, FromPrimitive | a static scale |
ToPrimitive | every 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.