Shared Rust kernels

format

All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.

measured_host · 4 lines · calc-func/src/format/mod.rs on GitHub
/// The host profiles measured by the scalar formatting questions.
fn measured_host(profile: &FormatProfile) -> bool {
    matches!(profile.id, LocaleId::EnZa | LocaleId::CurrentExcelHost)
}

Used by 18 functions.

currency_literal · 9 lines · calc-func/src/format/mod.rs on GitHub
/// A literal atom exactly naming the measured currency ignores ASCII case.
/// Quoted longer words stay literal; unquoted characters arrive one at a time.
fn currency_literal<'a>(profile: &FormatProfile, text: &'a str) -> &'a str {
    if measured_host(profile) && profile.currency_symbol == "R" && text.eq_ignore_ascii_case("R") {
        profile.currency_symbol
    } else {
        text
    }
}

Used by 18 functions.

digits · 8 lines · calc-func/src/format/mod.rs on GitHub
/// The magnitude of `x` rounded to 15 significant digits and then to `places` decimal places
/// (negative places round to tens, hundreds and so on), a half away from zero, as its integer
/// digits (no leading zero; empty for zero) and exactly `max(places, 0)` decimal digits.
pub(crate) fn digits(x: f64, places: i32) -> (String, String) {
    let first = fifteen(x.abs());
    let rounded = first.round_at(-places, Direction::Nearest).unwrap_or(first);
    split(&rounded, places.max(0) as usize)
}

Used by 19 functions.

fifteen · 5 lines · calc-func/src/format/mod.rs on GitHub
/// FIXED/TEXT and concatenation share their first15-digit conversion. Reuse
/// its existing near-tie behavior, including the open dc-b1l.25 departures.
fn fifteen(x: f64) -> ExactDecimal {
    crate::number_text::decimal(x)
}

Used by 19 functions.

split · 26 lines · calc-func/src/format/mod.rs on GitHub
/// A decimal's integer digits (without leading zeros) and `places` decimal digits.
fn split(decimal: &ExactDecimal, places: usize) -> (String, String) {
    if decimal.coefficient.is_zero() {
        return (String::new(), "0".repeat(places));
    }
    let digits = decimal.coefficient.digits();
    let exponent = decimal.exponent;
    let (whole, fraction) = if exponent >= 0 {
        (format!("{digits}{}", "0".repeat(exponent as usize)), String::new())
    } else {
        let point = digits.len() as i64 + exponent as i64;
        if point <= 0 {
            (String::new(), format!("{}{digits}", "0".repeat((-point) as usize)))
        } else {
            let point = point as usize;
            (digits[..point].to_owned(), digits[point..].to_owned())
        }
    };
    let whole = whole.trim_start_matches('0').to_owned();
    let mut fraction = fraction;
    fraction.truncate(places);
    while fraction.len() < places {
        fraction.push('0');
    }
    (whole, fraction)
}

Used by 19 functions.

grouped · 19 lines · calc-func/src/format/mod.rs on GitHub
fn grouped(whole: &str, separator: &str) -> String {
    if whole.len() <= 3 {
        return whole.to_owned();
    }
    let mut out = String::new();
    let first = whole.len() % 3;
    if first > 0 {
        out.push_str(&whole[..first]);
    }
    let mut at = first;
    while at < whole.len() {
        if !out.is_empty() {
            out.push_str(separator);
        }
        out.push_str(&whole[at..at + 3]);
        at += 3;
    }
    out
}

Used by 18 functions.

fixed · 27 lines · calc-func/src/format/mod.rs on GitHub
/// FIXED: the number rounded to `places`, grouped unless `no_commas`.
/// A missing profile refuses only when the output would contain a locale separator.
pub(crate) fn fixed(profile: Option<&FormatProfile>, value: f64, places: i32, no_commas: bool) -> Rendered {
    let decimal_separator = if places > 0 {
        Some(profile.ok_or(Failure::NotModelled)?.decimal_separator)
    } else {
        None
    };
    let (whole, fraction) = digits(value, places);
    let negative = value < 0.0 && (whole.chars().chain(fraction.chars()).any(|c| c != '0'));
    let whole = if whole.is_empty() { "0".to_owned() } else { whole };
    let whole = if no_commas || whole.len() <= 3 {
        whole
    } else {
        grouped(&whole, profile.ok_or(Failure::NotModelled)?.thousands_separator)
    };
    let mut out = String::new();
    if negative {
        out.push('-');
    }
    out.push_str(&whole);
    if let Some(separator) = decimal_separator {
        out.push_str(separator);
        out.push_str(&fraction);
    }
    Ok(out)
}

Used by 2 functions.

