use std::{borrow::Cow, string::String, vec::Vec};
use smol_str::SmolStr;
#[derive(Clone, Debug, thiserror::Error)]
pub enum NormalizationError {
#[error("normalised text is empty")]
EmptyText,
#[error("normaliser rule failed: {detail}")]
RuleFailed {
detail: SmolStr,
},
}
#[derive(Clone, Debug)]
pub struct NormalizedText<'a> {
normalized: String,
original_words: Vec<Cow<'a, str>>,
wildcard_boundary_per_word: Vec<WildcardBoundary>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct WildcardBoundary {
prefix: u32,
suffix: u32,
}
impl WildcardBoundary {
pub const NONE: Self = Self {
prefix: 0,
suffix: 0,
};
#[must_use]
pub const fn new(prefix: u32, suffix: u32) -> Self {
Self { prefix, suffix }
}
#[must_use]
pub const fn prefix(&self) -> u32 {
self.prefix
}
#[must_use]
pub const fn suffix(&self) -> u32 {
self.suffix
}
}
impl<'a> NormalizedText<'a> {
pub const fn new(normalized: String, original_words: Vec<Cow<'a, str>>) -> Self {
Self {
normalized,
original_words,
wildcard_boundary_per_word: Vec::new(),
}
}
pub fn with_wildcards(
normalized: String,
original_words: Vec<Cow<'a, str>>,
wildcard_boundary_per_word: Vec<WildcardBoundary>,
) -> Self {
assert_eq!(
original_words.len(),
wildcard_boundary_per_word.len(),
"wildcard_boundary_per_word must align 1:1 with original_words"
);
Self {
normalized,
original_words,
wildcard_boundary_per_word,
}
}
pub fn normalized(&self) -> &str {
&self.normalized
}
pub fn original_words(&self) -> &[Cow<'a, str>] {
&self.original_words
}
pub fn wildcard_boundary_per_word(&self) -> &[WildcardBoundary] {
&self.wildcard_boundary_per_word
}
}
pub trait TextNormalizer: Send {
fn normalize<'a>(&self, text: &'a str) -> Result<NormalizedText<'a>, NormalizationError>;
fn use_word_delimiter(&self) -> bool {
true
}
}
pub type DynTextNormalizer = std::boxed::Box<dyn TextNormalizer>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalized_text_round_trip() {
let nt = NormalizedText::new(
String::from("hello world"),
vec![Cow::Borrowed("Hello"), Cow::Borrowed("world.")],
);
assert_eq!(nt.normalized(), "hello world");
assert_eq!(nt.original_words().len(), 2);
assert_eq!(nt.original_words()[0], "Hello");
}
#[test]
fn normalization_error_displays_kinds() {
assert!(NormalizationError::EmptyText.to_string().contains("empty"));
assert!(
NormalizationError::RuleFailed {
detail: "bad contraction".into()
}
.to_string()
.contains("bad contraction")
);
}
}