use alloc::{string::String, vec::Vec};
use serde::{Deserialize, Serialize};
use svara::phoneme::Phoneme;
use super::PronunciationDict;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeteronymContext {
pub words: Vec<String>,
pub position: usize,
}
impl HeteronymContext {
#[must_use]
pub fn new(words: &[&str], position: usize) -> Self {
assert!(
position < words.len(),
"position {position} out of bounds for {} words",
words.len()
);
Self {
words: words
.iter()
.map(alloc::string::ToString::to_string)
.collect(),
position,
}
}
#[must_use]
pub fn preceding_words(&self) -> &[String] {
let pos = self.position.min(self.words.len());
&self.words[..pos]
}
#[must_use]
pub fn following_words(&self) -> &[String] {
if self.position + 1 < self.words.len() {
&self.words[self.position + 1..]
} else {
&[]
}
}
#[must_use]
pub fn target_word(&self) -> &str {
self.words
.get(self.position)
.map(|s| s.as_str())
.unwrap_or("")
}
}
pub trait HeteronymResolver: Send + Sync {
fn select_variant(&self, word: &str, context: &HeteronymContext) -> Option<usize>;
}
impl PronunciationDict {
#[must_use]
pub fn lookup_with_context(
&self,
word: &str,
resolver: &dyn HeteronymResolver,
context: &HeteronymContext,
) -> Option<&[Phoneme]> {
let entry = self.lookup_entry(word)?;
if entry.len() > 1
&& let Some(idx) = resolver.select_variant(word, context)
&& idx < entry.len()
{
return Some(entry.all()[idx].phonemes());
}
Some(entry.primary_phonemes())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct PrimaryResolver;
impl HeteronymResolver for PrimaryResolver {
fn select_variant(&self, _word: &str, _ctx: &HeteronymContext) -> Option<usize> {
Some(0)
}
}
struct SecondVariantResolver;
impl HeteronymResolver for SecondVariantResolver {
fn select_variant(&self, _word: &str, _ctx: &HeteronymContext) -> Option<usize> {
Some(1)
}
}
struct NoneResolver;
impl HeteronymResolver for NoneResolver {
fn select_variant(&self, _word: &str, _ctx: &HeteronymContext) -> Option<usize> {
None
}
}
#[test]
fn test_context_new() {
let ctx = HeteronymContext::new(&["I", "read", "books"], 1);
assert_eq!(ctx.target_word(), "read");
assert_eq!(ctx.preceding_words(), &["I".to_string()]);
assert_eq!(ctx.following_words(), &["books".to_string()]);
}
#[test]
fn test_context_first_word() {
let ctx = HeteronymContext::new(&["read", "this"], 0);
assert!(ctx.preceding_words().is_empty());
assert_eq!(ctx.following_words(), &["this".to_string()]);
}
#[test]
fn test_context_last_word() {
let ctx = HeteronymContext::new(&["I", "read"], 1);
assert_eq!(ctx.preceding_words(), &["I".to_string()]);
assert!(ctx.following_words().is_empty());
}
#[test]
fn test_lookup_with_context_single_pronunciation() {
let dict = PronunciationDict::english_minimal();
let ctx = HeteronymContext::new(&["the"], 0);
let result = dict.lookup_with_context("the", &SecondVariantResolver, &ctx);
assert!(result.is_some());
assert_eq!(result, dict.lookup("the"));
}
#[test]
fn test_lookup_with_context_heteronym() {
let dict = PronunciationDict::english();
let ctx = HeteronymContext::new(&["I", "read", "books"], 1);
let entry = dict.lookup_entry("read").unwrap();
assert!(entry.len() >= 2);
let primary = dict.lookup_with_context("read", &PrimaryResolver, &ctx);
assert_eq!(primary, Some(entry.all()[0].phonemes()));
let second = dict.lookup_with_context("read", &SecondVariantResolver, &ctx);
assert_eq!(second, Some(entry.all()[1].phonemes()));
}
#[test]
fn test_lookup_with_context_none_resolver_returns_primary() {
let dict = PronunciationDict::english();
let ctx = HeteronymContext::new(&["I", "read", "books"], 1);
let result = dict.lookup_with_context("read", &NoneResolver, &ctx);
assert_eq!(result, dict.lookup("read"));
}
#[test]
fn test_lookup_with_context_missing_word() {
let dict = PronunciationDict::english_minimal();
let ctx = HeteronymContext::new(&["xyzzy"], 0);
let result = dict.lookup_with_context("xyzzy", &PrimaryResolver, &ctx);
assert!(result.is_none());
}
#[test]
fn test_lookup_with_context_out_of_bounds_index() {
let dict = PronunciationDict::english();
let ctx = HeteronymContext::new(&["read"], 0);
struct BadResolver;
impl HeteronymResolver for BadResolver {
fn select_variant(&self, _word: &str, _ctx: &HeteronymContext) -> Option<usize> {
Some(999)
}
}
let result = dict.lookup_with_context("read", &BadResolver, &ctx);
assert_eq!(result, dict.lookup("read"));
}
#[test]
fn test_context_serde_roundtrip() {
let ctx = HeteronymContext::new(&["I", "read", "books"], 1);
let json = serde_json::to_string(&ctx).unwrap();
let ctx2: HeteronymContext = serde_json::from_str(&json).unwrap();
assert_eq!(ctx, ctx2);
}
}