cellrune 0.1.17

Bounded XLSX/XLSM reading, deterministic calculation, editing, and writing for Rust
Documentation
use super::value::ErrorKind;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Section {
    Prefix,
    IntegerDigits,
    FractionDigits,
    Suffix,
}

#[derive(Debug, Default)]
struct NumberFormat {
    prefix: String,
    integer_zeros: u32,
    integer_placeholders: u32,
    grouping: bool,
    fraction_zeros: u32,
    fraction_placeholders: u32,
    percent_count: u32,
    suffix: String,
}

const UNSUPPORTED_FORMAT_LETTERS: &str = "dmyhsegDMYHSEG?*_[]";

fn compile_format(format: &str) -> Result<NumberFormat, ErrorKind> {
    if format == "@" {
        return Err(ErrorKind::Unsupported);
    }
    let mut compiled = NumberFormat::default();
    let mut section = Section::Prefix;
    let mut characters = format.chars().peekable();
    while let Some(character) = characters.next() {
        match character {
            ';' | '@' => return Err(ErrorKind::Unsupported),
            '"' => {
                let mut literal = String::new();
                loop {
                    match characters.next() {
                        Some('"') => break,
                        Some(inner) => literal.push(inner),
                        None => return Err(ErrorKind::Unsupported),
                    }
                }
                match section {
                    Section::Prefix => compiled.prefix.push_str(&literal),
                    _ => {
                        section = Section::Suffix;
                        compiled.suffix.push_str(&literal);
                    }
                }
            }
            '\\' => {
                let Some(escaped) = characters.next() else {
                    return Err(ErrorKind::Unsupported);
                };
                match section {
                    Section::Prefix => compiled.prefix.push(escaped),
                    _ => {
                        section = Section::Suffix;
                        compiled.suffix.push(escaped);
                    }
                }
            }
            '0' | '#' => {
                let is_zero = character == '0';
                match section {
                    Section::Prefix => {
                        section = Section::IntegerDigits;
                        compiled.integer_placeholders += 1;
                        if is_zero {
                            compiled.integer_zeros += 1;
                        }
                    }
                    Section::IntegerDigits => {
                        compiled.integer_placeholders += 1;
                        if is_zero {
                            compiled.integer_zeros += 1;
                        }
                    }
                    Section::FractionDigits => {
                        compiled.fraction_placeholders += 1;
                        if is_zero {
                            compiled.fraction_zeros += 1;
                        }
                    }
                    Section::Suffix => return Err(ErrorKind::Unsupported),
                }
            }
            '.' => match section {
                Section::IntegerDigits => section = Section::FractionDigits,
                Section::Prefix => {
                    section = Section::FractionDigits;
                }
                _ => return Err(ErrorKind::Unsupported),
            },
            ',' => {
                if section == Section::IntegerDigits
                    && matches!(characters.peek(), Some('0') | Some('#'))
                {
                    compiled.grouping = true;
                } else {
                    return Err(ErrorKind::Unsupported);
                }
            }
            '%' => {
                compiled.percent_count += 1;
                match section {
                    Section::Prefix => compiled.prefix.push('%'),
                    _ => {
                        section = Section::Suffix;
                        compiled.suffix.push('%');
                    }
                }
            }
            _ if UNSUPPORTED_FORMAT_LETTERS.contains(character) => {
                return Err(ErrorKind::Unsupported);
            }
            _ => match section {
                Section::Prefix => compiled.prefix.push(character),
                _ => {
                    section = Section::Suffix;
                    compiled.suffix.push(character);
                }
            },
        }
    }
    if compiled.integer_placeholders == 0 && compiled.fraction_placeholders == 0 {
        return Err(ErrorKind::Unsupported);
    }
    Ok(compiled)
}

pub fn format_number(value: f64, format: &str) -> Result<String, ErrorKind> {
    let compiled = compile_format(format)?;
    let mut scaled = value;
    for _ in 0..compiled.percent_count {
        scaled *= 100.0;
    }
    if !scaled.is_finite() {
        return Err(ErrorKind::Num);
    }
    let negative = scaled < 0.0;
    let magnitude = scaled.abs();
    let factor = 10_f64.powi(compiled.fraction_placeholders as i32);
    let scaled_units = (magnitude * factor).round();
    if scaled_units >= 9.007_199_254_740_992e15 {
        return Err(ErrorKind::Num);
    }
    let units = scaled_units as u64;
    let unit_text = units.to_string();
    let fraction_len = compiled.fraction_placeholders as usize;
    let padded = if unit_text.len() <= fraction_len {
        format!(
            "{}{}",
            "0".repeat(fraction_len + 1 - unit_text.len()),
            unit_text
        )
    } else {
        unit_text
    };
    let split = padded.len() - fraction_len;
    let mut integer_part = padded[..split].to_owned();
    let mut fraction_part = padded[split..].to_owned();
    while integer_part.len() < compiled.integer_zeros as usize {
        integer_part.insert(0, '0');
    }
    if integer_part == "0" && compiled.integer_zeros == 0 && compiled.fraction_placeholders > 0 {
        integer_part.clear();
    }
    let optional_fraction = (compiled.fraction_placeholders - compiled.fraction_zeros) as usize;
    for _ in 0..optional_fraction {
        if fraction_part.ends_with('0') {
            fraction_part.pop();
        } else {
            break;
        }
    }
    if compiled.grouping {
        let mut grouped = String::new();
        for (index, digit) in integer_part.chars().enumerate() {
            if index > 0 && (integer_part.len() - index) % 3 == 0 {
                grouped.push(',');
            }
            grouped.push(digit);
        }
        integer_part = grouped;
    }
    let mut output = String::new();
    if negative && (units > 0 || compiled.integer_zeros > 0) {
        output.push('-');
    }
    output.push_str(&compiled.prefix);
    output.push_str(&integer_part);
    if !fraction_part.is_empty() {
        output.push('.');
        output.push_str(&fraction_part);
    }
    output.push_str(&compiled.suffix);
    Ok(output)
}