obadh_engine 0.8.1

A linguistically accurate Roman to Bengali transliteration engine
Documentation
use crate::definitions::conjuncts::ConjunctDefinitions;

use super::{
    parts::{extended_is_valid, BorrowedParts},
    PhoneticUnit, PhoneticUnitType,
};

struct ExplicitHasantChain {
    end: usize,
    text: String,
    position: usize,
}

pub(super) fn collapse_explicit_hasant_chains(
    units: &mut Vec<PhoneticUnit>,
    conjunct_defs: &ConjunctDefinitions,
) {
    let Some(first_match) = first_explicit_hasant_chain(units, conjunct_defs) else {
        return;
    };

    let mut read = first_match;
    let mut write = first_match;

    while read < units.len() {
        if let Some(chain) = explicit_hasant_chain_at(units, read, conjunct_defs) {
            units[write] = PhoneticUnit {
                text: chain.text,
                unit_type: PhoneticUnitType::Conjunct,
                position: chain.position,
            };
            read = chain.end;
            write += 1;
            continue;
        }

        super::move_unit(units, read, write);
        read += 1;
        write += 1;
    }

    units.truncate(write);
}

fn first_explicit_hasant_chain(
    units: &[PhoneticUnit],
    conjunct_defs: &ConjunctDefinitions,
) -> Option<usize> {
    (0..units.len().saturating_sub(2))
        .find(|&index| explicit_hasant_chain_at(units, index, conjunct_defs).is_some())
}

fn explicit_hasant_chain_at(
    units: &[PhoneticUnit],
    index: usize,
    conjunct_defs: &ConjunctDefinitions,
) -> Option<ExplicitHasantChain> {
    if index + 2 >= units.len()
        || units[index + 1].unit_type != PhoneticUnitType::ConsonantWithHasant
    {
        return None;
    }

    let mut parts = explicit_hasant_chain_start_parts(&units[index])?;
    let position = units[index].position;
    let mut next = index + 1;
    let mut consumed_hasant = false;

    while next < units.len() && units[next].unit_type == PhoneticUnitType::ConsonantWithHasant {
        if next + 1 >= units.len() {
            break;
        }

        if let Some(part) =
            explicit_hasant_chain_next_part(parts.as_slice(), &units[next + 1], conjunct_defs)
        {
            parts.push(part);
            consumed_hasant = true;
            next += 2;
        } else {
            break;
        }
    }

    if consumed_hasant
        && parts.len() >= 2
        && explicit_hasant_chain_is_renderable(parts.as_slice(), conjunct_defs)
    {
        return Some(ExplicitHasantChain {
            end: next,
            text: join_explicit_hasant_parts(parts.as_slice()),
            position,
        });
    }

    None
}

fn join_explicit_hasant_parts(parts: &[&str]) -> String {
    let separator_len = ",,".len() * parts.len().saturating_sub(1);
    let mut text =
        String::with_capacity(parts.iter().map(|part| part.len()).sum::<usize>() + separator_len);

    for (index, part) in parts.iter().enumerate() {
        if index > 0 {
            text.push_str(",,");
        }
        text.push_str(part);
    }

    text
}

fn explicit_hasant_chain_start_parts(unit: &PhoneticUnit) -> Option<BorrowedParts<'_>> {
    match unit.unit_type {
        PhoneticUnitType::Consonant => Some(BorrowedParts::from_one(unit.text.as_str())),
        PhoneticUnitType::RephOverConsonant => {
            let base = super::reph_base_part(unit)?;
            Some(BorrowedParts::from_two("rr", base))
        }
        PhoneticUnitType::SpecialForm if unit.text == "rr" => Some(BorrowedParts::from_one("rr")),
        _ => None,
    }
}

fn explicit_hasant_chain_next_part<'a>(
    parts: &[&str],
    unit: &'a PhoneticUnit,
    conjunct_defs: &ConjunctDefinitions,
) -> Option<&'a str> {
    match unit.unit_type {
        PhoneticUnitType::Consonant if is_explicit_phola_marker(&unit.text) => {
            if extended_is_valid(parts, &unit.text, conjunct_defs) {
                Some(unit.text.as_str())
            } else {
                None
            }
        }
        PhoneticUnitType::Consonant => Some(unit.text.as_str()),
        PhoneticUnitType::Unknown if unit.text == "w" => {
            if extended_is_valid(parts, "w", conjunct_defs) {
                Some("w")
            } else {
                None
            }
        }
        _ => None,
    }
}

fn explicit_hasant_chain_is_renderable(
    parts: &[&str],
    conjunct_defs: &ConjunctDefinitions,
) -> bool {
    if !parts.iter().any(|part| is_explicit_phola_marker(part)) {
        return true;
    }

    conjunct_defs.can_form_conjunct_from_parts(parts)
}

fn is_explicit_phola_marker(part: &str) -> bool {
    matches!(part, "w" | "y" | "Y")
}