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:
ParseErrorKind | The text |
|---|---|
Empty | is empty |
InvalidDigit | is not a number: a sign, digits, a point, an exponent |
PosOverflow | is above the decimal’s range |
NegOverflow | is below it |
TooManyDecimals | has a non-zero digit past the decimal’s scale |
RangeOutsideBuffer | was 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");
}