Shared Rust kernels
ported/complex_family
All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.
parse_ascii_decimal_15 · 62 lines · calc-func/src/ported/complex_family.rs on GitHub/// Decimal text of digits with at most one point and an optional exponent, read to its 15 leading
/// significant digits and converted to the nearest binary64; `None` outside that form or with a
/// scale outside -308..308 (OxFunc coercion.rs parse_ascii_decimal_15).
fn parse_ascii_decimal_15(text: &str) -> Option<f64> {
let (mantissa, exponent) = match text.find(['e', 'E']) {
Some(index) => {
let mut exponent = &text[index + 1..];
let negative = exponent.starts_with('-');
if exponent.starts_with(['+', '-']) {
exponent = &exponent[1..];
}
if exponent.is_empty() || !exponent.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let magnitude = exponent.bytes().fold(0_i64, |n, b| {
n.saturating_mul(10).saturating_add((b - b'0') as i64)
});
(&text[..index], if negative { -magnitude } else { magnitude })
}
None => (text, 0),
};
let mut significand = 0_u64;
let mut significant_digits = 0_i64;
let mut fractional_digits = 0_i64;
let mut after_point = false;
let mut any_digit = false;
for byte in mantissa.bytes() {
if byte == b'.' && !after_point {
after_point = true;
continue;
}
if !byte.is_ascii_digit() {
return None;
}
any_digit = true;
if after_point {
fractional_digits += 1;
}
if byte != b'0' || significant_digits > 0 {
if significant_digits < 15 {
significand = significand * 10 + (byte - b'0') as u64;
}
significant_digits += 1;
}
}
if !any_digit {
return None;
}
let position = exponent
.saturating_sub(fractional_digits)
.saturating_add(significant_digits);
if !(-308..=308).contains(&position) {
return None;
}
if significand == 0 {
return Some(0.0);
}
let scale = exponent
.saturating_sub(fractional_digits)
.saturating_add((significant_digits - 15).max(0));
format!("{significand}e{scale}").parse().ok()
}
Used by 25 functions.
ParsedComplex::new · 3 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) const fn new(re: f64, im: f64, suffix: Option<char>) -> Self {
Self { re, im, suffix }
}
Used by 26 functions.
ParsedComplex::abs · 3 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn abs(self) -> f64 {
crt_hypot(self.re, self.im)
}
Used by 22 functions.
normalize_zero · 3 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn normalize_zero(n: f64) -> f64 {
if n == 0.0 { 0.0 } else { n }
}
Used by 26 functions.
number_to_excel_text · 63 lines · calc-func/src/ported/complex_family.rs on GitHub/// Excel's complex coefficient rendering, observed on build 20430 through Value2 inputs:
/// 15 significant decimal digits using the binary scaling model in `complex_decimal`, fixed
/// notation while the unsigned result fits in 21 characters, otherwise scientific notation.
/// No near-integer snapping is applied. See the retained W111 captures and failed hypotheses.
pub(crate) fn number_to_excel_text(n: f64) -> Result<String, WorksheetErrorCode> {
if n == 0.0 {
return Ok("0".to_string());
}
// The exact maximum finite magnitude is rejected; its immediate finite neighbors are
// admitted. This is an observed admission boundary, not an inferred internal mechanism.
if !n.is_finite() || n.abs() == f64::MAX {
return Err(WorksheetErrorCode::Num);
}
let sign = if n < 0.0 { "-" } else { "" };
let (mut significand, mut scale) = complex_decimal::decimal_pair(n);
if significand == 1_000_000_000_000_000 {
significand /= 10;
scale += 1;
}
let decimal = format!("{significand}e{scale}");
let rounded: f64 = decimal
.parse()
.expect("finite-input decimal representation");
// Rounding MIN_POSITIVE to 15 digits crosses below the normal range. Excel publishes
// signed text zero for that coefficient without deleting its component from the result.
if rounded < f64::MIN_POSITIVE {
return Ok(format!("{sign}0"));
}
// Do not reject a decimal that rounds above binary64's range. For MAX-1 ULP, Excel
// legitimately publishes 1.79769313486232E+308 even though reparsing it overflows.
let digits = significand.to_string();
let exponent = scale + digits.len() as i32 - 1;
let decimal_pos = exponent + 1;
let mut out = if decimal_pos <= 0 {
format!("0.{}{}", "0".repeat((-decimal_pos) as usize), digits)
} else if decimal_pos as usize >= digits.len() {
format!(
"{}{}",
digits,
"0".repeat(decimal_pos as usize - digits.len())
)
} else {
let split = decimal_pos as usize;
format!("{}.{}", &digits[..split], &digits[split..])
};
if let Some(dot) = out.find('.') {
while out.ends_with('0') {
out.pop();
}
if out.len() == dot + 1 {
out.pop();
}
}
if out.len() > 21 {
let mantissa = format!("{}.{}", &digits[..1], &digits[1..]);
let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
out = format!("{mantissa}E{exponent:+03}");
}
Ok(format!("{sign}{out}"))
}
Used by 22 functions.
format_complex_text · 29 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn format_complex_text(
value: ParsedComplex,
suffix: char,
) -> Result<String, WorksheetErrorCode> {
let re = normalize_zero(value.re);
let im = normalize_zero(value.im);
if im == 0.0 {
return number_to_excel_text(re);
}
let coeff = if im.abs() == 1.0 {
String::new()
} else {
number_to_excel_text(im.abs())?
};
if re == 0.0 {
if im < 0.0 {
Ok(format!("-{}{suffix}", coeff))
} else {
Ok(format!("{}{suffix}", coeff))
}
} else {
let sign = if im < 0.0 { '-' } else { '+' };
Ok(format!(
"{}{sign}{}{suffix}",
number_to_excel_text(re)?,
coeff
))
}
}
Used by 22 functions.
parse_real_number · 12 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn parse_real_number(text: &str) -> Option<f64> {
let trimmed = text.trim_matches(' ');
if trimmed.is_empty() {
return None;
}
let negative = trimmed.starts_with('-');
let unsigned = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed).trim_matches(' ');
let parsed = parse_ascii_decimal_15(unsigned)?;
// Complex coefficient strings have their own grammar. Only decimal lexeme
// conversion is shared with numeric argument coercion; decorations are not.
Some(if parsed < f64::MIN_POSITIVE { 0.0 } else if negative { -parsed } else { parsed })
}
Used by 25 functions.
find_real_imag_split · 12 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn find_real_imag_split(body: &str) -> Option<usize> {
let bytes = body.as_bytes();
let mut out = None;
for i in 1..bytes.len() {
let ch = bytes[i] as char;
let prev = bytes[i - 1] as char;
if (ch == '+' || ch == '-') && prev != 'e' && prev != 'E' {
out = Some(i);
}
}
out
}
Used by 25 functions.
parse_imag_coeff · 10 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn parse_imag_coeff(text: &str) -> Option<f64> {
let trimmed = text.trim_matches(' ');
if trimmed.is_empty() || trimmed == "+" {
Some(1.0)
} else if trimmed == "-" {
Some(-1.0)
} else {
parse_real_number(trimmed)
}
}
Used by 25 functions.
parse_complex_text · 20 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn parse_complex_text(raw: &str) -> Option<ParsedComplex> {
let text = raw.trim_matches(' ');
if text.is_empty() {
return Some(ParsedComplex::new(0.0, 0.0, None));
}
if let Some(last) = text.chars().last() {
if last == 'i' || last == 'j' {
let body = &text[..text.len() - last.len_utf8()];
if let Some(split) = find_real_imag_split(body) {
return Some(ParsedComplex::new(
parse_real_number(&body[..split])?,
parse_imag_coeff(&body[split..])?,
Some(last),
));
}
return Some(ParsedComplex::new(0.0, parse_imag_coeff(body)?, Some(last)));
}
}
parse_real_number(text).map(|re| ParsedComplex::new(re, 0.0, None))
}
Used by 25 functions.
suffix_from_operands · 13 lines · calc-func/src/ported/complex_family.rs on GitHubpub(crate) fn suffix_from_operands(values: &[ParsedComplex]) -> Result<char, WorksheetErrorCode> {
let mut suffix = None;
for value in values {
if let Some(ch) = value.suffix {
match suffix {
None => suffix = Some(ch),
Some(current) if current == ch => {}
Some(_) => return Err(WorksheetErrorCode::Value),
}
}
}
Ok(suffix.unwrap_or('i'))
}
Used by 21 functions.