general · 67 lines · calc-func/src/format/mod.rs on GitHub
/// The General format's text: at most 11 characters besides the sign, as a cell of Excel's
/// standard column width shows a number. Public as [`crate::general_format`] for an app that
/// shows a value as a cell would.
pub fn general(profile: &FormatProfile, value: f64) -> String {
    const WIDTH: i64 = 11;
    if !value.is_finite() {
        if !value.is_nan() {
            return "#NUM!".to_owned();
        }
        // Excel's retained exponent-field-2047 readings show two significant digits,
        // without a minus sign, even when the stored encoding has its sign bit set.
        let exact = ExactDecimal::binary(value);
        let exponent = exact.magnitude();
        let (mantissa, carry) = significant(&exact, 2);
        let (head, tail) = mantissa.split_at(1);
        let tail = tail.trim_end_matches('0');
        let point = if tail.is_empty() { "" } else { profile.decimal_separator };
        return format!("{head}{point}{tail}E+{}", i64::from(exponent) + carry);
    }
    if value == 0.0 || value.is_subnormal() {
        return "0".to_owned();
    }
    let sign = if value < 0.0 { "-" } else { "" };
    let magnitude = value.abs();
    let first = fifteen(magnitude);
    let length = first.coefficient.digits().len() as i64;
    // The value is 0.d1d2... * 10^point.
    let point = length + first.exponent as i64;
    let fixed_fits = point <= WIDTH && point > -9;
    if fixed_fits {
        let places = if point >= WIDTH {
            0
        } else {
            WIDTH - point.max(1) - 1
        };
        let (whole, fraction) = digits(magnitude, places as i32);
        let fraction = fraction.trim_end_matches('0');
        let whole = if whole.is_empty() { "0".to_owned() } else { whole };
        if whole.len() as i64 <= WIDTH && (whole != "0" || !fraction.is_empty()) {
            let mut out = format!("{sign}{whole}");
            if !fraction.is_empty() {
                out.push_str(profile.decimal_separator);
                out.push_str(fraction);
            }
            return out;
        }
    }
    // Scientific: d.dddddE+xx, the mantissa's places filling what the exponent leaves.
    let mut exponent = point - 1;
    let exponent_width = if exponent.abs() >= 100 { 3 } else { 2 };
    let places = WIDTH - 2 - 2 - exponent_width;
    let (mantissa, carry) = significant(&first, places as usize + 1);
    exponent += carry;
    let (head, tail) = mantissa.split_at(1);
    let tail = tail.trim_end_matches('0');
    let mut out = format!("{sign}{head}");
    if !tail.is_empty() {
        out.push_str(profile.decimal_separator);
        out.push_str(tail);
    }
    out.push_str(&format!(
        "E{}{:02}",
        if exponent < 0 { '-' } else { '+' },
        exponent.abs()
    ));
    out
}

Used by 18 functions.

significant · 18 lines · calc-func/src/format/mod.rs on GitHub
/// The first `count` significant digits of a non-zero decimal, rounded a half away from zero, and
/// 1 when the rounding carried into a new leading digit (the digits are then 1 and zeros).
fn significant(decimal: &ExactDecimal, count: usize) -> (String, i64) {
    let digits = decimal.coefficient.digits();
    if digits.len() <= count {
        return (format!("{digits}{}", "0".repeat(count - digits.len())), 0);
    }
    let dropped = (digits.len() - count) as i32;
    let rounded = decimal
        .round_at(decimal.exponent + dropped, Direction::Nearest)
        .expect("digits below the position");
    let out = rounded.coefficient.digits();
    if out.len() > count {
        (out[..count].to_owned(), 1)
    } else {
        (out, 0)
    }
}

Used by 18 functions.

sections · 27 lines · calc-func/src/format/mod.rs on GitHub
/// The sections of a code, split at `;` outside quotes and escapes.
fn sections(code: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut current = String::new();
    let (mut quoted, mut escaped) = (false, false);
    for ch in code.chars() {
        if escaped {
            current.push(ch);
            escaped = false;
            continue;
        }
        match ch {
            '\\' => {
                current.push(ch);
                escaped = true;
            }
            '"' => {
                current.push(ch);
                quoted = !quoted;
            }
            ';' if !quoted => out.push(std::mem::take(&mut current)),
            _ => current.push(ch),
        }
    }
    out.push(current);
    out
}

Used by 18 functions.

bracket · 6 lines · calc-func/src/format/mod.rs on GitHub
/// A bracketed token at the start of `text`: its inside and the rest.
fn bracket(text: &str) -> Option<(&str, &str)> {
    let rest = text.strip_prefix('[')?;
    let close = rest.find(']')?;
    Some((&rest[..close], &rest[close + 1..]))
}

Used by 18 functions.

is_condition · 3 lines · calc-func/src/format/mod.rs on GitHub
fn is_condition(token: &str) -> bool {
    token.starts_with(['<', '>', '='])
}

Used by 18 functions.

is_elapsed · 6 lines · calc-func/src/format/mod.rs on GitHub
fn is_elapsed(token: &str) -> bool {
    let lower = token.to_ascii_lowercase();
    !lower.is_empty()
        && lower.chars().all(|c| matches!(c, 'h' | 'm' | 's'))
        && lower.chars().all(|c| Some(c) == lower.chars().next())
}

Used by 18 functions.

condition_holds · 14 lines · calc-func/src/format/mod.rs on GitHub
fn condition_holds(token: &str, value: f64) -> Option<bool> {
    let (op, rest) = ["<=", ">=", "<>", "<", ">", "="]
        .iter()
        .find_map(|op| token.strip_prefix(op).map(|rest| (*op, rest)))?;
    let threshold: f64 = rest.trim().parse().ok()?;
    Some(match op {
        "<" => value < threshold,
        "<=" => value <= threshold,
        ">" => value > threshold,
        ">=" => value >= threshold,
        "=" => value == threshold,
        _ => value != threshold,
    })
}

