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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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.