normalizer-tr 0.4.0

Bounded Turkish text-to-speech normalization with original-source diagnostics
Documentation
use std::borrow::Cow;

use unicode_normalization::{UnicodeNormalization, is_nfc};
use unicode_segmentation::UnicodeSegmentation;

use crate::{Hint, NormalizeError, SourceRange, WorkControl};

enum Mapping {
    Identity(Vec<usize>),
    Normalized(Vec<(usize, usize)>),
}

pub(crate) struct SourceMap<'a> {
    recognition: Cow<'a, str>,
    mapping: Mapping,
}

impl<'a> SourceMap<'a> {
    pub(crate) fn new(input: &'a str, control: &WorkControl) -> Result<Self, NormalizeError> {
        control.check()?;
        if is_nfc(input) {
            let mut boundaries = Vec::with_capacity(input.len() / 2 + 1);
            boundaries.push(0);
            for (offset, grapheme) in input.grapheme_indices(true) {
                control.check()?;
                boundaries.push(offset + grapheme.len());
            }
            return Ok(Self {
                recognition: Cow::Borrowed(input),
                mapping: Mapping::Identity(boundaries),
            });
        }
        let mut recognition = String::with_capacity(input.len());
        let mut boundaries = vec![(0, 0)];
        for (offset, grapheme) in input.grapheme_indices(true) {
            control.check()?;
            recognition.extend(grapheme.nfc());
            boundaries.push((recognition.len(), offset + grapheme.len()));
        }
        Ok(Self {
            recognition: Cow::Owned(recognition),
            mapping: Mapping::Normalized(boundaries),
        })
    }
    pub(crate) fn text(&self) -> &str {
        &self.recognition
    }
    pub(crate) fn cover(&self, range: SourceRange) -> SourceRange {
        match &self.mapping {
            Mapping::Identity(boundaries) => {
                let start = boundaries.partition_point(|offset| *offset <= range.start) - 1;
                let end = boundaries.partition_point(|offset| *offset < range.end);
                SourceRange::new(boundaries[start], boundaries[end])
            }
            Mapping::Normalized(boundaries) => {
                let start = boundaries.partition_point(|pair| pair.0 <= range.start) - 1;
                let end = boundaries.partition_point(|pair| pair.0 < range.end);
                SourceRange::new(boundaries[start].0, boundaries[end].0)
            }
        }
    }
    pub(crate) fn original(&self, range: SourceRange) -> Result<SourceRange, NormalizeError> {
        Ok(SourceRange::new(
            self.lookup(range.start, false)?,
            self.lookup(range.end, false)?,
        ))
    }
    fn lookup(&self, offset: usize, original: bool) -> Result<usize, NormalizeError> {
        match &self.mapping {
            Mapping::Identity(boundaries) => boundaries.binary_search(&offset).map(|_| offset),
            Mapping::Normalized(boundaries) => boundaries
                .binary_search_by_key(&offset, |pair| if original { pair.1 } else { pair.0 })
                .map(|index| {
                    if original {
                        boundaries[index].0
                    } else {
                        boundaries[index].1
                    }
                }),
        }
        .map_err(|_| NormalizeError::InvalidHint)
    }
    pub(crate) fn hints(&self, hints: &[Hint]) -> Result<Vec<Hint>, NormalizeError> {
        let mut mapped = Vec::with_capacity(hints.len());
        for hint in hints {
            if hint.range.start >= hint.range.end {
                return Err(NormalizeError::InvalidHint);
            }
            mapped.push(Hint::new(
                SourceRange::new(
                    self.lookup(hint.range.start, true)?,
                    self.lookup(hint.range.end, true)?,
                ),
                hint.kind,
            ));
        }
        mapped.sort_by_key(|hint| hint.range.start);
        if mapped
            .windows(2)
            .any(|pair| pair[0].range.end > pair[1].range.start)
        {
            return Err(NormalizeError::InvalidHint);
        }
        Ok(mapped)
    }
}