use crate::error::{NonFinite, ValueError};
use crate::numeric;
use std::error::Error as StdError;
use std::fmt;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum SumError {
#[non_exhaustive]
Input {
index: usize,
error: ValueError,
},
#[non_exhaustive]
Arithmetic {
index: Option<usize>,
error: ValueError,
},
}
impl SumError {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Input { .. } => "sum.input",
Self::Arithmetic { .. } => "sum.arithmetic",
}
}
}
impl fmt::Display for SumError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Input { index, error } => write!(f, "summation element {index}: {error}"),
Self::Arithmetic {
index: Some(index),
error,
} => {
write!(f, "summation at element {index}: {error}")
}
Self::Arithmetic { index: None, error } => write!(f, "summation result: {error}"),
}
}
}
impl StdError for SumError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Input { error, .. } | Self::Arithmetic { error, .. } => Some(error),
}
}
}
pub fn compensated_sum(values: &[f64]) -> Result<f64, SumError> {
for (index, &value) in values.iter().enumerate() {
numeric::finite("values", value).map_err(|error| SumError::Input { index, error })?;
}
let mut sum = 0.0_f64;
let mut correction = 0.0_f64;
for (index, &value) in values.iter().enumerate() {
let next = checked("partial sum", sum + value, Some(index))?;
let roundoff = if sum.abs() >= value.abs() {
(sum - next) + value
} else {
(value - next) + sum
};
correction = checked("summation correction", correction + roundoff, Some(index))?;
sum = next;
}
checked("compensated sum", sum + correction, None).map(canonical_zero)
}
pub fn exact_sum(values: &[f64]) -> Result<f64, SumError> {
let mut positive = [0_u64; LIMBS];
let mut negative = [0_u64; LIMBS];
for (index, &value) in values.iter().enumerate() {
numeric::finite("values", value).map_err(|error| SumError::Input { index, error })?;
let magnitude = if value.is_sign_negative() {
&mut negative
} else {
&mut positive
};
add_float(magnitude, value.to_bits());
}
let ordering = positive.iter().rev().cmp(negative.iter().rev());
let (magnitude, subtrahend, negative_result) = match ordering {
std::cmp::Ordering::Equal => return Ok(0.0),
std::cmp::Ordering::Greater => (&mut positive, &negative, false),
std::cmp::Ordering::Less => (&mut negative, &positive, true),
};
subtract(magnitude, subtrahend);
round_binary64(magnitude, negative_result)
}
const LIMBS: usize = (2098 + usize::BITS as usize).div_ceil(64);
const FRACTION_MASK: u64 = (1_u64 << 52) - 1;
#[expect(
clippy::cast_possible_truncation,
reason = "exponent is at most 2046; low-word conversion intentionally extracts 64 bits of a u128"
)]
fn add_float(words: &mut [u64; LIMBS], bits: u64) {
let exponent = ((bits >> 52) & 0x7ff) as usize;
let significand = (bits & FRACTION_MASK) | if exponent == 0 { 0 } else { 1 << 52 };
let shift = exponent.saturating_sub(1);
let position = shift / 64;
let shifted = u128::from(significand) << (shift % 64);
add_word(words, position, shifted as u64);
add_word(words, position + 1, (shifted >> 64) as u64);
}
fn add_word(words: &mut [u64; LIMBS], mut position: usize, mut addend: u64) {
while addend != 0 {
let (next, carry) = words[position].overflowing_add(addend);
words[position] = next;
addend = u64::from(carry);
position += 1;
}
}
fn subtract(words: &mut [u64; LIMBS], smaller: &[u64; LIMBS]) {
let mut borrow = false;
for (word, &other) in words.iter_mut().zip(smaller) {
let (difference, first_borrow) = word.overflowing_sub(other);
let (difference, second_borrow) = difference.overflowing_sub(u64::from(borrow));
*word = difference;
borrow = first_borrow || second_borrow;
}
}
fn round_binary64(words: &[u64; LIMBS], negative: bool) -> Result<f64, SumError> {
let Some(position) = words.iter().rposition(|&word| word != 0) else {
return Ok(0.0);
};
let highest = position * 64 + (63 - words[position].leading_zeros() as usize);
let sign = u64::from(negative) << 63;
if highest < 52 {
return Ok(f64::from_bits(sign | words[0]));
}
if highest > 2097 {
return Err(final_overflow(negative));
}
let shift = highest - 52;
let position = shift / 64;
let offset = shift % 64;
let mut significand = words[position] >> offset;
if offset != 0 {
significand |= words[position + 1] << (64 - offset);
}
if shift != 0 {
let round_bit = bit(words, shift - 1);
let sticky = any_below(words, shift - 1);
if round_bit && (sticky || significand & 1 != 0) {
significand += 1;
}
}
let mut exponent = highest - 51;
if significand == 1 << 53 {
significand >>= 1;
exponent += 1;
}
if exponent >= 2047 {
return Err(final_overflow(negative));
}
Ok(f64::from_bits(
sign | (exponent as u64) << 52 | (significand & FRACTION_MASK),
))
}
fn bit(words: &[u64; LIMBS], position: usize) -> bool {
words[position / 64] & (1_u64 << (position % 64)) != 0
}
fn any_below(words: &[u64; LIMBS], exclusive: usize) -> bool {
let limb = exclusive / 64;
words[..limb].iter().any(|&word| word != 0)
|| words[limb] & ((1_u64 << (exclusive % 64)) - 1) != 0
}
fn final_overflow(negative: bool) -> SumError {
SumError::Arithmetic {
index: None,
error: ValueError::ResultNotFinite {
quantity: "exact sum",
value: if negative {
NonFinite::NegativeInfinity
} else {
NonFinite::PositiveInfinity
},
},
}
}
fn checked(quantity: &'static str, value: f64, index: Option<usize>) -> Result<f64, SumError> {
numeric::finite_result(quantity, value).map_err(|error| SumError::Arithmetic { index, error })
}
fn canonical_zero(value: f64) -> f64 {
if value.to_bits() << 1 == 0 {
0.0
} else {
value
}
}