use std::collections::HashMap;
use std::sync::OnceLock;
const DATA: &str = include_str!("hyphen-en.txt");
struct Patterns {
values: HashMap<&'static str, Vec<u8>>,
longest: usize,
exceptions: HashMap<String, Vec<usize>>,
}
fn patterns() -> &'static Patterns {
static PATTERNS: OnceLock<Patterns> = OnceLock::new();
PATTERNS.get_or_init(|| {
let body = DATA.split_once("\npatterns\n").map_or("", |(_, b)| b);
let (list, exceptions) = body.split_once("\nexceptions\n").unwrap_or((body, ""));
let mut values = HashMap::new();
let mut longest = 0;
for p in list.split_whitespace() {
let letters_at = p.char_indices().filter(|(_, c)| !c.is_ascii_digit());
let key_len = letters_at.clone().count();
let mut v = vec![0u8; key_len + 1];
let mut k = 0;
for c in p.chars() {
match c.to_digit(10) {
Some(d) => v[k] = d as u8,
None => k += 1,
}
}
let key: &'static str = if key_len == p.len() { p } else { Box::leak(p.chars().filter(|c| !c.is_ascii_digit()).collect::<String>().into_boxed_str()) };
longest = longest.max(key.len());
values.insert(key, v);
}
let exceptions = exceptions
.split_whitespace()
.map(|w| {
let mut breaks = Vec::new();
let mut n = 0;
for c in w.chars() {
if c == '-' {
breaks.push(n);
} else {
n += 1;
}
}
(w.replace('-', ""), breaks)
})
.collect();
Patterns { values, longest, exceptions }
})
}
pub fn hyphenate(word: &str, left: usize, right: usize) -> Vec<usize> {
let lower = word.to_ascii_lowercase();
let n = lower.len();
if !lower.bytes().all(|b| b.is_ascii_lowercase()) || n < left + right {
return Vec::new();
}
let p = patterns();
let keep = |k: &usize| *k >= left && n - *k >= right;
if let Some(breaks) = p.exceptions.get(&lower) {
return breaks.iter().copied().filter(keep).collect();
}
let w = format!(".{lower}.");
let mut points = vec![0u8; w.len() + 1];
for i in 0..w.len() {
for j in i + 1..=(i + p.longest).min(w.len()) {
if let Some(v) = p.values.get(&w[i..j]) {
for (k, &d) in v.iter().enumerate() {
points[i + k] = points[i + k].max(d);
}
}
}
}
(1..n).filter(|k| points[k + 1] % 2 == 1).filter(keep).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn hyphenated(w: &str) -> String {
let mut out = String::new();
let breaks = hyphenate(w, 2, 3);
for (k, c) in w.chars().enumerate() {
if breaks.contains(&k) {
out.push('-');
}
out.push(c);
}
out
}
#[test]
fn as_tex_hyphenates() {
assert_eq!(hyphenated("demonstration"), "demon-stra-tion");
assert_eq!(hyphenated("hyphenation"), "hy-phen-ation");
assert_eq!(hyphenated("Randomization"), "Ran-dom-iza-tion");
assert_eq!(hyphenated("table"), "ta-ble");
assert_eq!(hyphenated("the"), "the");
}
}