braillify 2.0.0

Rust 기반 크로스플랫폼 한국어 점역 라이브러리
Documentation
//! 수학 제57항 — 정적분.
//!
//! Definite integral ∫ with subscript/superscript bounds.

use super::math_token_rule::{MathEncodeState, MathTokenEngine, MathTokenResult, MathTokenRule};
use super::parser::MathToken;
use super::{rule_6, rule_56};

fn next_non_space(tokens: &[MathToken], mut index: usize) -> Option<usize> {
    while matches!(tokens.get(index), Some(MathToken::Space)) {
        index += 1;
    }
    tokens.get(index).map(|_| index)
}

fn split_definite_integral_bounds(
    tokens: &[MathToken],
) -> Option<(Vec<MathToken>, Vec<MathToken>)> {
    let comma_index = tokens
        .iter()
        .position(|token| matches!(token, MathToken::Operator(',')))?;

    let lower = tokens[..comma_index]
        .iter()
        .filter(|token| !matches!(token, MathToken::Space))
        .cloned()
        .collect::<Vec<_>>();
    let upper = tokens[comma_index + 1..]
        .iter()
        .filter(|token| !matches!(token, MathToken::Space))
        .cloned()
        .collect::<Vec<_>>();

    if lower.is_empty() || upper.is_empty() {
        return None;
    }

    Some((lower, upper))
}

pub struct DefiniteIntegralRule;

impl MathTokenRule for DefiniteIntegralRule {
    fn name(&self) -> &'static str {
        "DefiniteIntegralRule"
    }

    fn priority(&self) -> u16 {
        40
    }

    fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
        let Some(open_index) = next_non_space(tokens, index + 1) else {
            return false;
        };
        matches!(tokens.get(index), Some(MathToken::MathSymbol('\u{222B}')))
            && matches!(tokens.get(open_index), Some(MathToken::OpenParen(_)))
    }

    fn apply(
        &self,
        tokens: &[MathToken],
        index: usize,
        result: &mut Vec<u8>,
        state: &mut MathEncodeState,
        engine: &MathTokenEngine,
    ) -> Result<MathTokenResult, String> {
        let open_index = next_non_space(tokens, index + 1)
            .ok_or_else(|| "Missing bounds opener in definite integral".to_string())?;

        let Some(close_index) = rule_6::find_matching_paren(tokens, open_index) else {
            return Ok(MathTokenResult::Skip);
        };

        let Some((lower, upper)) =
            split_definite_integral_bounds(&tokens[open_index + 1..close_index])
        else {
            return Ok(MathTokenResult::Skip);
        };

        rule_56::encode_integral_symbol('\u{222B}', result)?;
        result.push(48);
        engine.encode_tokens(&lower, result)?;
        result.push(0);
        engine.encode_tokens(&upper, result)?;
        if !matches!(tokens.get(close_index + 1), Some(MathToken::Space) | None) {
            result.push(0);
        }

        state.prev_was_number = false;
        Ok(MathTokenResult::Consumed(close_index + 1 - index))
    }
}

#[cfg(test)]
mod tests {
    use crate::rules::math::encoder::encode_math_expression;

    #[test]
    fn test_rule_57_placeholder() {
        // 수학 제57항 — 정적분
        // Encoding is handled by the math encoder pipeline.
    }

    #[test]
    fn encodes_parenthesized_bounds() {
        assert_eq!(
            encode_math_expression("∫(a,b) f(x)dx").unwrap(),
            vec![46, 48, 1, 0, 3, 0, 11, 38, 45, 52, 25, 45]
        );
    }

    #[test]
    fn encodes_parenthesized_bounds_with_space_after_integral() {
        assert_eq!(
            encode_math_expression("∫ (a,b) f(x)dx").unwrap(),
            vec![46, 48, 1, 0, 3, 0, 11, 38, 45, 52, 25, 45]
        );
    }
}