Skip to main content

greek_syllables/
lib.rs

1use unicode_segmentation::UnicodeSegmentation;
2use Accent::*;
3use Breathing::*;
4
5// Split a sequence of unicode Greek characters into syllables.
6// Characters can be accented. Characters can be composed as NFD or NFC.
7#[inline]
8pub fn syllables<'a>(word: &'a str) -> Vec<&'a str> {
9    let mut syllables = Vec::<&'a str>::new();
10    let mut state = Syllable::Ending;
11    let mut end: usize = word.len();
12    let mut last: usize = end;
13    let mut prev: char = 0 as char;
14    let mut prev_prev: char = 0 as char;
15    for (i, element) in UnicodeSegmentation::grapheme_indices(word, true).rev() {
16        let (c, _, vowel, _, _, diaeresis) = categorise(element);
17        if c == 0 as char {
18            state = Syllable::Ending;
19            if last == end {
20                syllables.push(&word[i..end]);
21            } else {
22                syllables.push(&word[last..end]);
23                if last > i {
24                    syllables.push(&word[i..last]);
25                }
26            }
27            end = i;
28            last = i;
29            prev = c;
30            continue;
31        }
32        match state {
33            Syllable::Ending => {
34                // Consume consonants until we consume a vowel.
35                if vowel {
36                    if diaeresis {
37                        syllables.push(&word[i..end]);
38                        end = i;
39                    }
40                    state = Syllable::Starting;
41                }
42            }
43            Syllable::Starting => {
44                // Complete syllable with dipthong or preceeding consonant.
45                if is_dipthong(c, prev) {
46                    // Keep reading
47                } else if vowel {
48                    syllables.push(&word[last..end]);
49                    end = last;
50                } else {
51                    state = Syllable::Restarting;
52                }
53            }
54            Syllable::Restarting => {
55                // Probably start of a new syllable, but some consonant
56                // clusters have special rules for splitting.
57                if vowel {
58                    syllables.push(&word[last..end]);
59                    end = last;
60                    state = Syllable::Starting;
61                } else if c == 'σ' && prev == 'τ' && prev_prev == 'ρ' {
62                    syllables.push(&word[i..end]);
63                    end = i;
64                    state = Syllable::Ending;
65                } else if joinable_consonant(c, prev) {
66                    prev_prev = prev;
67                } else {
68                    syllables.push(&word[last..end]);
69                    end = last;
70                    state = Syllable::Ending;
71                }
72            }
73        }
74        prev = c;
75        last = i;
76    }
77    if end != 0 {
78        syllables.push(&word[0..end]);
79    }
80
81    syllables.reverse();
82    return syllables;
83}
84
85#[derive(Debug)]
86enum Syllable {
87    Ending,
88    Starting,
89    Restarting,
90}
91
92#[inline]
93fn is_dipthong(a: char, b: char) -> bool {
94    match (a, b) {
95        ('α', 'ι') => true,
96        ('ε', 'ι') => true,
97        ('ο', 'ι') => true,
98        ('υ', 'ι') => true,
99        ('α', 'υ') => true,
100        ('ε', 'υ') => true,
101        ('ο', 'υ') => true,
102        ('η', 'υ') => true,
103        _ => false,
104    }
105}
106
107#[inline]
108fn joinable_consonant(a: char, b: char) -> bool {
109    match (a, b) {
110        ('β', 'δ') => true,
111        ('β', 'λ') => true,
112        ('β', 'ρ') => true,
113        ('γ', 'λ') => true,
114        ('γ', 'ν') => true,
115        ('γ', 'ρ') => true,
116        ('δ', 'ρ') => true,
117        ('θ', 'λ') => true,
118        ('θ', 'ν') => true,
119        ('θ', 'ρ') => true,
120        ('κ', 'λ') => true,
121        ('κ', 'ν') => true,
122        ('κ', 'ρ') => true,
123        ('κ', 'τ') => true,
124        ('μ', 'ν') => true,
125        ('π', 'λ') => true,
126        ('π', 'ν') => true,
127        ('π', 'ρ') => true,
128        ('π', 'τ') => true,
129        ('σ', 'β') => true,
130        ('σ', 'θ') => true,
131        ('σ', 'κ') => true,
132        ('σ', 'μ') => true,
133        ('σ', 'π') => true,
134        ('σ', 'τ') => true,
135        ('σ', 'φ') => true,
136        ('σ', 'χ') => true,
137        ('τ', 'ρ') => true,
138        ('φ', 'θ') => true,
139        ('φ', 'λ') => true,
140        ('φ', 'ρ') => true,
141        ('χ', 'λ') => true,
142        ('χ', 'ρ') => true,
143        _ => false,
144    }
145}
146
147#[derive(Debug, PartialEq)]
148pub enum Accent {
149    Acute,
150    Circumflex,
151    Grave,
152    Unaccented,
153}
154
155#[derive(Debug, PartialEq)]
156pub enum Breathing {
157    Rough,
158    Smooth,
159    None,
160}
161
162#[inline]
163fn categorise(c: &str) -> (char, &'static str, bool, Breathing, Accent, bool) {
164    match c {
165        "Α" | "α" => {
166            return ('α', "a", true, None, Unaccented, false);
167        }
168        "Ε" | "ε" => {
169            return ('ε', "e", true, None, Unaccented, false);
170        }
171        "Η" | "η" => {
172            return ('η', "e", true, None, Unaccented, false);
173        }
174        "Ι" | "ι" => {
175            return ('ι', "i", true, None, Unaccented, false);
176        }
177        "Ο" | "ο" => {
178            return ('ο', "o", true, None, Unaccented, false);
179        }
180        "Υ" | "υ" => {
181            return ('υ', "u", true, None, Unaccented, false);
182        }
183        "Ω" | "ω" => {
184            return ('ω', "o", true, None, Unaccented, false);
185        }
186        "Ϋ" | "ϋ" => {
187            return ('υ', "υ", true, None, Unaccented, true);
188        }
189        "Ϊ" | "ϊ" => {
190            return ('ι', "i", true, None, Unaccented, true);
191        }
192        "ᾶ" => {
193            return ('ᾶ', "a", true, None, Circumflex, false);
194        }
195        "ῆ" => {
196            return ('ῆ', "e", true, None, Circumflex, false);
197        }
198        "ῖ" => {
199            return ('ῖ', "i", true, None, Circumflex, false);
200        }
201        "ῶ" => {
202            return ('ῶ', "o", true, None, Circumflex, false);
203        }
204        "Ά" | "Α\u{301}" | "ά" | "α\u{301}" => {
205            return ('α', "a", true, Smooth, Acute, false);
206        }
207        "Έ" | "Ε\u{301}" | "έ" | "ε\u{301}" => {
208            return ('ε', "e", true, Smooth, Acute, false);
209        }
210        "Ή" | "Η\u{301}" | "ή" | "η\u{301}" => {
211            return ('η', "e", true, Smooth, Acute, false);
212        }
213        "Ί" | "Ι\u{301}" | "ί" | "ι\u{301}" => {
214            return ('ι', "i", true, Smooth, Acute, false);
215        }
216        "Ό" | "Ο\u{301}" | "ό" | "ο\u{301}" => {
217            return ('ο', "o", true, Smooth, Acute, false);
218        }
219        "Ύ" | "Υ\u{301}" | "ύ" | "υ\u{301}" => {
220            return ('υ', "u", true, Smooth, Acute, false);
221        }
222        "Ώ" | "Ω\u{301}" | "ώ" | "ω\u{301}" => {
223            return ('ω', "o", true, Smooth, Acute, false);
224        }
225        "Ὰ" | "Α\u{300}" | "ὰ" | "α\u{300}" => {
226            return ('α', "a", true, Smooth, Grave, false);
227        }
228        "Ὲ" | "Ε\u{300}" | "ὲ" | "ε\u{300}" => {
229            return ('ε', "e", true, Smooth, Grave, false);
230        }
231        "Ὴ" | "Η\u{300}" | "ὴ" | "η\u{300}" => {
232            return ('η', "e", true, Smooth, Grave, false);
233        }
234        "Ὶ" | "Ι\u{300}" | "ὶ" | "ι\u{300}" => {
235            return ('ι', "i", true, Smooth, Grave, false);
236        }
237        "Ὸ" | "Ο\u{300}" | "ὸ" | "ο\u{300}" => {
238            return ('ο', "o", true, Smooth, Grave, false);
239        }
240        "Ὺ" | "Υ\u{300}" | "ὺ" | "υ\u{300}" => {
241            return ('υ', "u", true, Smooth, Grave, false);
242        }
243        "Ὼ" | "Ω\u{300}" | "ὼ" | "ω\u{300}" => {
244            return ('ω', "o", true, Smooth, Grave, false);
245        }
246        "Ἀ" | "Α\u{313}" | "ἀ" | "α\u{313}" => {
247            return ('α', "a", true, Smooth, Unaccented, false);
248        }
249        "Ἐ" | "Ε\u{313}" | "ἐ" | "ε\u{313}" => {
250            return ('ε', "e", true, Smooth, Unaccented, false);
251        }
252        "Ἠ" | "Η\u{313}" | "ἠ" | "η\u{313}" => {
253            return ('η', "e", true, Smooth, Unaccented, false);
254        }
255        "Ἰ" | "Ι\u{313}" | "ἰ" | "ι\u{313}" => {
256            return ('ι', "i", true, Smooth, Unaccented, false);
257        }
258        "Ὀ" | "Ο\u{313}" | "ὀ" | "ο\u{313}" => {
259            return ('ο', "o", true, Smooth, Unaccented, false);
260        }
261        "Υ\u{313}" | "ὐ" | "υ\u{313}" => {
262            return ('υ', "u", true, Smooth, Unaccented, false);
263        }
264        "Ὠ" | "Ω\u{313}" | "ὠ" | "ω\u{313}" => {
265            return ('ω', "o", true, Smooth, Unaccented, false);
266        }
267        "Ἁ" | "ἁ" => {
268            return ('α', "a", true, Rough, Unaccented, false);
269        }
270        "Ἑ" | "ἑ" => {
271            return ('ε', "e", true, Rough, Unaccented, false);
272        }
273        "Ἡ" | "ἡ" => {
274            return ('η', "e", true, Rough, Unaccented, false);
275        }
276        "Ἱ" | "ἱ" => {
277            return ('ι', "i", true, Rough, Unaccented, false);
278        }
279        "Ὁ" | "ὁ" => {
280            return ('ο', "i", true, Rough, Unaccented, false);
281        }
282        "Ὑ" | "ὑ" => {
283            return ('υ', "u", true, Rough, Unaccented, false);
284        }
285        "Ὡ" | "ὡ" => {
286            return ('ω', "o", true, Rough, Unaccented, false);
287        }
288        "Ἄ" | "ἄ" => {
289            return ('α', "a", true, Smooth, Acute, false);
290        }
291        "Ἔ" | "ἔ" | "ε\u{313}\u{301}" | "ε\u{301}\u{313}" | "Ε\u{313}\u{301}" | "Ε\u{301}\u{313}" => {
292            return ('ε', "e", true, Smooth, Acute, false);
293        }
294        "Ἤ" | "ἤ" | "η\u{313}\u{301}" | "η\u{301}\u{313}" | "Η\u{313}\u{301}" | "Η\u{301}\u{313}" => {
295            return ('η', "e", true, Smooth, Acute, false);
296        }
297        "Ἴ" | "ἴ" | "ι\u{313}\u{301}" | "ι\u{301}\u{313}" | "Ι\u{313}\u{301}" | "Ι\u{301}\u{313}" => {
298            return ('ι', "i", true, Smooth, Acute, false);
299        }
300        "Ὄ" | "ὄ" | "ο\u{313}\u{301}" | "ο\u{301}\u{313}" | "Ο\u{313}\u{301}" | "Ο\u{301}\u{313}" => {
301            return ('ο', "o", true, Smooth, Acute, false);
302        }
303        "ὔ" | "υ\u{313}\u{301}" | "υ\u{301}\u{313}" => {
304            return ('υ', "u", true, Smooth, Acute, false);
305        }
306        "Ὤ" | "ὤ" | "Ω\u{313}\u{301}" | "Ω\u{301}\u{313}" | "ω\u{313}\u{301}" | "ω\u{301}\u{313}" => {
307            return ('ω', "o", true, Smooth, Acute, false);
308        }
309        "ἅ" | "α\u{301}\u{314}" | "α\u{314}\u{301}" | "Ἅ" | "Α\u{301}\u{314}" | "Α\u{314}\u{301}" => {
310            return ('α', "a", true, Rough, Acute, false);
311        }
312        "ἕ" | "ε\u{301}\u{314}" | "ε\u{314}\u{301}" | "Ἕ" | "Ε\u{301}\u{314}" | "Ε\u{314}\u{301}" => {
313            return ('ε', "e", true, Rough, Acute, false);
314        }
315        "ἥ" | "η\u{301}\u{314}" | "η\u{314}\u{301}" | "Ἥ" | "Η\u{301}\u{314}" | "Η\u{314}\u{301}" => {
316            return ('η', "e", true, Rough, Acute, false);
317        }
318        "ἵ" | "ι\u{301}\u{314}" | "ι\u{314}\u{301}" | "Ἵ" | "Ι\u{301}\u{314}" | "Ι\u{314}\u{301}" => {
319            return ('ι', "i", true, Rough, Acute, false);
320        }
321        "ὅ" | "ο\u{301}\u{314}" | "ο\u{314}\u{301}" | "Ὅ" | "Ο\u{301}\u{314}" | "Ο\u{314}\u{301}" => {
322            return ('ο', "o", true, Rough, Acute, false);
323        }
324        "ὕ" | "υ\u{301}\u{314}" | "υ\u{314}\u{301}" | "Ὕ" | "Υ\u{301}\u{314}" | "Υ\u{314}\u{301}" => {
325            return ('υ', "u", true, Rough, Acute, false);
326        }
327        "ὥ" | "ω\u{301}\u{314}" | "ω\u{314}\u{301}" | "Ὥ" | "Ω\u{301}\u{314}" | "Ω\u{314}\u{301}" => {
328            return ('ω', "o", true, Rough, Acute, false);
329        }
330        "Ἂ" | "ἂ" => {
331            return ('α', "a", true, Smooth, Grave, false);
332        }
333        "Ἒ" | "ἒ" => {
334            return ('ε', "e", true, Smooth, Grave, false);
335        }
336        "Ἢ" | "ἢ" => {
337            return ('η', "e", true, Smooth, Grave, false);
338        }
339        "Ἲ" | "ἲ" => {
340            return ('ι', "i", true, Smooth, Grave, false);
341        }
342        "Ὂ" | "ὂ" => {
343            return ('ο', "o", true, Smooth, Grave, false);
344        }
345        "ὒ" => {
346            return ('υ', "u", true, Smooth, Grave, false);
347        }
348        "Ὢ" | "ὢ" => {
349            return ('ω', "o", true, Smooth, Grave, false);
350        }
351        "Ἃ" | "ἃ" => {
352            return ('α', "a", true, Rough, Grave, false);
353        }
354        "Ἓ" | "ἓ" => {
355            return ('ε', "e", true, Rough, Grave, false);
356        }
357        "Ἣ" | "ἣ" => {
358            return ('η', "e", true, Rough, Grave, false);
359        }
360        "Ἳ" | "ἳ" => {
361            return ('ι', "i", true, Rough, Grave, false);
362        }
363        "Ὃ" | "ὃ" => {
364            return ('ο', "o", true, Rough, Grave, false);
365        }
366        "Ὓ" | "ὓ" => {
367            return ('υ', "u", true, Rough, Grave, false);
368        }
369        "Ὣ" | "ὣ" => {
370            return ('ω', "o", true, Rough, Grave, false);
371        }
372        "Β" | "β" => {
373            return ('β', "b", false, None, Unaccented, false);
374        }
375        "Γ" | "γ" => {
376            return ('γ', "g", false, None, Unaccented, false);
377        }
378        "Δ" | "δ" => {
379            return ('δ', "d", false, None, Unaccented, false);
380        }
381        "Ζ" | "ζ" => {
382            return ('ζ', "z", false, None, Unaccented, false);
383        }
384        "Θ" | "θ" => {
385            return ('θ', "th", false, None, Unaccented, false);
386        }
387        "Κ" | "κ" => {
388            return ('κ', "k", false, None, Unaccented, false);
389        }
390        "Λ" | "λ" => {
391            return ('λ', "l", false, None, Unaccented, false);
392        }
393        "Μ" | "μ" => {
394            return ('μ', "m", false, None, Unaccented, false);
395        }
396        "Ν" | "ν" => {
397            return ('ν', "n", false, None, Unaccented, false);
398        }
399        "Ξ" | "ξ" => {
400            return ('ξ', "x", false, None, Unaccented, false);
401        }
402        "Π" | "π" => {
403            return ('π', "p", false, None, Unaccented, false);
404        }
405        "Ρ" | "ρ" => {
406            return ('ρ', "r", false, None, Unaccented, false);
407        }
408        "Σ" | "σ" | "ς" => {
409            return ('σ', "s", false, None, Unaccented, false);
410        }
411        "Τ" | "τ" => {
412            return ('τ', "t", false, None, Unaccented, false);
413        }
414        "Φ" | "φ" => {
415            return ('φ', "f", false, None, Unaccented, false);
416        }
417        "Χ" | "χ" => {
418            return ('χ', "ch", false, None, Unaccented, false);
419        }
420        "Ψ" | "ψ" => {
421            return ('ψ', "ps", false, None, Unaccented, false);
422        }
423        _ => (0 as char, "", false, None, Unaccented, false),
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_categorise() {
433        let (gc, c, vowel, breathing, accent, diaeresis) = categorise("α");
434        assert_eq!(c, "a");
435        assert_eq!(gc, 'α');
436        assert_eq!(vowel, true);
437        assert_eq!(breathing, None);
438        assert_eq!(accent, Unaccented);
439        assert_eq!(diaeresis, false);
440
441        let (gc, c, vowel, breathing, accent, _) = categorise("β");
442        assert_eq!(c, "b");
443        assert_eq!(gc, 'β');
444        assert_eq!(vowel, false);
445        assert_eq!(breathing, None);
446        assert_eq!(accent, Unaccented);
447
448        let (gc, c, vowel, breathing, accent, _) = categorise("ὔ");
449        assert_eq!(c, "u");
450        assert_eq!(gc, 'υ');
451        assert_eq!(vowel, true);
452        assert_eq!(breathing, Smooth);
453        assert_eq!(accent, Acute);
454
455        let (gc, c, vowel, breathing, accent, _) = categorise("Β");
456        assert_eq!(c, "b");
457        assert_eq!(gc, 'β');
458        assert_eq!(vowel, false);
459        assert_eq!(breathing, None);
460        assert_eq!(accent, Unaccented);
461    }
462
463    #[test]
464    fn test_invalid_characters() {
465        // Invalid ascii found
466        assert_eq!(syllables("σaσ"), ["σ", "a", "σ"]);
467        assert_eq!(syllables("eχει"), ["e", "χει"]);
468        assert_eq!(syllables("ἔχεi"), ["ἔ", "χε", "i"]);
469        assert_eq!(syllables("ἔχeι"), ["ἔχ", "e", "ι"]);
470    }
471
472    #[test]
473    fn test_basic_words() {
474        // NFC
475        assert_eq!(syllables("αα"), ["α", "α"]);
476        assert_eq!(syllables("καα"), ["κα", "α"]);
477        assert_eq!(syllables("αλα"), ["α", "λα"]);
478        assert_eq!(syllables("ἄμα"), ["ἄ", "μα"]);
479        assert_eq!(syllables("ἀλά"), ["ἀ", "λά"]);
480        assert_eq!(syllables("ἀλὰ"), ["ἀ", "λὰ"]);
481        assert_eq!(syllables("ἈΛΆ"), ["Ἀ", "ΛΆ"]);
482        assert_eq!(syllables("χριστος"), ["χρι", "στος"]);
483        assert_eq!(syllables("χρίστος"), ["χρί", "στος"]);
484        assert_eq!(syllables("περιπα"), ["πε", "ρι", "πα"]);
485        assert_eq!(syllables("τραγος"), ["τρα", "γος"]);
486        assert_eq!(syllables("στρατιοτης"), ["στρα", "τι", "ο", "της"]);
487        assert_eq!(syllables("πιστευω"), ["πι", "στευ", "ω"]);
488        assert_eq!(syllables("γυναικός"), ["γυ", "ναι", "κός"]);
489        assert_eq!(syllables("φυω"), ["φυ", "ω"]);
490        assert_eq!(syllables("σσσ"), ["σσσ"]);
491        assert_eq!(syllables("μωϋσῆν"), ["μω", "ϋ", "σῆν"]);
492        assert_eq!(syllables("ὄσπριον"), ["ὄ", "σπρι", "ον"]);
493        assert_eq!(syllables("ὁσία"), ["ὁ", "σί", "α"]);
494        assert_eq!(syllables("ὅτου"), ["ὅ", "του"]);
495        assert_eq!(syllables("ἥτις"), ["ἥ", "τις"]);
496        assert_eq!(syllables("αἵτινες"), ["αἵ", "τι", "νες"]);
497        assert_eq!(syllables("οἵτινες"), ["οἵ", "τι", "νες"]);
498        assert_eq!(syllables("περιεπατήσαμεν"), ["πε", "ρι", "ε", "πα", "τή", "σα", "μεν"]);
499
500        // NFD
501        assert_eq!(syllables("ἀετός"), ["ἀ", "ε", "τός"]);
502        assert_eq!(syllables("ἀετὸν"), ["ἀ", "ε", "τὸν"]);
503        assert_eq!(syllables("ὥσπερ"), ["ὥ", "σπερ"]); // TODO: Is this correct?
504        assert_eq!(syllables("ἔχει"), ["ἔ", "χει"]);
505    }
506}