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

Run-Time Scales

Dynamic is a scale known only at run time: read from a venue’s rules, a database column’s type or the text of a number. A decimal of it carries its scale in a byte beside its steps, so a Decimal<i64, Dynamic> is 16 bytes where a D64<2> is 8. Text read into one takes the scale the text spells:

fn a_run_time_scale_is_the_one_its_text_spells() {
    use ddust::{Decimal, Dynamic};

    let price: Decimal<i64, Dynamic> = "1.50".parse().expect("a decimal");
    assert_eq!((price.steps(), price.decimals()), (150, 2));
    assert_eq!(size_of::<Decimal<i64, Dynamic>>(), 16, "the steps, and a byte of scale");
}

Dynamic::new makes a scale of up to 38 decimals, and returns None past them.

Lining Up

Two values at different run-time scales line up exactly at the finer one, as SQL’s DECIMAL does: the coarser is multiplied by a power of ten, and the operation is done at the finer scale. A sum, a difference, a remainder, a comparison and equality all line up, so 1.5 == 1.50, and the two hash alike. A product takes the sum of the factors’ decimals, as a static one does:

fn two_run_time_scales_line_up_at_the_finer() {
    use ddust::{Decimal, Dynamic};

    let read = |text: &str| -> Decimal<i64, Dynamic> { text.parse().expect("a decimal") };
    let sum = read("1.5") + read("1.25");
    assert_eq!((sum.to_string(), sum.decimals()), ("2.75".into(), 2));
    assert_eq!(read("1.5"), read("1.50"), "equal, whatever their scales");
}
fn a_run_time_product_takes_the_sum_of_its_factors_decimals() {
    use ddust::{Decimal, Dynamic};

    let read = |text: &str| -> Decimal<i64, Dynamic> { text.parse().expect("a decimal") };
    let product = read("1.5") * read("1.25");
    assert_eq!((product.to_string(), product.decimals()), ("1.875".into(), 3));
}

Lining up never rounds. A comparison is exact whatever the two scales are; a sum or a difference that does not fit at the finer scale overflows as any other does, panicking with overflow checks on and wrapping otherwise, and its checked_* method returns None. A product past 38 decimals, a scale no integer holds a step of, panics in every build, and checked_mul returns None for it. A quotient is at the dividend’s scale, as a static one is.

Rescaling

rescale moves a value to another scale exactly, and refuses a non-zero digit past it with TooManyDecimals; rescale_round rounds instead. A size read at five decimals, for a venue whose step is a thousandth:

fn a_scale_read_from_a_venue_rounds_on_purpose() {
    use ddust::round::Floor;
    use ddust::{ConvertErrorKind, Decimal, Dynamic};

    let size_step = Dynamic::new(3).expect("at most 38 decimals");
    let wanted =
        Decimal::<i64, Dynamic>::from_ascii(b"0.12345", Dynamic::new(5).expect("at most 38"))
            .expect("a decimal");
    let refused = wanted.rescale(size_step).expect_err("two decimals past the venue's");
    assert_eq!(refused.kind(), ConvertErrorKind::TooManyDecimals);
    let traded = wanted.rescale_round(size_step, Floor).expect("in range");
    assert_eq!(traded.to_string(), "0.123", "down to the venue's step");
}

Static and Run-Time Scales

A value of a static scale converts into one of a run-time scale by From, at the same decimals, and back by TryFrom, exactly or refused:

fn a_static_scale_converts_to_a_run_time_one_and_back() {
    use ddust::{ConvertError, ConvertErrorKind, D64, Decimal, Dynamic, dec};

    let price: D64<2> = dec!(1.25);
    let carried: Decimal<i64, Dynamic> = price.into();
    assert_eq!(carried.decimals(), 2);
    assert_eq!(D64::<2>::try_from(carried), Ok(price));
    assert_eq!(D64::<4>::try_from(carried), Ok(dec!(1.25)), "zeros added");
    let refused = D64::<1>::try_from(carried).map_err(ConvertError::kind);
    assert_eq!(refused, Err(ConvertErrorKind::TooManyDecimals), "a digit past one decimal");
}

A product of a static scale and a run-time one is at a run-time scale. A decimal of a static scale is the faster and smaller of the two, and a run-time one is for the values whose scale the program learns as it runs; Choosing a Decimal weighs them.

Another Crate’s Scale

Scale is a trait a crate built on ddust can implement, for a scale its own types give, such as a price’s scale that its assets decide. LINES_UP says what two of its values at different decimals do: line up at the finer one, as Dynamic’s do, or never mix, in which case an operator panics in every build and a checked_* method returns None.