dntk 3.1.0

Command line's multi-platform interactive calculator, GNU bc wrapper.
Documentation
use std::collections::HashMap;

use dashu::Decimal;

use super::error::BcError;

#[derive(Debug, Default)]
pub(crate) struct LiteralTable {
    values: HashMap<String, Decimal>,
    counter: usize,
}

impl LiteralTable {
    pub fn reset(&mut self) {
        self.values.clear();
        self.counter = 0;
    }

    pub fn get(&self, name: &str) -> Option<Decimal> {
        self.values.get(name).cloned()
    }

    pub fn substitute(&mut self, expr: &str) -> Result<String, BcError> {
        let mut result = String::with_capacity(expr.len());
        let chars: Vec<char> = expr.chars().collect();
        let mut index = 0;
        while index < chars.len() {
            if let Some((literal, consumed)) = Self::extract_numeric_literal(&chars, index) {
                let name = self.next_literal_name();
                let decimal = literal
                    .parse::<Decimal>()
                    .map_err(|_| BcError::Error(format!("Failed to parse literal: {literal}")))?;
                self.values.insert(name.clone(), decimal);
                result.push_str(&name);
                index += consumed;
            } else {
                result.push(chars[index]);
                index += 1;
            }
        }
        Ok(result)
    }

    fn next_literal_name(&mut self) -> String {
        let name = format!("__dntk_lit{}", self.counter);
        self.counter = self.counter.wrapping_add(1);
        name
    }

    fn extract_numeric_literal(chars: &[char], start: usize) -> Option<(String, usize)> {
        let len = chars.len();
        let mut index = start;
        let mut literal = String::new();

        let prev = Self::previous_non_whitespace(chars, start);

        if index >= len {
            return None;
        }

        if chars[index] == '+' || chars[index] == '-' {
            if !Self::is_unary_literal_start(chars[index], prev) {
                return None;
            }
            literal.push(chars[index]);
            index += 1;
            if index >= len {
                return None;
            }
        }

        let mut has_digits = false;
        let mut has_decimal_point = false;

        if index < len && chars[index].is_ascii_digit() {
            if let Some(p) = prev {
                if p.is_ascii_alphanumeric() || p == '_' {
                    return None;
                }
            }
            has_digits = true;
            while index < len && chars[index].is_ascii_digit() {
                literal.push(chars[index]);
                index += 1;
            }
        }

        if index < len && chars[index] == '.' {
            if let Some(p) = prev {
                if p.is_ascii_alphanumeric() || p == '_' {
                    return None;
                }
            }
            has_decimal_point = true;
            literal.push('.');
            index += 1;
            let mut frac_digits = 0;
            while index < len && chars[index].is_ascii_digit() {
                literal.push(chars[index]);
                index += 1;
                frac_digits += 1;
            }
            has_digits = has_digits || frac_digits > 0;
            if frac_digits == 0 {
                return None;
            }
        } else if !has_digits {
            return None;
        }

        if index < len && (chars[index] == 'e' || chars[index] == 'E') {
            literal.push(chars[index]);
            index += 1;
            if index < len && (chars[index] == '+' || chars[index] == '-') {
                literal.push(chars[index]);
                index += 1;
            }
            let mut exp_digits = 0;
            while index < len && chars[index].is_ascii_digit() {
                literal.push(chars[index]);
                index += 1;
                exp_digits += 1;
            }
            if exp_digits == 0 {
                return None;
            }
        }

        if !has_digits {
            return None;
        }

        if index < len {
            let next = chars[index];
            if next.is_ascii_alphanumeric() || next == '_' {
                return None;
            }
            if next == '.'
                && !has_decimal_point
                && index + 1 < len
                && chars[index + 1].is_ascii_alphabetic()
            {
                return None;
            }
        }

        Some((literal, index - start))
    }

    fn previous_non_whitespace(chars: &[char], index: usize) -> Option<char> {
        if index == 0 {
            return None;
        }
        let mut pos = index;
        while pos > 0 {
            pos -= 1;
            let ch = chars[pos];
            if !ch.is_whitespace() {
                return Some(ch);
            }
        }
        None
    }

    fn is_unary_literal_start(current: char, prev: Option<char>) -> bool {
        if let Some(p) = prev {
            if p.is_ascii_alphanumeric() || p == '_' || p == ')' {
                return false;
            }
        }
        current == '+' || current == '-'
    }
}