use anyhow::{Context, Result};
use clap::ValueEnum;
use regex::Regex;
const DATA: &str = include_str!("cmudict-0.7b.utf8");
const PAT_TEMPLATE_SUFFIX: &str = r"(?m)^([a-zA-Z-]*)\S* (.*{})$";
const PAT_TEMPLATE_PREFIX: &str = r"(?m)^(\S*) ({}.*)$";
const PAT_TEMPLATE_ANY: &str = r"(?m)^(\S*) (.*{}.*)$";
#[derive(PartialEq, Debug, Clone, ValueEnum)]
pub enum RhymeStyle {
Syllabic,
Vowel,
Consonant,
}
#[derive(PartialEq, Debug, Clone, ValueEnum)]
pub enum RhymeType {
Rhyme,
Alliteration,
Any,
}
pub fn output(v: &[String]) -> Result<()> {
let out = std::io::stdout();
let mut lock = out.lock();
use std::io::Write;
for s in v {
writeln!(lock, "{}", s.to_lowercase())?;
}
Ok(())
}
fn findem_re(s: &str, re: &Regex) -> Option<(String, String)> {
re.captures(&s)
.map(|caps| (caps[1].to_string(), caps[2].to_string()))
}
fn findwordphonemes(s: &str, word: &str) -> Vec<String> {
let pat = format!(r"(?m)^{} (.*)$", word);
let re = Regex::new(&pat).unwrap();
re.captures_iter(&s).map(|cap| cap[1].to_string()).collect()
}
pub fn find_onepass(
word: &str,
rhyme_style: RhymeStyle,
rhyme_type: RhymeType,
min_phonemes: usize,
keep_emphasis: bool,
bailout: Option<usize>,
) -> Result<Vec<String>> {
let phoneme_list = findwordphonemes(DATA, &word.to_uppercase());
println!("{:?}", &phoneme_list);
let phonemes = phoneme_list
.get(0)
.context(format!(
"The word '{}' was not found in the database.",
&word
))?
.clone();
println!("{:?}", &phonemes);
let match_phonemes = match rhyme_style {
RhymeStyle::Syllabic => phonemes
.split_ascii_whitespace()
.map(|s| s.to_string())
.collect(),
RhymeStyle::Vowel => wild_consos(&phonemes, keep_emphasis),
RhymeStyle::Consonant => wild_vowels(&phonemes),
};
println!("{:?}", &match_phonemes);
let n = match_phonemes.len();
let mut res = vec![];
for i in (min_phonemes..n).rev() {
let pat = match rhyme_type {
RhymeType::Rhyme => {
PAT_TEMPLATE_SUFFIX.replace("{}", &match_phonemes[n - i..].join(" "))
}
RhymeType::Alliteration => {
PAT_TEMPLATE_PREFIX.replace("{}", &match_phonemes[..i].join(" "))
}
RhymeType::Any => {
PAT_TEMPLATE_ANY.replace(
"{}",
&match_phonemes[n - i..].join(" "),
)
}
};
println!("{:?}", &pat);
let re = Regex::new(&pat).context("Unexpected regex compile error")?;
res.push(re);
}
println!("regexes:");
res.iter().for_each(|r| println!("{:?}", &r));
println!();
let mut result = vec![];
DATA.lines().for_each(|l| {
res.iter().try_for_each(|re| match findem_re(l, re) {
Some(hit) => {
result.push((hit.0, score(&phonemes, &hit.1)));
None
}
None => Some(()),
});
});
result.sort_by_key(|tup| tup.1);
Ok(result.iter().map(|tup| tup.0.clone()).collect())
}
fn score(word_phonemes: &str, candidate_phonemes: &str) -> u32 {
let w = word_phonemes.split_ascii_whitespace().count() as i32;
let c = candidate_phonemes.split_ascii_whitespace().count() as i32;
(w - c).abs() as u32
}
fn wild_consos(phonemes: &str, keep_vowel_emph: bool) -> Vec<String> {
let pat = r"([AEIOU][A-Z]*)(\d?)";
let re = Regex::new(&pat).unwrap();
phonemes
.split_ascii_whitespace()
.map(|s| {
if let Some(caps) = re.captures(s) {
let pre = caps.get(1).unwrap().as_str();
let mut num = caps.get(2).unwrap().as_str();
if !keep_vowel_emph {
num = r"\d?"
}
format!(r"{}{}", pre, num)
} else {
r"\S*".to_string()
}
})
.collect::<Vec<_>>()
}
fn wild_vowels(phonemes: &str) -> Vec<String> {
let pat = r"([AEIOU][A-Z]*)(\d?)";
let re = Regex::new(&pat).unwrap();
phonemes
.split_ascii_whitespace()
.map(|s| {
if let Some(caps) = re.captures(s) {
let pre = caps.get(1).unwrap().as_str();
let num = caps.get(2).unwrap().as_str();
println!("{}-{}", pre, num);
r"\S*".to_string()
} else {
s.to_string()
}
})
.collect::<Vec<_>>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output() {
let v = vec!["a", "b", "c"];
let v = v.iter().map(|s| s.to_string()).collect::<Vec<_>>();
assert!(output(&v).is_ok());
}
#[test]
fn test_just_find_onepass() {
let word = "FOCUS";
let v = find_onepass(word, RhymeStyle::Syllabic, RhymeType::Rhyme, 2, true, None);
println!("{:?}", v);
}
#[test]
fn test_just_find_onepass_vowel() {
let word = "FOCUS";
let v = find_onepass(word, RhymeStyle::Vowel, RhymeType::Rhyme, 2, true, None);
println!("{:?}", v);
}
#[test]
fn test_just_find_onepass_conso() {
let word = "FOCUS";
let v = find_onepass(word, RhymeStyle::Consonant, RhymeType::Rhyme, 2, true, None);
println!("{:?}", v);
}
}