Used by 18 functions.

condition_of · 11 lines · calc-func/src/format/mod.rs on GitHub
/// The leading condition of a section, if it has one.
fn condition_of(section: &str) -> Option<String> {
    let mut rest = section;
    while let Some((token, after)) = bracket(rest) {
        if is_condition(token) {
            return Some(token.to_owned());
        }
        rest = after;
    }
    None
}

Used by 18 functions.

strip_tokens · 62 lines · calc-func/src/format/mod.rs on GitHub
/// A section with its bracketed tokens resolved: colors dropped, locale recorded for names, a currency tag
/// `[$sym-xxxx]` written as its symbol (quoted), conditions dropped, elapsed-time tokens kept.
fn strip_tokens(section: &str) -> Result<(String, Option<crate::locale::LocaleId>, bool), Failure> {
    let mut out = String::new();
    let mut name_locale = None;
    let mut locale_declared = false;
    let mut chars = section.chars().peekable();
    let (mut quoted, mut escaped) = (false, false);
    while let Some(ch) = chars.next() {
        if escaped {
            out.push(ch);
            escaped = false;
            continue;
        }
        match ch {
            '\\' => {
                out.push(ch);
                escaped = true;
            }
            '"' => {
                out.push(ch);
                quoted = !quoted;
            }
            '[' if !quoted => {
                let mut token = String::new();
                for next in chars.by_ref() {
                    if next == ']' {
                        break;
                    }
                    token.push(next);
                }
                if is_elapsed(&token) {
                    out.push('[');
                    out.push_str(&token);
                    out.push(']');
                } else if let Some(body) = token.strip_prefix('$') {
                    let (symbol, locale) = body.split_once('-').unwrap_or((body, ""));
                    if !locale.is_empty() {
                        locale_declared = true;
                        use crate::locale::LocaleId;
                        name_locale = u16::from_str_radix(locale, 16).ok()
                            .and_then(LocaleId::from_excel_lcid)
                            .or_else(|| LocaleId::from_bcp47_language_tag(locale));
                    }
                    if !symbol.is_empty() {
                        out.push('"');
                        out.push_str(symbol);
                        out.push('"');
                    }
                } else if is_condition(&token)
                    || token.chars().all(|c| c.is_ascii_alphanumeric())
                {
                    // A color ([Red], [Color10]) or a condition.
                } else {
                    return Err(Failure::NotModelled);
                }
            }
            _ => out.push(ch),
        }
    }
    Ok((out, name_locale, locale_declared))
}

Used by 18 functions.

choose · 42 lines · calc-func/src/format/mod.rs on GitHub
/// The section a number is rendered with, and whether that section shows the number's sign
/// itself (so its magnitude is rendered).
fn choose(sections: &[String], value: f64) -> Result<(String, bool), Failure> {
    let numeric: Vec<&String> = sections.iter().take(3).collect();
    if numeric.iter().any(|s| condition_of(s).is_some()) {
        let mut fallback = None;
        for section in &numeric {
            match condition_of(section) {
                Some(token) => {
                    if condition_holds(&token, value).ok_or(Failure::NotModelled)? {
                        return Ok(((*section).clone(), false));
                    }
                }
                None => {
                    if fallback.is_none() {
                        fallback = Some((*section).clone());
                    }
                }
            }
        }
        return fallback.map(|s| (s, false)).ok_or(Failure::NotModelled);
    }
    Ok(match numeric.len() {
        1 => (numeric[0].clone(), false),
        2 => {
            if value < 0.0 {
                (numeric[1].clone(), true)
            } else {
                (numeric[0].clone(), false)
            }
        }
        _ => {
            if value > 0.0 {
                (numeric[0].clone(), false)
            } else if value < 0.0 {
                (numeric[1].clone(), true)
            } else {
                (numeric[2].clone(), false)
            }
        }
    })
}

Used by 18 functions.

