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

Text

Every value has one spelling. Display writes its shortest exact decimal, and the readers take it back exactly or refuse it. Nothing on that path goes through a float.

Reading

FromStr, from_ascii for bytes, and the other readers take what f64::from_str takes, without its infinities and NaN: a sign, digits with an optional point, and an optional exponent. Zeros past the scale’s decimals are allowed; a non-zero digit there is refused, never rounded:

fn text_is_read_exactly_or_refused() {
    use ddust::{D64, ParseError, ParseErrorKind, dec};

    let read = |text: &str| text.parse::<D64<2>>().map_err(ParseError::kind);
    assert_eq!(read("12.30"), Ok(dec!(12.3)));
    assert_eq!(read("1.2300"), read("1.23"), "zeros past the scale are no digits");
    assert_eq!(read("-.5"), read("-0.50"));
    assert_eq!(read("1.5e3"), read("1500"), "an exponent, as Rust's floats are written");
    assert_eq!(read("1.234"), Err(ParseErrorKind::TooManyDecimals), "never rounded unasked");
    assert_eq!(read("1,000"), Err(ParseErrorKind::InvalidDigit));
    assert_eq!(read("NaN"), Err(ParseErrorKind::InvalidDigit), "a decimal has no NaN");
    assert_eq!(read("1e17"), Err(ParseErrorKind::PosOverflow));
    assert_eq!(read(""), Err(ParseErrorKind::Empty));
}

A refusal is a ParseError, whose kind says why:

ParseErrorKindThe text
Emptyis empty
InvalidDigitis not a number: a sign, digits, a point, an exponent
PosOverflowis above the decimal’s range
NegOverflowis below it
TooManyDecimalshas a non-zero digit past the decimal’s scale
RangeOutsideBufferwas asked for at a range its buffer does not hold

A decimal of a run-time scale reads at the scale its text spells, so "1.50" is 1.50 at two decimals; from_ascii takes a scale to read at instead.

Text with more decimals than the scale is rounded only when asked, by from_ascii_round:

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

    let price = D64::from_ascii_round(b"64250.745", Fixed::<2>, HalfEven).expect("in range");
    assert_eq!(price, dec!(64250.74), "a tie, to the even cent");
}

Reading Inside a Message

A number inside a message is read where it lies, with no copy. from_ascii_at takes the buffer and the range the number fills, and from_ascii_prefix reads the number at the front of a slice and says how many bytes it took, for a reader that has not found where it ends:

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

    let message = br#"{"p":"64250.75","q":"0.0153"}"#;
    let price = D64::from_ascii_at(message, 6..14, Fixed::<2>).expect("a price");
    assert_eq!(price, dec!(64250.75));
    let (quantity, length) = D64::from_ascii_prefix(&message[21..], Fixed::<4>).expect("a size");
    assert_eq!((quantity, length), (dec!(0.0153), 6), "the number, and the bytes it took");
}

On a build with a vector unit, a number of up to 32 bytes is read in 32 lanes at once, NEON’s on aarch64 and SSSE3’s on x86_64. from_ascii reads only its slice: the first 16 bytes and the last 16 of a number that long, and of a shorter one its first eight and last eight, or the one word it fits in. from_ascii_at reads the 16 bytes from the number’s start and the 16 before its end wherever the buffer holds them, so it reads every number the same way, whatever its length. Platforms and Features says which builds have the lanes.

Writing

Display writes the shortest exact decimal. {:#} writes every decimal of the scale, {:.N} exactly N decimals, a tie rounded to even as f64’s is, and width, fill and sign work as an integer’s do. Debug writes as {:#}, so a run-time value’s scale shows, and LowerExp and UpperExp write scientific notation:

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

    let price: D64<4> = dec!(1.2500);
    assert_eq!(format!("{price}"), "1.25", "the shortest exact decimal");
    assert_eq!(format!("{price:#}"), "1.2500", "every decimal of the scale");
    assert_eq!(format!("{price:.1}"), "1.2", "a tie, to the even digit");
    assert_eq!(format!("{price:>+8}"), "   +1.25", "width, fill and sign as an integer's");
    assert_eq!(format!("{price:e}"), "1.25e0");
    assert_eq!(format!("{price:?}"), "1.2500");
}

write_ascii writes the same text as Display into a byte buffer, with no formatter, and returns its length, or None when the buffer is too short. It touches no byte past the text, and MAX_ASCII_LEN bytes, 41, hold every value’s: a sign, 39 digits and a point.

fn a_value_is_written_into_bytes_with_no_formatter() {
    use ddust::{D128, MAX_ASCII_LEN, dec};

    let mut buffer = [0_u8; MAX_ASCII_LEN];
    let value: D128<18> = dec!(-0.000000000000000001);
    let length = value.write_ascii(&mut buffer).expect("room for every value");
    assert_eq!(&buffer[..length], b"-0.000000000000000001");
    assert_eq!(value.write_ascii(&mut [0; 8]), None, "a buffer too short is refused");
}