Shared Rust kernels

format/cell_info

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

sections · 24 lines · calc-func/src/format/cell_info.rs on GitHub
/// The code's sections, split at `;` outside quotes, brackets and escapes.
fn sections(code: &str) -> Vec<&str> {
    let mut out = Vec::new();
    let (mut start, mut quoted, mut bracket, mut escaped) = (0, false, false, false);
    for (i, c) in code.char_indices() {
        if escaped {
            escaped = false;
            continue;
        }
        match c {
            '\\' if !quoted => escaped = true,
            '"' if !bracket => quoted = !quoted,
            '[' if !quoted => bracket = true,
            ']' if !quoted => bracket = false,
            ';' if !quoted && !bracket => {
                out.push(&code[start..i]);
                start = i + 1;
            }
            _ => {}
        }
    }
    out.push(&code[start..]);
    out
}

Used by 5 functions.

tokens · 139 lines · calc-func/src/format/cell_info.rs on GitHub
fn tokens(section: &str) -> Vec<Token> {
    let chars: Vec<char> = section.chars().collect();
    let mut out = Vec::new();
    let mut i = 0;
    let starts = |i: usize, word: &str| -> bool {
        let w: Vec<char> = word.chars().collect();
        i + w.len() <= chars.len()
            && chars[i..i + w.len()]
                .iter()
                .zip(&w)
                .all(|(a, b)| a.eq_ignore_ascii_case(b))
    };
    while i < chars.len() {
        let c = chars[i];
        let run = |i: usize, lower: char| {
            chars[i..]
                .iter()
                .take_while(|x| x.to_ascii_lowercase() == lower)
                .count()
        };
        match c {
            '"' => {
                let end = chars[i + 1..]
                    .iter()
                    .position(|&x| x == '"')
                    .map_or(chars.len(), |p| i + 1 + p);
                out.push(Token::Quoted(chars[i + 1..end].iter().collect()));
                i = end + 1;
            }
            '\\' => {
                if let Some(&x) = chars.get(i + 1) {
                    out.push(Token::Literal(x));
                }
                i += 2;
            }
            '_' | '*' => {
                out.push(Token::Spacing);
                i += 2;
            }
            '[' => {
                let end = chars[i + 1..]
                    .iter()
                    .position(|&x| x == ']')
                    .map_or(chars.len(), |p| i + 1 + p);
                let inner: String = chars[i + 1..end].iter().collect();
                let lower = inner.to_ascii_lowercase();
                out.push(if let Some(rest) = inner.strip_prefix('$') {
                    Token::Locale {
                        with_id: rest.contains('-'),
                    }
                } else if inner.starts_with(['<', '>', '=']) {
                    Token::Condition
                } else if !lower.is_empty()
                    && (lower.chars().all(|x| x == 'h')
                        || lower.chars().all(|x| x == 'm')
                        || lower.chars().all(|x| x == 's'))
                {
                    Token::Elapsed
                } else {
                    Token::Color
                });
                i = end + 1;
            }
            _ if starts(i, "general") => {
                out.push(Token::General);
                i += 7;
            }
            _ if starts(i, "am/pm") => {
                out.push(Token::AmPm);
                i += 5;
            }
            _ if starts(i, "a/p") => {
                out.push(Token::AmPmShort);
                i += 3;
            }
            '0' | '#' | '?' => {
                out.push(Token::Placeholder);
                i += 1;
            }
            '.' => {
                out.push(Token::Point);
                i += 1;
            }
            ',' => {
                out.push(Token::Group);
                i += 1;
            }
            '%' => {
                out.push(Token::Percent);
                i += 1;
            }
            '/' => {
                out.push(Token::Slash);
                i += 1;
            }
            '@' => {
                out.push(Token::At);
                i += 1;
            }
            'E' | 'e' if matches!(chars.get(i + 1), Some('+' | '-')) => {
                out.push(Token::Exponent);
                i += 2;
            }
            _ => match c.to_ascii_lowercase() {
                'd' => {
                    let n = run(i, 'd');
                    out.push(Token::Day(n));
                    i += n;
                }
                'm' => {
                    let n = run(i, 'm');
                    out.push(Token::Month(n));
                    i += n;
                }
                'y' => {
                    i += run(i, 'y');
                    out.push(Token::Year);
                }
                'h' => {
                    i += run(i, 'h');
                    out.push(Token::Hour);
                }
                's' => {
                    i += run(i, 's');
                    out.push(Token::Second);
                }
                'e' | 'g' | 'b' => {
                    i += run(i, c.to_ascii_lowercase());
                    out.push(Token::Era);
                }
                _ => {
                    out.push(Token::Literal(c));
                    i += 1;
                }
            },
        }
    }
    out
}

Used by 5 functions.

is_date_time · 14 lines · calc-func/src/format/cell_info.rs on GitHub
fn is_date_time(token: &Token) -> bool {
    matches!(
        token,
        Token::Day(_)
            | Token::Month(_)
            | Token::Year
            | Token::Hour
            | Token::Second
            | Token::AmPm
            | Token::AmPmShort
            | Token::Era
            | Token::Elapsed
    )
}

Used by 5 functions.