items · 84 lines · calc-func/src/format/mod.rs on GitHub
/// A section's items, with quoted text, escapes, `_x` (a space) and `*x` (nothing) resolved.
fn items(section: &str, profile: &FormatProfile) -> Result<Vec<Item>, Failure> {
    let decimal = profile.decimal_separator;
    let group = profile.thousands_separator;
    let chars: Vec<char> = section.chars().collect();
    let mut out = Vec::new();
    let mut at = 0;
    let literal = |out: &mut Vec<Item>, text: &str| {
        let text = currency_literal(profile, text);
        match out.last_mut() {
            Some(Item::Literal(existing)) => existing.push_str(text),
            _ => out.push(Item::Literal(text.to_owned())),
        }
    };
    while at < chars.len() {
        let ch = chars[at];
        let rest: String = chars[at..].iter().collect();
        match ch {
            '"' => {
                let close = chars[at + 1..]
                    .iter()
                    .position(|c| *c == '"')
                    .ok_or(Failure::NotModelled)?;
                let text: String = chars[at + 1..at + 1 + close].iter().collect();
                literal(&mut out, &text);
                at += close + 2;
            }
            '\\' => {
                let next = chars.get(at + 1).ok_or(Failure::NotModelled)?;
                let text = next.to_string();
                match out.last_mut() {
                    Some(Item::Literal(existing)) => existing.push_str(&text),
                    _ => out.push(Item::Literal(text)),
                }
                at += 2;
            }
            '_' => {
                literal(&mut out, " ");
                at += 2;
            }
            '*' => at += 2,
            '0' | '#' | '?' => {
                out.push(Item::Digit(ch));
                at += 1;
            }
            '%' => {
                out.push(Item::Percent);
                at += 1;
            }
            '@' => {
                out.push(Item::TextPlaceholder);
                at += 1;
            }
            'E' | 'e' if matches!(chars.get(at + 1), Some('+' | '-')) => {
                out.push(Item::Exponent(chars[at + 1] == '+'));
                at += 2;
            }
            _ if rest.starts_with(decimal) => {
                out.push(Item::Point);
                at += decimal.chars().count();
            }
            _ if !group.is_empty() && rest.starts_with(group) => {
                out.push(Item::Group);
                at += group.chars().count();
            }
            '/' => return Err(Failure::NotModelled),
            'E' if measured_host(profile) => return Err(Failure::Value),
            c if c.is_ascii_alphabetic() => {
                // A letter that is a token in some other format (dates, eras, A/P) is not modelled.
                if "ymdhsbegYMDHSBEG".contains(c)
                    || (matches!(c, 'a' | 'A') && !measured_host(profile)) {
                    return Err(Failure::NotModelled);
                }
                literal(&mut out, &c.to_string());
                at += 1;
            }
            c => {
                literal(&mut out, &c.to_string());
                at += 1;
            }
        }
    }
    Ok(out)
}

Used by 18 functions.

numeric · 132 lines · calc-func/src/format/mod.rs on GitHub
/// Renders a number with a numeric section's items.
fn numeric(
    profile: &FormatProfile,
    value: f64,
    items: &[Item],
    show_minus: bool,
) -> Rendered {
    let first = items
        .iter()
        .position(|i| matches!(i, Item::Digit(_)))
        .ok_or(Failure::NotModelled)?;
    let last = items
        .iter()
        .rposition(|i| matches!(i, Item::Digit(_)))
        .expect("a digit placeholder");
    let exponent_at = items.iter().position(|i| matches!(i, Item::Exponent(_)));
    let mantissa_end = exponent_at.unwrap_or(last + 1);
    let point_at = items[..mantissa_end]
        .iter()
        .position(|i| *i == Item::Point);
    // Group separators right after the last mantissa placeholder divide by 1000 each.
    let mut scale_groups = 0;
    let mut cursor = items[..mantissa_end]
        .iter()
        .rposition(|i| matches!(i, Item::Digit(_)))
        .map(|p| p + 1)
        .unwrap_or(mantissa_end);
    while items.get(cursor) == Some(&Item::Group) {
        scale_groups += 1;
        cursor += 1;
    }
    let scale_end = cursor;
    let percents = items.iter().filter(|i| **i == Item::Percent).count() as i32;
    let mantissa_last = items[..mantissa_end]
        .iter()
        .rposition(|i| matches!(i, Item::Digit(_)))
        .unwrap_or(first);
    let integer_end = point_at.unwrap_or(mantissa_last + 1);
    // A group separator beside a literal in the integer part (dc-b1l, Excel 16.0.20430 on
    // 2026-10-25, the catalogue oracle's dcuar_text rows): one that follows a literal and precedes
    // a placeholder is written as the separator's text, a literal (TEXT(5415485.8,"##- 0") is
    // 541548- 6); one that follows a placeholder and precedes a literal is dropped and groups
    // nothing (TEXT(843124.584,"0# ,0") is 84312,5). One between two placeholders groups.
    let separator = Item::Literal(profile.thousands_separator.to_owned());
    let integer_items: Vec<Item> = items[first..integer_end.max(first)]
        .iter()
        .enumerate()
        .filter_map(|(at, item)| {
            let all = &items[first..integer_end.max(first)];
            if *item != Item::Group {
                return Some(item.clone());
            }
            let before = at.checked_sub(1).map(|p| &all[p]);
            let after = all.get(at + 1);
            match (before, after) {
                (Some(Item::Literal(_)), Some(Item::Digit(_))) => Some(separator.clone()),
                (Some(Item::Digit(_)), Some(Item::Literal(_))) => None,
                _ => Some(Item::Group),
            }
        })
        .collect();
    let integer: Vec<&Item> = integer_items.iter().collect();
    let mut fraction_literals: Vec<(usize, String)> = Vec::new();
    let fraction: Vec<char> = match point_at {
        Some(p) => {
            let mut places = Vec::new();
            for item in &items[p + 1..mantissa_end] {
                match item {
                    Item::Digit(c) => places.push(*c),
                    Item::Group => {}
                    Item::Literal(text) => fraction_literals.push((places.len(), text.clone())),
                    _ => return Err(Failure::NotModelled),
                }
            }
            places
        }
        None => Vec::new(),
    };
    // A literal among the decimal places stands after as many of them as precede it in the code
    // (Excel 16.0.20430, dc-uar: TEXT(1234567.891,"# ##0.0,0") is 1 234 567.8,9); modelled for
    // `0` places only.
    if !fraction_literals.is_empty() && fraction.iter().any(|c| *c != '0') {
        return Err(Failure::NotModelled);
    }
    let grouping = integer
        .iter()
        .enumerate()
        .any(|(at, i)| **i == Item::Group && integer[at + 1..].iter().any(|j| matches!(j, Item::Digit(_))));
    let prefix: String = items[..first]
        .iter()
        .map(|i| literal_text(i, profile))
        .collect::<Result<String, Failure>>()?;
    let suffix_from = match exponent_at {
        Some(_) => last + 1,
        None => scale_end.max(last + 1),
    };
    let suffix: String = items[suffix_from..]
        .iter()
        .map(|i| literal_text(i, profile))
        .collect::<Result<String, Failure>>()?;

    let mut scaled = value.abs() * 100f64.powi(percents);
    if scale_groups > 0 {
        scaled /= 1000f64.powi(scale_groups);
    }
    let places = fraction.len() as i32;

    let (body, nonzero) = match exponent_at {
        Some(e) => scientific(profile, scaled, &integer, &fraction, items, e)?,
        None => {
            let (whole, frac) = digits(scaled, places);
            let nonzero = whole.chars().chain(frac.chars()).any(|c| c != '0');
            let whole_text = integer_part(profile, &whole, &integer, grouping)?;
            let mut frac_text = fraction_part(&frac, &fraction);
            for (after, text) in fraction_literals.iter().rev() {
                frac_text.insert_str((*after).min(frac_text.len()), text);
            }
            let mut body = whole_text;
            if point_at.is_some() {
                body.push_str(profile.decimal_separator);
                body.push_str(&frac_text);
            }
            (body, nonzero)
        }
    };
    let minus = if show_minus && value < 0.0 && nonzero {
        "-"
    } else {
        ""
    };
    Ok(format!("{minus}{prefix}{body}{suffix}"))
}

