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.