date_time_class · 69 lines · calc-func/src/format/cell_info.rs on GitHub
/// The class of a date or time section: `None` for `G`.
fn date_time_class(tokens: &[Token]) -> Option<u8> {
    if tokens.iter().any(|t| {
        matches!(t, Token::Era | Token::Elapsed) || matches!(t, Token::Day(n) if *n >= 3)
    }) {
        return None;
    }
    let tagged = tokens
        .iter()
        .any(|t| matches!(t, Token::Locale { with_id: true }));
    // The date and time tokens in order, each month token resolved to month (`M`) or minute.
    let parts: Vec<&Token> = tokens
        .iter()
        .filter(|t| is_date_time(t) && !matches!(t, Token::AmPm | Token::AmPmShort))
        .collect();
    let minute = |at: usize| {
        matches!(parts[at], Token::Month(_))
            && ((at > 0 && matches!(parts[at - 1], Token::Hour))
                || matches!(parts.get(at + 1), Some(Token::Second)))
    };
    match parts.first()? {
        Token::Hour => {
            if tagged {
                return None;
            }
            let has_minute = (0..parts.len()).any(minute);
            if !has_minute {
                return None;
            }
            let seconds = parts.iter().any(|t| matches!(t, Token::Second));
            let am_pm = tokens.iter().any(|t| matches!(t, Token::AmPm | Token::AmPmShort));
            Some(match (seconds, am_pm) {
                (true, true) => 6,
                (true, false) => 8,
                (false, true) => 7,
                (false, false) => 9,
            })
        }
        Token::Second => None,
        Token::Month(_) if minute(0) => None,
        _ => {
            let mut order = Vec::new();
            for (at, part) in parts.iter().enumerate() {
                let key = match part {
                    Token::Day(_) => 'd',
                    Token::Month(_) if !minute(at) => 'm',
                    Token::Year => 'y',
                    _ => continue,
                };
                if !order.contains(&key) {
                    order.push(key);
                }
            }
            let class: u8 = match order.as_slice() {
                ['d', 'm', 'y'] => 1,
                ['d', 'm'] => 2,
                ['m', 'y'] => 3,
                ['m', 'd', 'y'] => 4,
                ['m', 'd'] => 5,
                _ => return None,
            };
            if tagged {
                class.checked_sub(2).filter(|c| *c > 0)
            } else {
                Some(class)
            }
        }
    }
}

Used by 5 functions.

places · 13 lines · calc-func/src/format/cell_info.rs on GitHub
/// The decimal places a number section shows: the placeholders after its decimal point, before
/// an exponent, at most 15.
fn places(tokens: &[Token]) -> usize {
    let Some(point) = tokens.iter().position(|t| *t == Token::Point) else {
        return 0;
    };
    tokens[point + 1..]
        .iter()
        .take_while(|t| **t != Token::Exponent)
        .filter(|t| **t == Token::Placeholder)
        .count()
        .min(15)
}

Used by 5 functions.

number_class · 32 lines · calc-func/src/format/cell_info.rs on GitHub
fn number_class(tokens: &[Token], profile: &FormatProfile) -> String {
    if tokens.iter().any(|t| *t == Token::General) {
        return "G".to_owned();
    }
    let n = places(tokens);
    let symbol = profile.currency_symbol;
    let currency = !symbol.is_empty()
        && tokens
            .iter()
            .any(|t| matches!(t, Token::Quoted(q) if q == symbol));
    if currency {
        return format!("C{n}");
    }
    if tokens.iter().any(|t| *t == Token::Percent) {
        return format!("P{n}");
    }
    if tokens.iter().any(|t| *t == Token::Exponent) {
        return format!("S{n}");
    }
    let slash = tokens.iter().position(|t| *t == Token::Slash);
    if slash.is_some_and(|at| {
        tokens[..at].iter().any(|t| *t == Token::Placeholder)
            || tokens[at + 1..].iter().any(|t| *t == Token::Placeholder)
    }) {
        return "G".to_owned();
    }
    if tokens.iter().any(|t| *t == Token::Placeholder) {
        let grouped = tokens.windows(2).any(|w| w == [Token::Placeholder, Token::Group]);
        return if grouped { format!(",{n}") } else { format!("F{n}") };
    }
    "G".to_owned()
}

Used by 5 functions.

classify · 35 lines · calc-func/src/format/cell_info.rs on GitHub
/// What CELL reports of the number format `code` under `profile`; `builtin` is the built-in
/// format ID the cell's format names, when it names one.
pub(crate) fn classify(code: &str, builtin: Option<u32>, profile: &FormatProfile) -> FormatClass {
    let short_date;
    let code = match builtin {
        Some(14) => profile.short_date_pattern,
        Some(22) => {
            short_date = format!("{} h:mm", profile.short_date_pattern);
            short_date.as_str()
        }
        _ => code,
    };
    let parts = sections(code);
    let first = tokens(parts[0]);
    let mut class = if first.iter().any(is_date_time) {
        date_time_class(&first).map_or_else(|| "G".to_owned(), |c| format!("D{c}"))
    } else {
        number_class(&first, profile)
    };
    let color = parts
        .get(1)
        .is_some_and(|second| tokens(second).iter().any(|t| *t == Token::Color));
    let parentheses = first.iter().any(|t| *t == Token::Literal('('));
    if color {
        class.push('-');
    }
    if parentheses {
        class.push_str("()");
    }
    FormatClass {
        code: class,
        color,
        parentheses,
    }
}

Used by 5 functions.