Used by 18 functions.

literal_text · 9 lines · calc-func/src/format/mod.rs on GitHub
fn literal_text(item: &Item, profile: &FormatProfile) -> Result<String, Failure> {
    Ok(match item {
        Item::Literal(text) => text.clone(),
        Item::Percent => "%".to_owned(),
        Item::Group => profile.thousands_separator.to_owned(),
        Item::Point => profile.decimal_separator.to_owned(),
        Item::Digit(_) | Item::Exponent(_) | Item::TextPlaceholder => return Err(Failure::NotModelled),
    })
}

Used by 18 functions.

integer_part · 122 lines · calc-func/src/format/mod.rs on GitHub
/// The integer part: digits fill the placeholders from the right, the leftmost placeholder takes
/// any digits left over, `0` writes a zero and `?` a space where no digit is left, literals stay
/// where they stand, and grouping writes the locale's group separator between groups of three.
fn integer_part(
    profile: &FormatProfile,
    whole: &str,
    pattern: &[&Item],
    grouping: bool,
) -> Result<String, Failure> {
    let placeholders: Vec<char> = pattern
        .iter()
        .filter_map(|i| match i {
            Item::Digit(c) => Some(*c),
            _ => None,
        })
        .collect();
    if grouping {
        // A literal among grouped placeholders stands where as many digits are to its right as
        // there are placeholders to its right in the code, the grouping counted on the digits
        // alone (Excel 16.0.20430, dc-uar: TEXT(1234567.891,"# ##0,0") is 1 234 56,8, and
        // TEXT(12,"#-# ##0") is -12). Modelled for `0` and `#` placeholders and literals that do
        // not fall beside a group separator.
        // A group separator beside a literal that the caller did not resolve (one between two
        // literals, or at either end) is not modelled.
        let beside_literal = pattern.iter().enumerate().any(|(at, item)| {
            **item == Item::Group
                && (at == 0
                    || !matches!(pattern[at - 1], Item::Digit(_))
                    || !matches!(pattern.get(at + 1), Some(Item::Digit(_))))
        });
        if beside_literal && pattern.iter().any(|i| matches!(i, Item::Literal(_))) {
            return Err(Failure::NotModelled);
        }
        let mut literals: Vec<(usize, String)> = Vec::new();
        let mut right = 0;
        for item in pattern.iter().rev() {
            match item {
                Item::Digit('?') => return Err(Failure::NotModelled),
                Item::Digit(_) => right += 1,
                Item::Group => {}
                other => {
                    // Literals with no placeholder between them stand together, in the code's
                    // order (dc-b1l: TEXT(503134,"0- 0 #") is 503 1- 34).
                    let text = literal_text(other, profile)?;
                    match literals.last_mut() {
                        Some((count, joined)) if *count == right => joined.insert_str(0, &text),
                        _ => literals.push((right, text)),
                    }
                }
            }
        }
        let zeros = placeholders
            .iter()
            .rev()
            .take_while(|_| true)
            .enumerate()
            .filter(|(_, c)| **c == '0')
            .map(|(at, _)| at + 1)
            .max()
            .unwrap_or(0);
        let mut digits = whole.to_owned();
        while digits.len() < zeros {
            digits.insert(0, '0');
        }
        let mut text = grouped(&digits, profile.thousands_separator);
        // Literals nearest the right first; each goes left of its digits.
        for (count, literal) in &literals {
            let mut seen = 0;
            let mut at = text.len();
            if *count > 0 {
                at = 0;
                for (index, c) in text.char_indices().rev() {
                    if c.is_ascii_digit() {
                        seen += 1;
                        if seen == *count {
                            at = index;
                            break;
                        }
                    }
                }
                // A literal where a group separator falls stands after it (dc-b1l:
                // TEXT(9514218,"0x#0# ###") is 9 x514 218).
                if seen < *count {
                    at = 0;
                }
            }
            text.insert_str(at, literal);
        }
        return Ok(text);
    }
    let chars: Vec<char> = whole.chars().collect();
    let mut next = chars.len();
    let mut out: Vec<String> = Vec::new();
    let leftmost = pattern
        .iter()
        .position(|i| matches!(i, Item::Digit(_)))
        .unwrap_or(0);
    for (at, item) in pattern.iter().enumerate().rev() {
        match item {
            Item::Digit(c) => {
                let mut piece = String::new();
                if next > 0 {
                    next -= 1;
                    piece.push(chars[next]);
                    if at == leftmost && next > 0 {
                        let rest: String = chars[..next].iter().collect();
                        piece.insert_str(0, &rest);
                        next = 0;
                    }
                } else if *c == '0' {
                    piece.push('0');
                } else if *c == '?' {
                    piece.push(' ');
                }
                out.push(piece);
            }
            other => out.push(literal_text(other, profile)?),
        }
    }
    out.reverse();
    Ok(out.concat())
}

