1#[derive(Default)]
11pub struct Hyphenator {
12 data: Vec<u8>,
18 patterns: trie::Trie,
19}
20
21pub trait LowerCaser {
23 fn to_lower_case(&self, c: char) -> Option<char>;
26}
27
28#[derive(Default)]
31pub struct AsciiLowerCaser {}
32
33impl LowerCaser for AsciiLowerCaser {
34 fn to_lower_case(&self, c: char) -> Option<char> {
35 if c.is_ascii_alphabetic() {
36 Some(c.to_ascii_lowercase())
37 } else {
38 None
39 }
40 }
41}
42
43impl Hyphenator {
44 pub fn plain_tex_en_us() -> Self {
46 let patterns = include_str!("plain_tex_patterns.txt");
47 let exceptions = include_str!("plain_tex_exceptions.txt");
48 let mut h: Self = Default::default();
49 h.load_patterns(patterns);
50 h.insert_exceptions(exceptions);
51 h
52 }
53 pub fn load_patterns(&mut self, patterns: &str) {
55 let mut empty_value: Option<trie::Value> = None;
57 for pattern in patterns.split_whitespace() {
58 let mut vertex = self.patterns.root();
59 let mut value = &mut empty_value;
60 if pattern.starts_with('.') {
61 (vertex, value) = self.patterns.next(vertex, trie::Edge::StartOfWord);
62 }
63 let data_start = self.data.len();
64 enum State {
65 AfterChar(usize),
70 AfterScore,
71 }
72 let mut state = State::AfterChar(0);
73 for c in pattern.chars() {
74 match c {
75 '0'..='9' => {
76 let num_zeros: u8 = match state {
77 State::AfterChar(mut n) => {
78 while let Some(m) = n.checked_sub(16) {
79 self.data.push(15 * 16);
83 n = m;
84 }
85 n.try_into().expect("n<16 which fits in u8")
86 }
87 State::AfterScore => {
88 self.data.pop();
91 0
92 }
93 };
94 let op: u8 = (c as u32 - '0' as u32).try_into().expect("digits are <= 9");
95 self.data.push(op + num_zeros * 16);
96 state = State::AfterScore;
97 }
98 '.' => {
99 continue;
101 }
102 _ => {
103 (vertex, value) = self.patterns.next(vertex, trie::Edge::Char(c));
106 state = State::AfterChar(match state {
107 State::AfterChar(n) => n + 1,
108 State::AfterScore => 0,
109 });
110 }
111 }
112 }
113 let terminal_op = if pattern.ends_with('.') {
114 (vertex, value) = self.patterns.next(vertex, trie::Edge::EndOfWord);
115 11
116 } else {
117 10
118 };
119 let num_zeros: u8 = match state {
120 State::AfterChar(mut n) => {
121 while let Some(m) = n.checked_sub(16) {
122 self.data.push(15 * 16);
126 n = m;
127 }
128 n.try_into().expect("n<16 which fits in u8")
129 }
130 State::AfterScore => 0,
131 };
132 self.data.push(terminal_op + num_zeros * 16);
133 *value = Some(trie::Value(data_start));
134 }
135 }
136 pub fn insert_exceptions(&mut self, hyphenated_words: &str) {
139 hyphenated_words
140 .lines()
141 .map(|l| l.trim())
142 .filter(|l| !l.is_empty())
143 .for_each(|l| {
144 self.insert_exception(l);
145 });
146 }
147 pub fn insert_exception(&mut self, hyphenated_word: &str) {
150 let mut vertex = self.patterns.root();
151 vertex = self.patterns.next(vertex, trie::Edge::StartOfWord).0;
152 let data_start = self.data.len();
153 let mut word = String::new();
154 let mut indices = vec![0];
155 self.data.push(6);
156 for c in hyphenated_word.chars() {
157 if c == '-' {
158 indices.pop();
159 indices.push(7);
160 self.data.pop();
161 self.data.push(7);
162 } else {
163 vertex = self.patterns.next(vertex, trie::Edge::Char(c)).0;
164 word.push(c);
165 indices.push(6);
166 self.data.push(6);
167 }
168 }
169 self.data.push(10);
170 let value = self.patterns.next(vertex, trie::Edge::EndOfWord).1;
171 *value = Some(trie::Value(data_start));
172 }
173 pub fn hypthenate<L: LowerCaser>(&self, lower_caser: &L, word: &str, target: &mut String) {
175 let mut indices = self.calculate_indices(lower_caser, word);
176 let mut next = indices.next();
177 for (i, c) in word.chars().enumerate() {
178 if next == Some(i) {
179 target.push('-');
180 next = indices.next();
181 }
182 target.push(c);
183 }
184 }
185
186 pub fn calculate_indices<L: LowerCaser>(
188 &self,
189 lower_caser: &L,
190 word: &str,
191 ) -> impl Iterator<Item = usize> {
192 self.calculate_aggregate_scores(lower_caser, word)
193 .into_iter()
194 .enumerate()
195 .filter(|(_, score)| *score % 2 != 0)
196 .map(|(i, _)| i)
197 }
198 fn calculate_aggregate_scores<L: LowerCaser>(&self, lower_caser: &L, word: &str) -> Vec<u8> {
199 let mut scores = vec![0_u8; word.len() + 1];
200 self.for_each_pattern(lower_caser, word, |p| {
201 let mut k = 0;
202 for op in p.data {
203 let num_zeros = op / 16;
204 k += (num_zeros) as usize;
205 let op = op % 16;
206 match op {
207 score @ ..10 => {
208 if scores[p.offset + k] < score {
209 scores[p.offset + k] = score;
210 }
211 k += 1;
212 }
213 _ => {
214 break;
215 }
216 }
217 }
218 });
219 let num_chars = word.chars().count();
220 scores[0] = 0;
222 scores.truncate(num_chars);
224 scores
225 }
226
227 pub fn calculate_explanation<L: LowerCaser>(&self, lower_caser: &L, word: &str) -> Explanation {
229 let lower_cased: String = word
230 .chars()
231 .map(|c| lower_caser.to_lower_case(c).unwrap_or(c))
232 .collect();
233 let mut patterns: Vec<MatchedPattern> = vec![];
235 self.for_each_pattern(lower_caser, word, |p| {
236 let mut scores: Vec<u8> = vec![];
237 let mut it = p.data.iter();
238 let end_of_word = loop {
239 let Some(op) = it.next() else { break false };
240 let num_zeros = op / 16;
241 scores.resize(scores.len() + num_zeros as usize, 0_u8);
242 let op = op % 16;
243 match op {
244 score @ ..10 => {
245 scores.push(score);
246 }
247 _ => {
248 break op == 11;
249 }
250 }
251 };
252 patterns.push(MatchedPattern {
253 start_of_word: p.start_of_word,
254 offset: p.offset,
255 chars: lower_cased
256 .chars()
257 .skip(p.offset)
258 .take(p.num_chars)
259 .collect(),
260 end_of_word,
261 scores,
262 });
263 });
264 Explanation {
265 lower_cased,
266 patterns,
267 aggregate_scores: self.calculate_aggregate_scores(lower_caser, word),
268 }
269 }
270
271 fn for_each_pattern<L: LowerCaser, F: FnMut(Pattern)>(
277 &self,
278 lower_caser: &L,
279 word: &str,
280 mut for_each: F,
281 ) {
282 let mut process =
283 |mut vertex: trie::Vertex, start_of_word: bool, lower: usize, lower_chars: usize| {
284 let mut chars = word[lower..].chars().map(|c| lower_caser.to_lower_case(c));
285 let mut num_chars = 0_usize;
286 loop {
287 num_chars += 1;
288 let edge = match chars.next() {
289 None => trie::Edge::EndOfWord,
290 Some(None) => {
291 return;
294 }
295 Some(Some(c)) => trie::Edge::Char(c),
296 };
297 let pattern;
298 (vertex, pattern) = match self.patterns.next_or(vertex, edge) {
299 None => return,
300 Some(entry) => entry,
301 };
302 let Some(pattern) = pattern else {
303 continue;
304 };
305 for_each(Pattern {
306 start_of_word,
307 offset: lower_chars,
308 num_chars,
309 data: &self.data[pattern.0..],
310 });
311 }
312 };
313 if let Some((vertex, _)) = self
314 .patterns
315 .next_or(self.patterns.root(), trie::Edge::StartOfWord)
316 {
317 process(vertex, true, 0, 0);
318 }
319 let mut lower_chars: usize = 0;
320 let mut lower = 0;
321 while let Some(c) = word[lower..].chars().next() {
322 if lower_caser.to_lower_case(c).is_none() {
323 break;
324 }
325 process(self.patterns.root(), false, lower, lower_chars);
326 lower += c.len_utf8();
327 lower_chars += 1;
328 }
329 }
330}
331
332struct Pattern<'a> {
333 start_of_word: bool,
334 offset: usize,
335 num_chars: usize,
336 data: &'a [u8],
337}
338
339pub fn strip_hyphens(word: &str) -> String {
341 word.chars().filter(|c| *c != '-').collect()
342}
343
344#[derive(Debug, PartialEq)]
346pub struct Explanation {
347 pub lower_cased: String,
349 pub patterns: Vec<MatchedPattern>,
351 pub aggregate_scores: Vec<u8>,
353}
354
355impl Explanation {
356 pub fn is_interesting(&self) -> bool {
362 if !self.aggregate_scores.iter().any(|score| score % 2 == 1) {
364 return false;
365 }
366 self.patterns.iter().any(|p| {
368 p.scores
369 .iter()
370 .zip(self.aggregate_scores[p.offset..].iter())
371 .any(|(&pattern_score, &aggregate_score)| {
372 pattern_score > 0
376 && aggregate_score > 0
377 && pattern_score % 2 != aggregate_score % 2
378 })
380 })
381 }
382}
383
384impl std::fmt::Display for Explanation {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 let n = self.lower_cased.chars().count();
387 write!(f, ".")?;
388 for c in self.lower_cased.chars() {
389 write!(f, " {}", c)?;
390 }
391 writeln!(f, " .")?;
392 for _ in 0..n {
393 write!(f, "--")?;
394 }
395 writeln!(f, "---")?;
396 for pattern in &self.patterns {
397 writeln!(f, "{pattern}")?;
398 }
399 for _ in 0..n {
400 write!(f, "--")?;
401 }
402 writeln!(f, "---")?;
403 for score in &self.aggregate_scores {
404 write!(f, " {score}")?;
405 }
406 writeln!(f)?;
407 for score in &self.aggregate_scores {
408 write!(f, " {}", if score % 2 == 0 { ' ' } else { '-' })?;
409 }
410 writeln!(f)?;
411 write!(f, ".")?;
412 for (i, c) in self.lower_cased.chars().enumerate() {
413 write!(
414 f,
415 "{}{}",
416 if self.aggregate_scores.get(i).unwrap_or(&0) % 2 == 0 {
417 ' '
418 } else {
419 '-'
420 },
421 c
422 )?;
423 }
424 writeln!(f, " .")?;
425 Ok(())
426 }
427}
428
429#[derive(Debug, PartialEq)]
431pub struct MatchedPattern {
432 pub start_of_word: bool,
434 pub offset: usize,
436 pub chars: String,
438 pub end_of_word: bool,
440 pub scores: Vec<u8>,
442}
443
444impl std::fmt::Display for MatchedPattern {
445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446 write!(f, "{}", if self.start_of_word { '.' } else { ' ' })?;
447 for _ in 0..self.offset {
448 write!(f, " ")?;
449 }
450 let mut chars = self.chars.chars();
451 for score in &self.scores {
452 if *score == 0 {
453 write!(f, " {}", chars.next().unwrap_or(' '))?;
454 } else {
455 write!(f, "{}{}", score, chars.next().unwrap_or(' '))?;
456 };
457 }
458 if self.end_of_word {
459 write!(f, " .")?;
460 }
461 Ok(())
462 }
463}
464
465mod trie {
466 use std::collections::HashMap;
467
468 #[derive(Debug, Default)]
469 pub struct Trie {
470 m: HashMap<(Vertex, Edge), (Vertex, Option<Value>)>,
471 next_vertex: Vertex,
472 }
473
474 impl Trie {
475 pub fn root(&self) -> Vertex {
476 Vertex(u32::MAX)
477 }
478 pub fn next_or(&self, current: Vertex, edge: Edge) -> Option<(Vertex, Option<Value>)> {
479 self.m.get(&(current, edge)).copied()
480 }
481 pub fn next(&mut self, current: Vertex, edge: Edge) -> (Vertex, &mut Option<Value>) {
482 let (a, b) = self.m.entry((current, edge)).or_insert_with(|| {
483 let next = self.next_vertex;
484 self.next_vertex = Vertex(self.next_vertex.0 + 1);
485 (next, None)
486 });
487 (*a, b)
488 }
489 }
490
491 #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Copy)]
492 pub struct Vertex(u32);
493
494 #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
495 pub enum Edge {
496 StartOfWord,
497 Char(char),
498 EndOfWord,
499 }
500
501 #[derive(Debug, Clone, Copy)]
502 pub struct Value(pub usize);
503}
504
505#[cfg(test)]
506mod test {
507 use super::*;
508 use pretty_assertions::assert_eq;
509
510 macro_rules! hyphenation_tests {
511 ( $( $name:ident => $expected:literal, )* ) => {
512 $(
513 #[test]
514 #[allow(non_snake_case)]
515 fn $name() {
516 let hyphenator = Hyphenator::plain_tex_en_us();
517 let mut got = String::new();
518 let lower_caser: AsciiLowerCaser = Default::default();
519 hyphenator.hypthenate( &lower_caser, stringify!($name), &mut got);
520 assert_eq!(got, $expected);
521 }
522 )*
523 };
524 }
525
526 hyphenation_tests!(
527 record => "record",
530 hyphenation => "hy-phen-ation",
531 concatenation => "con-cate-na-tion",
532 supercalifragilisticexpialidocious => "su-per-cal-ifrag-ilis-tic-ex-pi-ali-do-cious",
533 bachelor => "bach-e-lor",
534 echelon => "ech-e-lon",
535 toothaches => "toothaches",
536 campfire => "camp-fire",
537 biorhythm => "biorhyth-m",
538 algorithm => "al-go-rith-m",
539 pneumonoultramicroscopicsilicovolcanoconiosis => "p-neu-monoul-tra-mi-cro-scop-ic-sil-i-co-vol-canoco-nio-sis",
540 project => "project",
541 present => "present",
542 table => "ta-ble",
543 Table => "Ta-ble",
544 ach => "ach",
546 Aaronic => "Aa-ron-ic",
547 Abelia => "A-beli-a",
548 William => "William",
549 chaffless => "chaf-f-less",
550 );
551
552 macro_rules! explanation_tests {
553 ( $( $name:ident => ($expected:expr, $want_explain: expr,), )* ) => {
554 $(
555 #[test]
556 #[allow(non_snake_case)]
557 fn $name() {
558 let hyphenator = Hyphenator::plain_tex_en_us();
559 let lower_caser: AsciiLowerCaser = Default::default();
560 let got = hyphenator.calculate_explanation( &lower_caser, stringify!($name));
561 assert_eq!(got, $expected);
562 assert_eq!(format!["{}", got], $want_explain);
563 }
564 )*
565 };
566 }
567
568 explanation_tests!(
569 DifFicult => (Explanation{
570 lower_cased: "difficult".into(),
571 patterns: vec![
572 MatchedPattern{
574 start_of_word: false,
575 offset: 0,
576 chars: "dif".into(),
577 end_of_word: false,
578 scores: vec![0, 1, 0],
579 },
580 MatchedPattern{
582 start_of_word: false,
583 offset: 2,
584 chars: "ff".into(),
585 end_of_word: false,
586 scores: vec![4, 1],
587 },
588 MatchedPattern{
590 start_of_word: false,
591 offset: 3,
592 chars: "fi".into(),
593 end_of_word: false,
594 scores: vec![1, 0],
595 },
596 MatchedPattern{
598 start_of_word: false,
599 offset: 3,
600 chars: "ficu".into(),
601 end_of_word: false,
602 scores: vec![0, 0, 3, 0],
603 },
604 MatchedPattern{
606 start_of_word: false,
607 offset: 7,
608 chars: "lt".into(),
609 end_of_word: false,
610 scores: vec![4, 0],
611 },
612 ],
613 aggregate_scores: vec![0, 1, 4, 1, 0, 3, 0, 4, 0],
614 },
615". d i f f i c u l t .
616---------------------
617 d1i f
618 4f1f
619 1f i
620 f i3c u
621 4l t
622---------------------
623 0 1 4 1 0 3 0 4 0
624 - - -
625. d-i f-f i-c u l t .
626",
627 ),
628 cove => (Explanation{
629 lower_cased: "cove".into(),
630 patterns: vec![
631 MatchedPattern{
633 start_of_word: false,
634 offset: 0,
635 chars: "co".into(),
636 end_of_word: false,
637 scores: vec![1, 0],
638 },
639 MatchedPattern{
641 start_of_word: false,
642 offset: 0,
643 chars: "cov".into(),
644 end_of_word: false,
645 scores: vec![0, 0, 0, 1],
646 },
647 MatchedPattern{
649 start_of_word: false,
650 offset: 0,
651 chars: "cove".into(),
652 end_of_word: false,
653 scores: vec![0, 0, 0, 0, 4],
654 },
655 MatchedPattern{
657 start_of_word: false,
658 offset: 2,
659 chars: "ve".into(),
660 end_of_word: true,
661 scores: vec![4, 0],
662 },
663 ],
664 aggregate_scores: vec![0, 0, 4, 1],
665 },
666". c o v e .
667-----------
668 1c o
669 c o v1
670 c o v e4
671 4v e .
672-----------
673 0 0 4 1
674 -
675. c o v-e .
676",
677 ),
678 antce => (Explanation{
679 lower_cased: "antce".into(),
680 patterns: vec![
681 MatchedPattern{
683 start_of_word: true,
684 offset: 0,
685 chars: "ant".into(),
686 end_of_word: false,
687 scores: vec![0, 0, 0, 4],
688 },
689 MatchedPattern{
691 start_of_word: false,
692 offset: 0,
693 chars: "an".into(),
694 end_of_word: false,
695 scores: vec![0, 2],
696 },
697 MatchedPattern{
699 start_of_word: false,
700 offset: 1,
701 chars: "nt".into(),
702 end_of_word: false,
703 scores: vec![0, 1],
704 },
705 MatchedPattern{
707 start_of_word: false,
708 offset: 2,
709 chars: "tc".into(),
710 end_of_word: false,
711 scores: vec![4, 0],
712 },
713 MatchedPattern{
715 start_of_word: false,
716 offset: 3,
717 chars: "ce".into(),
718 end_of_word: true,
719 scores: vec![2, 0],
720 },
721 ],
722 aggregate_scores: vec![0, 2, 4, 4, 0],
723 },
724". a n t c e .
725-------------
726. a n t4
727 a2n
728 n1t
729 4t c
730 2c e .
731-------------
732 0 2 4 4 0
733
734. a n t c e .
735",
736 ),
737 );
738}