use std::sync::OnceLock;
use regex::Regex;
use serde_json::Value;
use crate::language::language::value_as_f64;
use super::counter::SyllableCounter;
fn non_letter_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"[^\p{L}]").unwrap())
}
pub struct HeuristicSyllableCounter {
has_config: bool,
problem_words: std::collections::HashMap<String, i64>,
subtract_patterns: Vec<fancy_regex::Regex>,
add_patterns: Vec<fancy_regex::Regex>,
prefixes: Vec<(String, i64)>,
suffixes: Vec<(String, i64)>,
vowel_mode_individual: bool,
individual_re: Regex,
cluster_re: Regex,
problem_words_present: bool,
subtract_present: bool,
add_present: bool,
}
fn object_string_array(v: Option<&Value>) -> Vec<String> {
match v {
Some(Value::Array(arr)) => arr
.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect(),
_ => Vec::new(),
}
}
fn object_int_map_ordered(v: Option<&Value>) -> Vec<(String, i64)> {
match v {
Some(Value::Object(map)) => map
.iter()
.map(|(k, val)| (k.clone(), value_as_f64(val).unwrap_or(0.0) as i64))
.collect(),
_ => Vec::new(),
}
}
impl HeuristicSyllableCounter {
pub fn new(config: Option<&Value>) -> Self {
let obj = config.and_then(|c| c.as_object());
let problem_words: std::collections::HashMap<String, i64> = obj
.and_then(|o| o.get("problemWords"))
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.map(|(k, val)| (k.clone(), value_as_f64(val).unwrap_or(0.0) as i64))
.collect()
})
.unwrap_or_default();
let subtract_raw = object_string_array(obj.and_then(|o| o.get("subtractPatterns")));
let add_raw = object_string_array(obj.and_then(|o| o.get("addPatterns")));
let prefixes = object_int_map_ordered(obj.and_then(|o| o.get("prefixes")));
let suffixes = object_int_map_ordered(obj.and_then(|o| o.get("suffixes")));
let subtract_present = !subtract_raw.is_empty();
let add_present = !add_raw.is_empty();
let problem_words_present = !problem_words.is_empty();
let subtract_patterns = subtract_raw
.iter()
.filter_map(|p| fancy_regex::Regex::new(p).ok())
.collect();
let add_patterns = add_raw
.iter()
.filter_map(|p| fancy_regex::Regex::new(p).ok())
.collect();
let vowel_pattern = obj
.and_then(|o| o.get("vowelPattern"))
.and_then(|v| v.as_str())
.unwrap_or("[aeiouy]");
let vowel_chars: String = vowel_pattern
.trim_matches(|c| c == '[' || c == ']')
.to_string();
let vowel_mode_individual = obj
.and_then(|o| o.get("vowelMode"))
.and_then(|v| v.as_str())
.map(|s| s == "individual")
.unwrap_or(false);
let individual_re = Regex::new(&format!("[{vowel_chars}]"))
.unwrap_or_else(|_| Regex::new("[aeiouy]").unwrap());
let cluster_re = Regex::new(&format!("[^{vowel_chars}]+"))
.unwrap_or_else(|_| Regex::new("[^aeiouy]+").unwrap());
HeuristicSyllableCounter {
has_config: config.is_some(),
problem_words,
subtract_patterns,
add_patterns,
prefixes,
suffixes,
vowel_mode_individual,
individual_re,
cluster_re,
problem_words_present,
subtract_present,
add_present,
}
}
fn count_vowel_groups(&self, clean: &str) -> i64 {
if self.vowel_mode_individual {
self.individual_re.find_iter(clean).count() as i64
} else {
self.cluster_re
.split(clean)
.filter(|p| !p.is_empty())
.count() as i64
}
}
pub fn has_rules(&self) -> bool {
self.has_config
&& (self.problem_words_present
|| self.subtract_present
|| self.add_present
|| !self.prefixes.is_empty()
|| !self.suffixes.is_empty())
}
pub fn has_word(&self, word: &str) -> bool {
let word = word.trim();
if word.is_empty() {
return false;
}
self.problem_words.contains_key(&word.to_lowercase())
}
}
impl SyllableCounter for HeuristicSyllableCounter {
fn count_syllables(&self, word: &str) -> i64 {
let word = word.trim();
if word.is_empty() {
return 0;
}
let lower = word.to_lowercase();
if let Some(&n) = self.problem_words.get(&lower) {
return n;
}
let clean_owned = non_letter_re().replace_all(&lower, "").into_owned();
if clean_owned.is_empty() {
return 1;
}
let mut clean = clean_owned;
let mut affix_syllables: i64 = 0;
for (prefix, n) in &self.prefixes {
if let Some(stripped) = clean.strip_prefix(prefix.as_str()) {
clean = stripped.to_string();
affix_syllables += n;
}
}
for (suffix, n) in &self.suffixes {
if let Some(stripped) = clean.strip_suffix(suffix.as_str()) {
clean = stripped.to_string();
affix_syllables += n;
}
}
let vowel_run_count = self.count_vowel_groups(&clean);
let mut count = vowel_run_count + affix_syllables;
for re in &self.subtract_patterns {
if re.is_match(&clean).unwrap_or(false) {
count -= 1;
}
}
for re in &self.add_patterns {
if re.is_match(&clean).unwrap_or(false) {
count += 1;
}
}
count.max(1)
}
fn split_syllables(&self, word: &str) -> Vec<String> {
let count = self.count_syllables(word);
if count <= 1 {
return if word.is_empty() {
Vec::new()
} else {
vec![word.to_string()]
};
}
let chars: Vec<char> = word.chars().collect();
let length = chars.len() as i64;
if count >= length {
return chars.iter().map(|c| c.to_string()).collect();
}
let count = count as usize;
let length = length as usize;
let part_len = length / count;
let extra = length % count;
let mut parts: Vec<String> = Vec::with_capacity(count);
let mut pos = 0usize;
for i in 0..count {
let cur_len = part_len + if i < extra { 1 } else { 0 };
let part: String = chars[pos..pos + cur_len].iter().collect();
parts.push(part);
pos += cur_len;
}
parts
}
}