Used by 18 functions.

fraction_part · 19 lines · calc-func/src/format/mod.rs on GitHub
/// The decimal places: `0` writes its digit, `#` drops a trailing zero, `?` writes a trailing
/// zero as a space.
fn fraction_part(digits: &str, pattern: &[char]) -> String {
    let chars: Vec<char> = digits.chars().collect();
    let mut out: Vec<char> = chars.clone();
    for at in (0..chars.len()).rev() {
        if chars[at] != '0' {
            break;
        }
        match pattern[at] {
            '#' => {
                out.pop();
            }
            '?' => out[at] = ' ',
            _ => break,
        }
    }
    out.into_iter().collect()
}

Used by 18 functions.

scientific · 87 lines · calc-func/src/format/mod.rs on GitHub
/// A scientific mantissa and exponent.
fn scientific(
    profile: &FormatProfile,
    value: f64,
    integer: &[&Item],
    fraction: &[char],
    items: &[Item],
    exponent_at: usize,
) -> Result<(String, bool), Failure> {
    let Item::Exponent(plus) = items[exponent_at] else {
        unreachable!()
    };
    let exponent_digits: Vec<char> = items[exponent_at + 1..]
        .iter()
        .take_while(|i| matches!(i, Item::Digit(_)))
        .map(|i| match i {
            Item::Digit(c) => *c,
            _ => unreachable!(),
        })
        .collect();
    if exponent_digits.is_empty() {
        return Err(Failure::NotModelled);
    }
    let whole_places: Vec<char> = integer
        .iter()
        .filter_map(|i| match i {
            Item::Digit(c) => Some(*c),
            Item::Group => None,
            _ => Some('!'),
        })
        .collect();
    if whole_places.contains(&'!') || whole_places.is_empty() {
        return Err(Failure::NotModelled);
    }
    let width = whole_places.len() as i64;
    let engineering = width > 1 && whole_places[0] == '#';
    if value == 0.0 {
        let mantissa = format!(
            "0{}{}",
            if fraction.is_empty() { "" } else { profile.decimal_separator },
            "0".repeat(fraction.len())
        );
        let exponent = format!("E{}{}", if plus { "+" } else { "" }, "0".repeat(exponent_digits.len()));
        return Ok((format!("{mantissa}{exponent}"), false));
    }
    let first = fifteen(value);
    let length = first.coefficient.digits().len() as i64;
    let point = length + first.exponent as i64; // value = 0.d * 10^point
    let leading = if engineering {
        // The exponent is a multiple of the width.
        let e = (point - 1).div_euclid(width) * width;
        point - e
    } else {
        width.min(point.max(1)).max(1)
    };
    let mut exponent = point - leading;
    let count = (leading + fraction.len() as i64) as usize;
    let (mut mantissa_digits, carry) = significant(&first, count);
    if carry == 1 {
        exponent += 1;
        if engineering && exponent.rem_euclid(width) != 0 {
            // A carry past the width moves to the next multiple.
            mantissa_digits.push('0');
        }
    }
    let lead = leading as usize;
    let (whole, frac) = mantissa_digits.split_at(lead.min(mantissa_digits.len()));
    let frac = fraction_part(&frac[..fraction.len().min(frac.len())], fraction);
    let mut out = whole.to_owned();
    if !fraction.is_empty() {
        out.push_str(profile.decimal_separator);
        out.push_str(&frac);
    }
    let sign = if exponent < 0 {
        "-"
    } else if plus {
        "+"
    } else {
        ""
    };
    out.push_str(&format!(
        "E{sign}{:0width$}",
        exponent.abs(),
        width = exponent_digits.len()
    ));
    Ok((out, true))
}

