uqa-analysis 0.3.5

Tokenizers, char/token filters, and analyzers for UQA full-text search
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Exact Korean numeral parsing and reference-compatible token composition.

use super::error::{check_limit, invalid};
use super::filters::Work;
use super::NoriLimits;
use crate::AnalysisResult;
use uqa_core::memory::{Budgeted, MemoryBudget};

mod policy;

pub(super) use policy::filter;

struct Context<'a, 'b> {
    work: &'a mut Work<'b>,
    budget: &'a MemoryBudget,
    maximum: usize,
}

impl Context<'_, '_> {
    fn check_digits(&self, digits: usize) -> AnalysisResult<()> {
        check_limit("Nori numeric units", digits, self.maximum)?;
        Ok(())
    }
}

impl crate::morphology::decimal::Context for Context<'_, '_> {
    fn tick(&mut self) -> AnalysisResult<()> {
        self.work.tick()
    }
    fn budget(&self) -> &MemoryBudget {
        self.budget
    }
    fn check_digits(&self, digits: usize) -> AnalysisResult<()> {
        Self::check_digits(self, digits)
    }
    fn invalid(&self, reason: &'static str) -> crate::AnalysisError {
        invalid("Nori number", reason).into()
    }
}

/// Normalize the reference numeric prefix with exact decimal arithmetic.
///
/// Malformed decimals or absent numeric prefixes retain the complete input. A successfully parsed prefix discards the remaining suffix, as Lucene does. Resource failures remain errors.
///
/// ```
/// use uqa_analysis::nori::normalize_number;
/// assert_eq!(normalize_number("3.2천")?, "3200");
/// assert_eq!(normalize_number("15,7")?, "157");
/// assert_eq!(normalize_number("12원")?, "12");
/// assert_eq!(normalize_number("1.2.3")?, "1.2.3");
/// # Ok::<(), uqa_analysis::AnalysisError>(())
/// ```
pub fn normalize_number(input: &str) -> AnalysisResult<String> {
    Ok(normalize_number_budgeted(
        input,
        NoriLimits::default(),
        &MemoryBudget::new(usize::MAX),
        &mut || Ok(()),
    )?
    .into_parts()
    .0)
}

/// Normalize a scalar numeral while retaining reservations for coefficients and both output encodings.
///
/// Input encoding, numeric coefficient buffers, and the returned text reserve bytes before allocation. A replacement coexists with its predecessor in the allowance. Count limits remain active; allocation or callback errors publish no partial result. Borrowed input and allocator bookkeeping are outside these reservations.
///
/// ```
/// use uqa_analysis::nori::{normalize_number_budgeted, NoriLimits};
/// use uqa_core::memory::MemoryBudget;
/// let budget = MemoryBudget::new(64 * 1024);
/// let output = normalize_number_budgeted(
///     "3.2천", NoriLimits::default(), &budget, &mut || Ok(()),
/// )?;
/// assert_eq!(output.as_str(), "3200");
/// assert_eq!(budget.used(), output.reserved_bytes());
/// drop(output);
/// assert_eq!(budget.used(), 0);
/// # Ok::<(), uqa_analysis::AnalysisError>(())
/// ```
pub fn normalize_number_budgeted(
    input: &str,
    limits: NoriLimits,
    budget: &MemoryBudget,
    poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Budgeted<String>> {
    let units = super::tokenizer::allocation::encode(input, limits.max_input_utf16, budget, poll)?;
    let normalized = normalize_number_utf16_budgeted(&units, limits, budget, poll)?;
    drop(units);
    let (term, memory) = crate::TokenTerm::from_utf16_budgeted(normalized, poll)?.into_parts();
    let text = term
        .into_string()
        .map_err(|_| invalid("Nori number", "invalid scalar result"))?;
    Ok(Budgeted::new(text, memory))
}

/// Apply numeric-prefix normalization to raw UTF-16 with explicit limits and cancellation.
///
/// Malformed decimals or absent numeric prefixes retain all original units, including unpaired surrogates. The output-unit limit also bounds intermediate numeric coefficients and formatting.
pub fn normalize_number_utf16(
    input: &[u16],
    limits: NoriLimits,
    poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Vec<u16>> {
    Ok(
        normalize_number_utf16_budgeted(input, limits, &MemoryBudget::new(usize::MAX), poll)?
            .into_parts()
            .0,
    )
}

/// Normalize lossless UTF-16 with one allowance for numeric coefficients and the returned buffer.
///
/// Malformed decimal input and absent numeric prefixes are copied exactly, including isolated surrogates. The caller's borrowed input has separate ownership. Byte-limit and callback errors propagate without being converted into a successful unchanged result.
pub fn normalize_number_utf16_budgeted(
    input: &[u16],
    limits: NoriLimits,
    budget: &MemoryBudget,
    poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Budgeted<Vec<u16>>> {
    let mut work = Work::new(poll)?;
    check_limit(
        "Nori input UTF-16 units",
        input.len(),
        limits.max_input_utf16,
    )?;
    let result = normalize_budgeted(input, limits.max_output_utf16, budget, &mut work)?;
    work.finish()?;
    Ok(result)
}

fn normalize_budgeted(
    input: &[u16],
    maximum: usize,
    budget: &MemoryBudget,
    work: &mut Work<'_>,
) -> AnalysisResult<Budgeted<Vec<u16>>> {
    let mut context = Context {
        work,
        budget,
        maximum,
    };
    crate::morphology::number::normalize::<policy::Symbols>(input, &mut context)
}

fn digit(unit: u16) -> Option<u8> {
    Some(match unit {
        0x0030..=0x0039 => (unit - 0x0030) as u8,
        0xff10..=0xff19 => (unit - 0xff10) as u8,
        0xc601 => 0,
        0xc77c => 1,
        0xc774 => 2,
        0xc0bc => 3,
        0xc0ac => 4,
        0xc624 => 5,
        0xc721 => 6,
        0xce60 => 7,
        0xd314 => 8,
        0xad6c => 9,
        _ => return None,
    })
}

fn exponent(unit: u16) -> usize {
    match unit {
        0xc2ed => 1,
        0xbc31 => 2,
        0xcc9c => 3,
        0xb9cc => 4,
        0xc5b5 => 8,
        0xc870 => 12,
        0xacbd => 16,
        0xd574 => 20,
        _ => 0,
    }
}