dntk 3.1.0

Command line's multi-platform interactive calculator, GNU bc wrapper.
Documentation
use super::error::BcError;

impl super::BcExecuter {
    pub(super) fn parse_branch<'a>(
        &self,
        input: &'a str,
    ) -> Result<(Vec<&'a str>, &'a str), BcError> {
        let trimmed = input.trim_start();
        if trimmed.starts_with('{') {
            let end = Self::find_matching(trimmed, 0, '{', '}')?;
            let body = &trimmed[1..end];
            let remainder = &trimmed[end + 1..];
            let statements = self.split_statements(body);
            Ok((statements, remainder))
        } else {
            let chars: Vec<char> = trimmed.chars().collect();
            let mut idx = 0usize;
            let mut depth_round = 0;
            let mut depth_square = 0;
            let mut depth_curly = 0;

            while idx < chars.len() {
                match chars[idx] {
                    '(' => depth_round += 1,
                    ')' => {
                        if depth_round > 0 {
                            depth_round -= 1;
                        }
                    }
                    '[' => depth_square += 1,
                    ']' => {
                        if depth_square > 0 {
                            depth_square -= 1;
                        }
                    }
                    '{' => depth_curly += 1,
                    '}' => {
                        if depth_curly > 0 {
                            depth_curly -= 1;
                        }
                    }
                    'e' | 'E' if depth_round == 0 && depth_square == 0 && depth_curly == 0 => {
                        if trimmed[idx..].starts_with("else")
                            && Self::is_keyword_boundary(trimmed, idx, idx + 4)
                        {
                            break;
                        }
                    }
                    ';' if depth_round == 0 && depth_square == 0 && depth_curly == 0 => {
                        idx += 1;
                        break;
                    }
                    _ => {}
                }
                idx += 1;
            }

            let statement = trimmed[..idx].trim();
            let remainder = trimmed[idx..].trim_start();
            let statements = if statement.is_empty() {
                Vec::new()
            } else {
                vec![statement]
            };
            Ok((statements, remainder))
        }
    }

    pub(super) fn split_statements<'a>(&self, input: &'a str) -> Vec<&'a str> {
        let mut statements = Vec::new();
        let mut depth_round = 0;
        let mut depth_square = 0;
        let mut depth_curly = 0;

        let mut start = 0;
        for (idx, ch) in input.char_indices() {
            match ch {
                '(' => depth_round += 1,
                ')' => {
                    if depth_round > 0 {
                        depth_round -= 1;
                    }
                }
                '[' => depth_square += 1,
                ']' => {
                    if depth_square > 0 {
                        depth_square -= 1;
                    }
                }
                '{' => depth_curly += 1,
                '}' => {
                    if depth_curly > 0 {
                        depth_curly -= 1;
                    }
                }
                ';' | '\n' if depth_round == 0 && depth_square == 0 && depth_curly == 0 => {
                    let trimmed = input[start..idx].trim();
                    if !trimmed.is_empty() {
                        statements.push(trimmed);
                    }
                    start = idx + ch.len_utf8();
                    continue;
                }
                _ => {}
            }
        }

        let trimmed = input[start..].trim();
        if !trimmed.is_empty() {
            statements.push(trimmed);
        }

        statements
    }

    pub(super) fn detect_assignment(stmt: &str) -> Option<(&str, &str)> {
        let mut depth_round = 0;
        let mut depth_square = 0;
        let mut depth_curly = 0;
        let mut prev_char: Option<char> = None;

        for (index, ch) in stmt.char_indices() {
            match ch {
                '(' => depth_round += 1,
                ')' => {
                    if depth_round > 0 {
                        depth_round -= 1;
                    }
                }
                '[' => depth_square += 1,
                ']' => {
                    if depth_square > 0 {
                        depth_square -= 1;
                    }
                }
                '{' => depth_curly += 1,
                '}' => {
                    if depth_curly > 0 {
                        depth_curly -= 1;
                    }
                }
                '=' if depth_round == 0 && depth_square == 0 && depth_curly == 0 => {
                    if matches!(prev_char, Some('<') | Some('>') | Some('!')) {
                        prev_char = Some('=');
                        continue;
                    }
                    let rest = &stmt[index + ch.len_utf8()..];
                    if rest.starts_with('=') {
                        prev_char = Some('=');
                        continue;
                    }

                    let left = stmt[..index].trim();
                    let right = rest.trim();
                    if left.is_empty() || right.is_empty() {
                        return None;
                    }
                    return Some((left, right));
                }
                _ => {}
            }
            prev_char = Some(ch);
        }
        None
    }

    pub(super) fn split_top_level(input: &str, delimiter: char) -> Vec<&str> {
        let mut parts = Vec::new();
        let mut depth_round = 0;
        let mut depth_square = 0;
        let mut depth_curly = 0;

        let mut start = 0;
        for (idx, ch) in input.char_indices() {
            match ch {
                '(' => depth_round += 1,
                ')' => {
                    if depth_round > 0 {
                        depth_round -= 1;
                    }
                }
                '[' => depth_square += 1,
                ']' => {
                    if depth_square > 0 {
                        depth_square -= 1;
                    }
                }
                '{' => depth_curly += 1,
                '}' => {
                    if depth_curly > 0 {
                        depth_curly -= 1;
                    }
                }
                _ => {}
            }

            if ch == delimiter && depth_round == 0 && depth_square == 0 && depth_curly == 0 {
                let segment = &input[start..idx];
                parts.push(segment.trim());
                start = idx + ch.len_utf8();
            }
        }

        let tail = input[start..].trim();
        if !tail.is_empty() {
            parts.push(tail);
        }

        parts
    }

    pub(super) fn lookup_keyword(bytes: &str, expected: &str) -> bool {
        bytes.len() >= expected.len()
            && bytes[..expected.len()].eq_ignore_ascii_case(expected)
            && Self::is_keyword_boundary(bytes, 0, expected.len())
    }

    pub(super) fn starts_with_keyword(input: &str, keyword: &str) -> bool {
        let trimmed = input.trim_start();
        Self::lookup_keyword(trimmed, keyword)
    }

    pub(super) fn is_keyword_boundary(input: &str, start: usize, end: usize) -> bool {
        let bytes = input.as_bytes();
        let before = start.checked_sub(1).and_then(|idx| bytes.get(idx));
        let after = bytes.get(end);

        let prev_ok = before.is_none_or(|c| !Self::is_ident_char(*c));
        let next_ok = after.is_none_or(|c| !Self::is_ident_char(*c));
        prev_ok && next_ok
    }

    pub(super) fn find_matching(
        input: &str,
        start: usize,
        open: char,
        close: char,
    ) -> Result<usize, BcError> {
        let mut depth = 0;
        for (index, ch) in input.char_indices().skip(start) {
            if ch == open {
                depth += 1;
            } else if ch == close {
                depth -= 1;
                if depth == 0 {
                    return Ok(index);
                }
            }
        }
        Err(BcError::Error("Unmatched delimiter".to_string()))
    }

    pub(super) fn preprocess_bc_syntax(&self, statement: &str) -> String {
        let bytes = statement.as_bytes();
        let mut result = String::with_capacity(statement.len());
        let mut i = 0;
        while i < bytes.len() {
            if i + 1 < bytes.len() && bytes[i + 1] == b'(' && !Self::has_ident_before(bytes, i) {
                let replacement = match bytes[i] {
                    b's' => Some("sin("),
                    b'c' => Some("cos("),
                    b'a' => Some("atan("),
                    b'l' => Some("ln("),
                    b'e' => Some("exp("),
                    _ => None,
                };
                if let Some(rep) = replacement {
                    result.push_str(rep);
                    i += 2;
                    continue;
                }
            }
            result.push(bytes[i] as char);
            i += 1;
        }
        result
    }

    pub(super) fn is_valid_identifier(name: &str) -> bool {
        let mut chars = name.chars();
        match chars.next() {
            Some(c) if c.is_ascii_alphabetic() || c == '_' => (),
            _ => return false,
        }
        chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
    }

    pub(super) fn is_ident_char(byte: u8) -> bool {
        byte.is_ascii_alphanumeric() || byte == b'_'
    }

    pub(super) fn has_ident_before(bytes: &[u8], idx: usize) -> bool {
        if idx == 0 {
            return false;
        }
        Self::is_ident_char(bytes[idx - 1])
    }
}