Used by 18 functions.

fraction_code · 30 lines · calc-func/src/format/mod.rs on GitHub
fn fraction_code(section: &str) -> Option<FractionCode> {
    let (top, bottom) = section.split_once('/')?;
    let placeholders = |s: &str| !s.is_empty() && s.chars().all(|c| matches!(c, '0' | '#' | '?'));
    if !placeholders(top) {
        return None;
    }
    let denominator = if placeholders(bottom) {
        let chars: Vec<char> = bottom.chars().collect();
        if chars.contains(&'0') && chars.iter().any(|c| *c != '0') {
            return None;
        }
        Err(chars)
    } else if !bottom.is_empty()
        && bottom.len() <= 9
        && bottom.chars().all(|c| c.is_ascii_digit())
        && !bottom.starts_with('0')
    {
        Ok(bottom.parse().ok()?)
    } else {
        return None;
    };
    let numerator: Vec<char> = top.chars().collect();
    if numerator.contains(&'0') && numerator.iter().any(|c| *c != '0') {
        return None;
    }
    Some(FractionCode {
        numerator,
        denominator,
    })
}

Used by 18 functions.

FractionCode::render · 50 lines · calc-func/src/format/mod.rs on GitHub
fn render(&self, value: f64, show_minus: bool) -> Rendered {
    let x = value.abs();
    let (numerator, denominator) = match &self.denominator {
        Ok(d) => {
            let n = (x * *d as f64 + 0.5).floor();
            if !(n <= 32767.0) {
                return Err(Failure::Value);
            }
            (n as u64, *d)
        }
        Err(places) => {
            let largest = 10u64.pow(places.len() as u32) - 1;
            convergent(x, largest).ok_or(Failure::NotModelled)?
        }
    };
    let mut out = String::new();
    if show_minus && value < 0.0 && numerator != 0 {
        out.push('-');
    }
    let top = numerator.to_string();
    let missing = self.numerator.len().saturating_sub(top.len());
    for c in &self.numerator[..missing] {
        match c {
            '0' => out.push('0'),
            '?' => out.push(' '),
            _ => {}
        }
    }
    out.push_str(&top);
    out.push('/');
    let bottom = denominator.to_string();
    match &self.denominator {
        Ok(_) => out.push_str(&bottom),
        Err(places) => {
            let missing = places.len().saturating_sub(bottom.len());
            if places.first() == Some(&'0') {
                out.push_str(&"0".repeat(missing));
                out.push_str(&bottom);
            } else {
                out.push_str(&bottom);
                for c in &places[places.len() - missing..] {
                    if *c == '?' {
                        out.push(' ');
                    }
                }
            }
        }
    }
    Ok(out)
}

Used by 18 functions.

mixed_fraction_under_space_grouping · 17 lines · calc-func/src/format/mod.rs on GitHub
/// Whether `section` is a fraction with an integer part set off by the group separator, under a
/// locale whose group separator is a space: Excel 16.0.20430 under en-ZA gave #VALUE! for every
/// such code it was given (`# ?/?`, `# ??/??`, `# ???/???`, `# ?/8`, `# ?/100`, `0 ?/?`,
/// `#,##0 ?/?`; dc-uar, the catalogue oracle's sheet dcuar_text).
fn mixed_fraction_under_space_grouping(section: &str, profile: &FormatProfile) -> bool {
    if profile.thousands_separator != " " {
        return false;
    }
    let Some((whole, fraction)) = section.rsplit_once(' ') else {
        return false;
    };
    let placeholders = |s: &str| !s.is_empty() && s.chars().all(|c| matches!(c, '0' | '#' | '?'));
    whole.chars().any(|c| matches!(c, '0' | '#'))
        && whole.chars().all(|c| matches!(c, '0' | '#' | '?' | ','))
        && fraction_code(fraction).is_some()
        && fraction.split_once('/').is_some_and(|(top, _)| placeholders(top))
}

Used by 18 functions.

convergent · 22 lines · calc-func/src/format/mod.rs on GitHub
/// The last convergent of `x`'s continued fraction, computed in binary64, whose denominator is
/// at most `largest`.
fn convergent(x: f64, largest: u64) -> Option<(u64, u64)> {
    let (mut h0, mut h1, mut k0, mut k1) = (0f64, 1f64, 1f64, 0f64);
    let mut best = None;
    let mut y = x;
    for _ in 0..64 {
        let a = y.floor();
        let (h2, k2) = (a * h1 + h0, a * k1 + k0);
        if k2 > largest as f64 || !h2.is_finite() || h2 >= 1e15 {
            break;
        }
        (h0, h1, k0, k1) = (h1, h2, k1, k2);
        best = Some((h1 as u64, k1 as u64));
        let rest = y - a;
        if rest == 0.0 {
            break;
        }
        y = 1.0 / rest;
    }
    best
}

Used by 18 functions.

