Shared Rust kernels
catalogue/rounding
All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.
call · 18 lines · calc-func/src/catalogue/rounding.rs on GitHub/// An eager named call that takes from `min` to as many arguments as it names parameters.
const fn call(
id: FunctionId,
spelling: &'static str,
min: usize,
parameters: &'static [Parameter],
kernel: EagerKernel,
) -> CatalogueEntry {
CatalogueEntry {
id,
spelling,
notation: Notation::Call,
arity: CallableArityShape::range(min, parameters.len()),
parameters,
demand: ArgumentDemand::Eager,
kernel: Kernel::Eager(kernel),
}
}
Used by 15 functions.
numeric · 23 lines · calc-func/src/catalogue/rounding.rs on GitHub/// Runs `compute` on the call's arguments, read as numbers in order, and publishes its number.
fn numeric(
id: FunctionId,
arguments: &[CalcValue],
services: &EvalServices<'_>,
compute: fn(&Site<'_>, &[f64]) -> Step<f64>,
) -> Result<CalcValue, FunctionFault> {
let site = Site::new(id, services);
let flush_input = matches!(id, TRUNC | ROUND | ROUNDUP | ROUNDDOWN | MOD);
Site::finish(numbers(&site, arguments, flush_input).and_then(|read| {
let result = compute(&site, &read)?;
// Only `stored` makes a NaN here: Excel's own bit pattern for a result past DBL_MAX,
// which the worksheet holds as a number.
if result.is_nan() && matches!(id, TRUNC | ROUND | ROUNDDOWN) {
Ok(CalcValue::excel_overflow_number(result).expect("the stored rounding result"))
} else if result.is_subnormal() && matches!(id, TRUNC | ROUNDUP | ROUNDDOWN) {
// Only the measured initial-reading return below can reach this publication.
super::kit::out_with_subnormal(&site, Ok(result))
} else {
site.number(result)
}
}))
}
Used by 15 functions.
numbers · 25 lines · calc-func/src/catalogue/rounding.rs on GitHub/// The arguments as numbers: an array argument is refused first, then each argument is read in
/// order, so the first one's error is the result.
fn numbers(site: &Site<'_>, arguments: &[CalcValue], flush_input: bool) -> Step<Vec<f64>> {
site.scalar_operands(arguments)?;
arguments
.iter()
.enumerate()
.map(|(index, argument)| {
// W111 typed MROUND/QUOTIENT readings: logicals are #VALUE!, an explicit
// omission #N/A. A referenced blank remains Empty and coerces to zero.
if matches!(site.id(), MROUND | QUOTIENT) {
match argument.core_ref() {
CoreRef::Logical(_) => return Err(Exit::Error(WorksheetErrorCode::Value)),
CoreRef::Missing => return Err(Exit::Error(WorksheetErrorCode::NA)),
_ => {}
}
}
if flush_input && argument.as_number().is_some_and(f64::is_subnormal) {
Ok(0.0)
} else {
site.number_argument(index, argument)
}
})
.collect()
}
Used by 15 functions.
abs · 11 lines · calc-func/src/catalogue/rounding.rs on GitHub/// ABS: the number without its sign.
fn abs(
arguments: &[CalcValue],
services: &mut EvalServices<'_>,
) -> Result<CalcValue, FunctionFault> {
if arguments[0].is_excel_overflow() {
let magnitude = arguments[0].as_number().expect("qualified overflow number").abs();
return Ok(CalcValue::excel_overflow_number(magnitude).expect("sign removal retains overflow encoding"));
}
numeric(ABS, arguments, services, |_, n| Ok(n[0].abs()))
}
Used by 8 functions.
reading · 6 lines · calc-func/src/catalogue/rounding.rs on GitHub/// The fifteen significant digits the rounding functions round: the x87 reading where it is
/// known ([`ExactDecimal::fifteen_x87`]), and otherwise the number read to fifteen significant
/// digits, a half away from zero ([`ExactDecimal::fifteen`]).
fn reading(number: f64, ties_up: bool) -> ExactDecimal {
ExactDecimal::fifteen_x87(number, ties_up).unwrap_or_else(|| ExactDecimal::fifteen(number).0)
}
Used by 5 functions.
flush · 5 lines · calc-func/src/catalogue/rounding.rs on GitHub/// A result below the normal range is 0, as Excel gives it (dc-bd5:
/// ROUNDDOWN(2.5E-308,308) is 0 in Excel 16.0.20430).
fn flush(result: f64) -> f64 {
if result.is_subnormal() { 0.0 } else { result }
}
Used by 4 functions.
whole_reading · 9 lines · calc-func/src/catalogue/rounding.rs on GitHub/// The reading itself as the result: the number unchanged, with no rounding, when the reading
/// is its exact value, and otherwise the reading converted at an arithmetic site.
fn whole_reading(site: &Site<'_>, number: f64, reading: &ExactDecimal) -> Step<f64> {
if reading.is_value_of(number) {
Ok(number)
} else {
Ok(flush(stored(site, reading)?))
}
}
Used by 4 functions.
stored · 26 lines · calc-func/src/catalogue/rounding.rs on GitHub/// An exact decimal result as Excel stores it in binary64: the nearest binary64, except that a
/// result of 2^1024 or more keeps its 53-bit significand and carries its exponent into the
/// exponent field without a check, so the field reads 2047 and the bits are a NaN. So
/// ROUND(1.7976931348623157E+308,-308), 2E+308, is 0x7ff1ccf385ebc8a0 and the reading of
/// DBL_MAX, 1.79769313486232E+308, is 0x7ff000000000000a, as Excel 16.0.20430 and 16.0.20527 give
/// them (627 of 627 such bank rows). Excel shows such a value as 2E+308, ISNUMBER is TRUE, and
/// arithmetic on it is #NUM!. A decimal of 2^1025 or more, which no reading reaches, is #NUM!.
fn stored(site: &Site<'_>, decimal: &ExactDecimal) -> Step<f64> {
let nearest = site.nearest(decimal)?;
if nearest.is_finite() {
return Ok(nearest);
}
// Half the decimal, exactly: the same significand one binade lower.
let mut coefficient = decimal.coefficient.clone();
coefficient.add(&decimal.coefficient.times(&decimal::Natural::from_u64(4)));
let half = site.nearest(&ExactDecimal {
negative: decimal.negative,
coefficient,
exponent: decimal.exponent - 1,
})?;
if half.is_finite() {
Ok(f64::from_bits(half.to_bits() + (1 << 52)))
} else {
Err(Exit::Error(WorksheetErrorCode::Num))
}
}
Used by 4 functions.
directed_places · 20 lines · calc-func/src/catalogue/rounding.rs on GitHub/// How ROUNDUP, ROUNDDOWN and TRUNC count num_digits: the 16 low bits of the integer part of its
/// magnitude, with its sign. A magnitude of 2^32 or more, or one whose 16 low bits reach 32768,
/// counts as 100 places either way and is out of range (false in the second place). So 32768 to
/// 65535 places count as 100, 65537 as 1, and -2147483748 as -100, as Excel 16.0.20430 gives
/// them (the OxFunc-Evidence bank's count-refinement and held-out runs).
fn directed_places(places: f64) -> (i32, bool) {
let magnitude = places.abs();
let low = if magnitude >= 4_294_967_296.0 {
None
} else {
// Below 2^32, so the conversion is exact; the mask keeps 16 bits.
let low = (decimal::truncate(magnitude) as u64) & 0xFFFF;
(low < 0x8000).then(|| i32::try_from(low).expect("15 bits"))
};
let (count, in_range) = match low {
Some(low) => (low, true),
None => (100, false),
};
(if places < 0.0 { -count } else { count }, in_range)
}
Used by 3 functions.
directed · 57 lines · calc-func/src/catalogue/rounding.rs on GitHub/// ROUNDUP's, ROUNDDOWN's and TRUNC's kernel: the reading truncated at 10^-places, and for
/// ROUNDUP, when a digit was dropped, that truncation plus one unit of the place, the two
/// converted to binary64 and added at an arithmetic site (so ROUNDUP(-1.125,2) is
/// -1.1300000000000001 and ROUNDUP(3.535,2) 3.5399999999999996, as Excel gives them). With
/// num_digits out of range ROUNDUP adds no unit for a positive count and is #NUM! for a negative
/// one whenever a digit is dropped.
fn directed(site: &Site<'_>, number: f64, places: f64, direction: Direction) -> Step<f64> {
if number == 0.0 {
return Ok(0.0);
}
let (places, in_range) = directed_places(places);
let position = (-places).clamp(-PLACES, PLACES);
let reading = reading(number, true);
if let Some(initial) = directed_initial_underflow(site, number, &reading)? {
return Ok(initial);
}
let Some(down) = reading.round_at(position, Direction::TowardZero) else {
let whole = whole_reading(site, number, &reading)?;
// ROUNDUP gives #NUM! where ROUNDDOWN and TRUNC keep the bits past DBL_MAX (Excel
// 16.0.20430: 102 of 102 bank rows, ROUNDUP(1.7976931348623153E+308,0) among them).
return if whole.is_nan() && direction == Direction::AwayFromZero {
Err(Exit::Error(WorksheetErrorCode::Num))
} else {
Ok(whole)
};
};
let lower = stored(site, &down)?;
let dropped = direction == Direction::AwayFromZero
&& reading
.round_at(position, Direction::AwayFromZero)
.is_some_and(|up| up.coefficient != down.coefficient);
if !dropped {
return Ok(flush(lower));
}
if !in_range {
return if places > 0 {
Ok(flush(lower))
} else {
Err(Exit::Error(WorksheetErrorCode::Num))
};
}
if lower.is_nan() {
// Past DBL_MAX, adding the unit is an overflow: #NUM!, as Excel gives it.
return Err(Exit::Error(WorksheetErrorCode::Num));
}
let unit = site.nearest(&ExactDecimal::unit(position))?;
// A unit below the normal range is not added, whichever the sign, and a positive number whose
// reading lies less than 2^-1026 above the truncation, both as binary64, stays at it (Excel
// 16.0.20527: ROUNDUP(1.0001086461844975E-307,307) is 1E-307, while
// ROUNDUP(-1.0001086461844975E-307,307) is -2E-307). The negative rule has no such distance.
if unit.is_subnormal() || (number > 0.0 && site.nearest(&reading)? - lower < SMALLEST_DISTANCE)
{
return Ok(flush(lower));
}
let magnitude = site.add(lower.abs(), unit)?;
Ok(flush(if number < 0.0 { -magnitude } else { magnitude }))
}
Used by 3 functions.
directed_initial_underflow · 25 lines · calc-func/src/catalogue/rounding.rs on GitHub/// A normal input whose initial15-digit reading falls below MIN_NORMAL returns before
/// digit rounding in the directed functions. Excel20430 and20527 retain its sign and the
/// normalized significand with exponent field zero, without shifting for a subnormal.
/// Doubling the exact decimal gives a normal value; decrementing its encoded exponent
/// reproduces that observed packing. This is an output model, not an internal-code claim.
/// The other directed paths still flush final subnormal results; ROUND keeps its own policy.
fn directed_initial_underflow(
site: &Site<'_>,
number: f64,
reading: &ExactDecimal,
) -> Step<Option<f64>> {
// Only the lowest normal binade can have a15-digit reading below MIN_NORMAL.
// Keep arithmetic-free exact calls on their existing path, including under Exact mode.
if number.abs() >= 2.0 * f64::MIN_POSITIVE || reading.is_value_of(number) {
return Ok(None);
}
if !site.nearest(reading)?.is_subnormal() {
return Ok(None);
}
let mut twice = reading.clone();
twice.coefficient.add(&reading.coefficient);
let normal = site.nearest(&twice)?;
debug_assert!(normal.is_normal());
Ok(Some(f64::from_bits(normal.to_bits() - (1 << 52))))
}
Used by 3 functions.
ceiling_for · 16 lines · calc-func/src/catalogue/rounding.rs on GitHubfn ceiling_for(
id: FunctionId,
arguments: &[CalcValue],
services: &mut EvalServices<'_>,
) -> Result<CalcValue, FunctionFault> {
numeric(id, arguments, services, |site, n| {
let (number, significance) = (n[0], n[1]);
if significance == 0.0 || number == 0.0 {
Ok(0.0)
} else if number > 0.0 && significance < 0.0 {
Err(Exit::Error(WorksheetErrorCode::Num))
} else {
to_multiple(site, number, significance, Toward::Up)
}
})
}
Used by 2 functions.
to_multiple · 23 lines · calc-func/src/catalogue/rounding.rs on GitHub/// The multiple of `significance` that `toward` chooses for `number`, both non-zero, in binary64:
/// significance * ceil(number / significance) for CEILING and significance *
/// floor(number / significance) for FLOOR, whatever the signs. The division and the product are
/// arithmetic sites, each rounding once; the integer above or below a binary64 is exact. The
/// quotient is first rounded to 15 significant digits, so one that rounds to just below or above
/// an integer counts as that integer (0.3 / 0.1 is 2.9999999999999996, counted as 3), and a
/// quotient beyond binary64's range gives an infinite product, which the number rule publishes
/// as #NUM!.
fn to_multiple(site: &Site<'_>, number: f64, significance: f64, toward: Toward) -> Step<f64> {
let quotient = site.divide(number, significance)?;
// Counted from the quotient rounded to 15 significant digits, as Excel counts (dc-j9k; see
// ceiling_floor_family::excel_quotient).
let quotient = if quotient.is_finite() && quotient != 0.0 {
crate::ported::ceiling_floor_family::excel_quotient(number, significance)
} else {
quotient
};
let count = match toward {
Toward::Up => decimal::ceil(quotient),
Toward::Down => decimal::floor(quotient),
};
site.multiply(significance, count)
}
Used by 3 functions.
quotient · 20 lines · calc-func/src/catalogue/rounding.rs on GitHub/// QUOTIENT: the integer part of the binary64 quotient numerator / denominator, toward zero. A
/// zero denominator is #DIV/0!.
fn quotient(
arguments: &[CalcValue],
services: &mut EvalServices<'_>,
) -> Result<CalcValue, FunctionFault> {
numeric(QUOTIENT, arguments, services, |site, n| {
let (numerator, denominator) = (n[0], n[1]);
if denominator == 0.0 {
return Err(Exit::Error(WorksheetErrorCode::Div0));
}
if numerator == 0.0 {
return Ok(0.0);
}
// The binary64 quotient, toward zero (dc-bd5: QUOTIENT(0.3,0.1) is 2 in Excel
// 16.0.20430, the quotient being 2.9999999999999996).
let quotient = site.divide(numerator, denominator)?;
Ok(decimal::truncate(quotient))
})
}
Used by 6 functions.
odd · 9 lines · calc-func/src/catalogue/rounding.rs on GitHub/// ODD: rounded away from zero to an odd integer; an odd integer is unchanged, and 0 gives 1.
fn odd(
arguments: &[CalcValue],
services: &mut EvalServices<'_>,
) -> Result<CalcValue, FunctionFault> {
numeric(ODD, arguments, services, |site, n| {
to_parity(site, n[0], true)
})
}
Used by 2 functions.
to_parity · 18 lines · calc-func/src/catalogue/rounding.rs on GitHub/// The integer of the wanted parity at or beyond |number|, with the number's sign. The integer
/// above |number| is exact; stepping on to the next integer is an addition at an arithmetic site,
/// which from 2^53 on, where every binary64 is even, rounds the odd integer to a binary64.
fn to_parity(site: &Site<'_>, number: f64, odd: bool) -> Step<f64> {
if number == 0.0 {
return Ok(if odd { 1.0 } else { 0.0 });
}
let magnitude = decimal::ceil(number.abs());
// Halving an integer of at least 1 is exact.
let half = magnitude * 0.5;
let is_even = decimal::truncate(half) == half;
let rounded = if is_even == odd {
site.add(magnitude, 1.0)?
} else {
magnitude
};
Ok(if number < 0.0 { -rounded } else { rounded })
}
Used by 2 functions.