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

Decimals and Scales

A Decimal<I, S> is a whole number of steps, held in an integer I, at a scale S that says how many decimals a step is. 12.34 at two decimals is 1,234 steps of 0.01:

fn a_decimal_is_a_whole_number_of_steps() {
    use ddust::{D64, Fixed, dec};

    let price: D64<2> = dec!(12.34);
    assert_eq!(price.steps(), 1_234, "1,234 steps of 0.01");
    assert_eq!(price.decimals(), 2);
    assert_eq!(D64::from_steps(1_234, Fixed::<2>), price);
    assert_eq!(size_of::<D64<2>>(), size_of::<i64>(), "the scale takes no room");
}

The steps are the decimal’s whole state. Adding two values of one scale adds their steps, comparing them compares their steps, and nothing is normalized or rescaled behind the caller’s back.

The Integer

I is one of Rust’s ten integers, which Int names; the five signed ones are Signed too. Each has an alias at a static scale of D decimals:

SignedUnsignedBytesDigits every value has room for
D8<D>UD8<D>12
D16<D>UD16<D>24
D32<D>UD32<D>49
D64<D>UD64<D>818, and 19 unsigned
D128<D>UD128<D>1638

The scale splits those digits between the whole part and the fraction: a D64<8> has room for ten whole digits and eight decimals, and a D128<18> for twenty and eighteen. The integer’s range is the decimal’s, so a D64<2> runs from −92,233,720,368,547,758.08 to 92,233,720,368,547,758.07.

The Scale

Fixed<D> is a scale the compiler knows: zero-sized, so a decimal of it is its integer alone, and two values of it never differ in scale. D is at most MAX_DECIMALS, 38, since is the largest power of ten a 128-bit integer holds; a Fixed<39> fails the build. Dynamic carries its scale at run time, in a byte beside the steps, and has a chapter of its own, Run-Time Scales.

Two static scales are two types, so their values never meet until one is converted:

//! Two static scales are two types: their values never add.

use ddust::{D64, dec};

fn main() {
    let (tenths, hundredths): (D64<1>, D64<2>) = (dec!(1.5), dec!(1.25));
    let _sum = tenths + hundredths;
}
error[E0308]: mismatched types
 --> tests/compile_fail/two_static_scales_never_mix.rs:7:25
  |
7 |     let _sum = tenths + hundredths;
  |                         ^^^^^^^^^^ expected `1`, found `2`
  |
  = note: expected struct `Decimal<i64, Fixed<1>>`
             found struct `Decimal<i64, Fixed<2>>`

Making a Decimal

A program reads a decimal from text, as Text shows, or writes it as a literal. dec! takes its type from where it goes, or from a type named after a colon, and is checked when the constant is evaluated: a literal with more decimals than its scale, past its range, or negative for an unsigned decimal fails the build. The value is the literal exactly, never rounded.

fn a_literal_is_checked_when_the_constant_is_evaluated() {
    use ddust::{D64, D128, dec};

    const FEE: D64<4> = dec!(0.0025);
    const RATE: D128<18> = dec!(1.2e-5);
    assert_eq!(FEE.steps(), 25);
    assert_eq!(RATE.to_string(), "0.000012");
    assert_eq!(dec!(-7.5: D64<1>).steps(), -75, "a type named after a colon");
}

From digits known at run time, new takes the digits, their decimals and the scale, and makes exactly, or refuses it with a ConvertError: TooManyDecimals for a non-zero digit past the scale, PosOverflow or NegOverflow past the range. new_round rounds where new refuses, and to_digits goes back:

fn digits_and_decimals_make_a_decimal_exactly() {
    use ddust::round::HalfEven;
    use ddust::{ConvertErrorKind, D64, Fixed, dec};

    let half: D64<1> = D64::new(600_005, 1, Fixed).expect("one decimal fits one");
    assert_eq!(half, dec!(60_000.5));
    let refused = D64::new(600_005, 1, Fixed::<0>).expect_err("a decimal past the scale");
    assert_eq!(refused.kind(), ConvertErrorKind::TooManyDecimals);
    assert_eq!(D64::new_round(600_005, 1, Fixed::<0>, HalfEven), Ok(dec!(60_000: D64<0>)));
    assert_eq!(half.to_digits(4), Ok(600_005_000), "back to digits, at four decimals");
}

from_steps and steps are the raw representation, for storage and for types built on a decimal. from_int makes a whole number, and from_f64 reads a double, as Floats shows.

Widths

A value moves to a wider integer at the same scale by From, since every value fits, and back by TryFrom, which refuses one that does not. widen moves to the integer twice as wide, from D8 to D16 up to D64 to D128, and the unsigned alike, where a product or more digits have room:

fn a_value_moves_to_a_wider_integer_freely_and_back_by_a_check() {
    use ddust::{ConvertErrorKind, D32, D64, dec};

    let small: D32<2> = dec!(19.99);
    let wide: D64<2> = small.into();
    assert_eq!(D32::try_from(wide), Ok(small), "it fits again");
    let large: D64<2> = dec!(30_000_000);
    let refused = D32::<2>::try_from(large).expect_err("past an i32's 21,474,836.47");
    assert_eq!(refused.kind(), ConvertErrorKind::PosOverflow);
}

Values of two widths never mix, as two integers do not:

//! Two integers are two types: values of two widths never add until one is converted.

use ddust::{D32, D64, dec};

fn main() {
    let (wide, narrow): (D64<2>, D32<2>) = (dec!(1.5), dec!(1.25));
    let _sum = wide + narrow;
}
error[E0308]: mismatched types
 --> tests/compile_fail/two_widths_never_mix.rs:7:23
  |
7 |     let _sum = wide + narrow;
  |                       ^^^^^^ expected `Decimal<i64, Fixed<2>>`, found `Decimal<i32, Fixed<2>>`
  |
  = note: expected struct `Decimal<i64, Fixed<2>>`
             found struct `Decimal<i32, Fixed<2>>`
help: call `Into::into` on this expression to convert `Decimal<i32, Fixed<2>>` into `Decimal<i64, Fixed<2>>`
  |
7 |     let _sum = wide + narrow.into();
  |                             +++++++

Products

A product’s scale is the sum of its factors’ decimals, so a product of two decimals is exact. Its type is a Sum of the two scales, which converts into the Fixed of their sum with its steps unchanged; a Fixed of any other decimals fails the build:

fn a_product_takes_the_sum_of_its_factors_scales() {
    use ddust::{D64, dec};

    let (price, quantity): (D64<2>, D64<4>) = (dec!(1.25), dec!(0.0003));
    let notional: D64<6> = (price * quantity).into();
    assert_eq!(notional.steps(), 375, "1.25 × 0.0003 is 0.000375, exactly");
}

A product’s scale could be written in the type, Fixed<{ A + B }>, only with the unstable and incomplete generic_const_exprs; Sum gives the same result without it.