text_declared_date_locale · 17 lines · calc-func/src/format/mod.rs on GitHub
/// A declared date-name locale supplies all profile fields used by these date-only
/// sections. Host separators and designators remain unavailable; admit only fields
/// that never read them, then use the same formatter with the locale the code names.
pub(crate) fn text_declared_date_locale(system: DateSystem, value: f64, code: &str) -> Rendered {
    let sections = sections(code);
    if sections.iter().any(|section| condition_of(section).is_some()) {
        return Err(Failure::NotModelled);
    }
    let (section, _) = choose(&sections, value).map_err(|_| Failure::NotModelled)?;
    let (stripped, name_locale, _) = strip_tokens(&section).map_err(|_| Failure::NotModelled)?;
    let locale = name_locale.ok_or(Failure::NotModelled)?;
    if stripped.trim().is_empty() || !stripped.chars().all(|c| matches!(c,
        'y' | 'Y' | 'm' | 'M' | 'd' | 'D' | 'g' | 'G' | 'e' | 'E' | ' ')) {
        return Err(Failure::NotModelled);
    }
    text(&crate::locale::format_profile(locale), system, value, code)
}

Used by 18 functions.

text · 76 lines · calc-func/src/format/mod.rs on GitHub
/// TEXT of a number under `code`.
pub(crate) fn text(profile: &FormatProfile, system: DateSystem, value: f64, code: &str) -> Rendered {
    if !value.is_finite() {
        return Err(Failure::Value);
    }
    let sections = sections(code);
    let (section, signed_section) = choose(&sections, value)?;
    let (stripped, name_locale, locale_declared) = strip_tokens(&section)?;
    if stripped.trim().eq_ignore_ascii_case("general") {
        let shown = general(profile, if signed_section { value.abs() } else { value });
        return Ok(shown);
    }
    let magnitude_only = signed_section;
    if !locale_declared && measured_host(profile) && datetime::has_invalid_exponent(&stripped) {
        return Err(Failure::Value);
    }
    if datetime::looks_like_datetime_format(&stripped) {
        if value < 0.0 {
            return Err(Failure::Value);
        }
        if value >= 2_958_466.0 {
            return Err(Failure::Value);
        }
        if datetime::without_subseconds(&stripped)
            .chars()
            .any(|c| matches!(c, 'b' | 'B' | '0') ||
                (matches!(c, 'e' | 'g' | 'G') && name_locale != Some(crate::locale::LocaleId::JaJp)
                    && !(!locale_declared && measured_host(profile))))
        {
            return Err(Failure::NotModelled);
        }
        return datetime::render_with_datetime_tokens(profile, system, value, &stripped, name_locale, !locale_declared)
            .ok_or(Failure::NotModelled);
    }
    if let Some(fraction) = fraction_code(&stripped) {
        return fraction.render(value, !magnitude_only);
    }
    if mixed_fraction_under_space_grouping(&stripped, profile) {
        return Err(Failure::Value);
    }
    let mut items = items(&stripped, profile)?;
    if !items.iter().any(|i| matches!(i, Item::Digit(_))) {
        // Fn8 literal-only controls: a run of ordinary-space grouping tokens
        // writes one space. Numeric grouping and trailing scaling stay below.
        if measured_host(profile) && profile.thousands_separator == " " {
            items.dedup_by(|a, b| *a == Item::Group && *b == Item::Group);
            for item in &mut items {
                if *item == Item::Group { *item = Item::Literal(" ".to_owned()); }
            }
        }
        // A section of literals alone writes them (an empty one writes nothing).
        if items.iter().any(|i| matches!(i, Item::Point | Item::Group | Item::Exponent(_))) {
            return Err(Failure::NotModelled);
        }
        if items.iter().any(|i| matches!(i, Item::TextPlaceholder)) {
            // Fn4 qt001..qt030: active @ and @! select General for numbers,
            // including values whose full numeric text has different precision.
            return Ok(general(profile, if signed_section { value.abs() } else { value }));
        }
        let literal = items
            .iter()
            .map(|i| literal_text(i, profile))
            .collect::<Result<String, Failure>>()?;
        // Keep the ordinary automatic sign when section selection has not
        // suppressed it, even for quoted text (fn4 qt008). Empty stays empty.
        return Ok(if value < 0.0 && !signed_section && !literal.is_empty() {
            format!("-{literal}")
        } else {
            literal
        });
    }
    if items.iter().any(|i| matches!(i, Item::TextPlaceholder)) {
        return Err(Failure::NotModelled);
    }
    numeric(profile, value, &items, !magnitude_only)
}

Used by 18 functions.

text_of_text · 22 lines · calc-func/src/format/mod.rs on GitHub
/// TEXT of a text value under `code`: the fourth section, or a first section holding `@`, with
/// `@` written as the text; otherwise the text as it is.
pub(crate) fn text_of_text(text: &str, code: &str, profile: &FormatProfile) -> Rendered {
    let sections = sections(code);
    let section = match sections.get(3) {
        Some(section) => section.clone(),
        None if sections.len() == 1 && sections[0].contains('@') => sections[0].clone(),
        None => return Ok(text.to_owned()),
    };
    let (stripped, _, _) = strip_tokens(&section)?;
    let mut out = String::new();
    for part in stripped.split('@').enumerate() {
        if part.0 > 0 {
            out.push_str(text);
        }
        let items = items(part.1, profile)?;
        for item in &items {
            out.push_str(&literal_text(item, profile)?);
        }
    }
    Ok(out)
}

Used by 18 functions.