syllabize_es/
lib.rs

1// #![warn(missing_docs)]
2#![feature(test)]
3
4//! Converting Spanish words into syllables, and much more!
5
6extern crate test;
7
8use char_util::can_form_hiatus;
9use std::fmt;
10use std::fmt::Display;
11use std::usize;
12use str_util::is_both_b_or_v;
13use str_util::is_both_s_or_z;
14use str_util::loose_match;
15
16pub mod char_util;
17pub mod str_util;
18pub mod syllable;
19
20use crate::char_util::can_form_triphthong;
21use crate::char_util::combo_type;
22use crate::char_util::ComboType;
23use crate::char_util::IsVowel;
24use crate::str_util::is_consonant_group;
25use crate::syllable::Syllable;
26
27type Result<T> = std::result::Result<T, InvalidWord>;
28
29#[derive(Debug, Clone)]
30struct InvalidWord;
31
32impl fmt::Display for InvalidWord {
33    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
34        write!(f, "invalid word")
35    }
36}
37
38/// Types of stress
39#[derive(PartialEq, Debug)]
40pub enum StressType {
41    /// aguda, stress at the last syllable
42    Oxytone,
43    /// llana or grave, stress at the penultimate syllable
44    Paroxytone,
45    /// esdrújula, stress at the 3rd last syllable
46    Proparoxytone,
47    /// sobresdrújula, stress at the 4th last syllable or earlier
48    Superproparoxytone,
49}
50
51// TODO: Another stress type
52// pub enum StressType {
53//     Prosodic,  // Acento prosódico
54//     Orthographic,  // Acento ortográfico
55// }
56
57#[derive(PartialEq)]
58enum Position {
59    None,
60    Onset,   // Dos, ataque
61    Nucleus, // dOs, nucleo
62    Coda,    // doS
63}
64
65#[derive(PartialEq, Debug)]
66pub enum HiatusType {
67    Simple,
68    Accentual,
69}
70
71#[derive(PartialEq, Debug)]
72pub enum DiphthongType {
73    Rising,     // Creciente
74    Falling,    // Decrescente
75    Homogenous, // Homogéneo o Anticreciente
76}
77
78pub struct Hiatus {
79    pub syllable_index: usize,
80    pub composite: String,
81    pub kind: HiatusType,
82}
83
84pub struct Diphthong {
85    pub syllable_index: usize,
86    pub composite: String,
87    pub kind: DiphthongType,
88}
89
90pub struct Triphthong {
91    pub syllable_index: usize,
92    pub composite: String,
93}
94
95pub struct VowelCombos {
96    pub hiatuses: Vec<Hiatus>,
97    pub diphthongs: Vec<Diphthong>,
98    pub triphthongs: Vec<Triphthong>,
99}
100
101#[derive(Clone, Copy)]
102pub struct RhymeOptions {
103    pub yeismo: bool,
104    pub seseo: bool,
105    pub b_equals_v: bool,
106}
107
108pub fn equal_onset(a: &Syllable, b: &Syllable, opt: &RhymeOptions) -> bool {
109    if a.onset == b.onset {
110        return true;
111    }
112    if opt.seseo {
113        if a.onset == "c" && b.onset == "s" {
114            let a_first_nucleus = a.nucleus.chars().next().unwrap();
115            if a_first_nucleus.is_soft_c_trigger()
116                && loose_match(a.nucleus.as_str(), b.nucleus.as_str())
117            {
118                return true;
119            }
120        } else if a.onset == "s" && b.onset == "c" {
121            let b_first_nucleus = b.nucleus.chars().next().unwrap();
122            if b_first_nucleus.is_soft_c_trigger()
123                && loose_match(a.nucleus.as_str(), b.nucleus.as_str())
124            {
125                return true;
126            }
127        } else if is_both_s_or_z(a.onset.as_str(), b.onset.as_str()) {
128            return true;
129        }
130    }
131    if opt.yeismo {
132        if a.onset == "y" && b.onset == "ll" || a.onset == "ll" && b.onset == "y" {
133            return true;
134        }
135    }
136    if opt.b_equals_v {
137        if is_both_b_or_v(a.onset.as_str(), b.onset.as_str()) {
138            return true;
139        }
140    }
141
142    false
143}
144
145/// A parsed word that contains syllables and stress information
146#[derive(Clone, Debug)]
147pub struct Word {
148    pub syllables: Vec<Syllable>,
149    pub stress_index: usize,
150}
151
152impl Word {
153    pub fn rhyme(&self) -> String {
154        if self.syllables.len() == 0 {
155            return String::new();
156        }
157        let stress_syllable = &self.syllables[self.stress_index];
158        let mut rhyme = stress_syllable.vowels_since_stress();
159        rhyme.push_str(stress_syllable.coda.as_str());
160
161        for i in self.stress_index + 1..self.syllables.len() {
162            rhyme.push_str(self.syllables[i].to_string().as_str())
163        }
164        rhyme
165    }
166
167    pub fn assonant_rhymes_with(&self, other: &Word) -> bool {
168        let this_syllables = &self.syllables[self.stress_index..self.syllables.len()];
169        let that_syllables = &other.syllables[other.stress_index..other.syllables.len()];
170        if this_syllables.len() != that_syllables.len() {
171            return false;
172        }
173        for (i, j) in this_syllables.iter().enumerate() {
174            if i == 0 {
175                let k1 = j.vowels_since_stress();
176                let k2 = that_syllables[i].vowels_since_stress();
177                if k1 != k2 {
178                    if k1.chars().count() == k2.chars().count() {
179                        if k1.chars().count() == 1 {
180                            let m1 = k1.chars().collect::<String>();
181                            let m2 = k2.chars().collect::<String>();
182                            if !loose_match(m1.as_str(), m2.as_str()) {
183                                return false;
184                            }
185                        } else if k1.chars().collect::<String>() != k2.chars().collect::<String>() {
186                            return false;
187                        }
188                    } else {
189                        return false;
190                    }
191                }
192            } else if j.nucleus != that_syllables[i].nucleus {
193                return false;
194            }
195        }
196        true
197    }
198
199    pub fn rhymes_with(&self, other: &Word, opt: Option<RhymeOptions>) -> bool {
200        let opt = opt.unwrap_or(RhymeOptions {
201            seseo: false,
202            yeismo: true,
203            b_equals_v: true,
204        });
205        let this_syllables = &self.syllables[self.stress_index..self.syllables.len()];
206        let that_syllables = &other.syllables[other.stress_index..other.syllables.len()];
207        if this_syllables.len() != that_syllables.len() {
208            return false;
209        }
210        for (i, j) in this_syllables.iter().enumerate() {
211            if i == 0 {
212                let k1 = j.vowels_since_stress();
213                let k2 = that_syllables[i].vowels_since_stress();
214                if this_syllables.len() == 1 {
215                    if !loose_match(&k1, &k2) {
216                        return false;
217                    }
218                    if !opt.seseo && j.coda != that_syllables[i].coda {
219                        return false;
220                    }
221                    if opt.seseo && is_both_s_or_z(j.coda.as_str(), that_syllables[i].coda.as_str())
222                    {
223                        return true;
224                    }
225                    return true;
226                } else if k1 != k2 || j.coda != that_syllables[i].coda {
227                    return false;
228                }
229            } else if !equal_onset(j, &that_syllables[i], &opt) // j.onset != that_syllables[i].onset
230                || j.nucleus != that_syllables[i].nucleus
231                || !(j.coda == that_syllables[i].coda || (opt.seseo && is_both_s_or_z(j.coda.as_str(), that_syllables[i].coda.as_str())))
232            {
233                return false;
234            }
235        }
236        true
237    }
238
239    pub fn vowel_combos(&self) -> VowelCombos {
240        let syllables = &self.syllables;
241        let mut index = 0;
242        let mut hiatuses = vec![];
243        let mut diphthongs = vec![];
244        let mut triphthongs = vec![];
245        while index < syllables.len() {
246            let nucleus_chars: Vec<char> = syllables[index].nucleus.chars().collect();
247            if syllables[index].coda.is_empty()
248                && nucleus_chars.len() == 1
249                && index + 1 < syllables.len()
250                && (syllables[index + 1].onset.is_empty() || syllables[index + 1].onset == "h")
251                && syllables[index + 1].nucleus.chars().count() == 1
252            {
253                let mut composite = syllables[index].nucleus.clone();
254                composite.push_str(syllables[index + 1].nucleus.as_str());
255                hiatuses.push(Hiatus {
256                    syllable_index: index,
257                    composite,
258                    kind: if syllables[index].has_accented_vowel()
259                        || syllables[index + 1].has_accented_vowel()
260                    {
261                        HiatusType::Accentual
262                    } else {
263                        HiatusType::Simple
264                    },
265                });
266            } else if nucleus_chars.len() == 2 {
267                let dp_type: DiphthongType = match combo_type(nucleus_chars[0], nucleus_chars[1]) {
268                    ComboType::Diphthong(t) => t,
269                    _ => panic!("Not a diphthong"),
270                };
271                diphthongs.push(Diphthong {
272                    syllable_index: index,
273                    kind: dp_type,
274                    composite: syllables[index].nucleus.clone(),
275                });
276            } else if syllables[index].nucleus.chars().count() == 3 {
277                triphthongs.push(Triphthong {
278                    syllable_index: index,
279                    composite: syllables[index].nucleus.clone(),
280                });
281            } else if syllables[index].coda.is_empty()
282                && syllables[index].nucleus.chars().count() == 2
283                && index + 1 < syllables.len()
284                && (syllables[index + 1].onset.is_empty() || syllables[index + 1].onset == "h")
285                && syllables[index + 1].nucleus.chars().count() == 1
286            {
287                // ???
288            }
289            index += 1;
290        }
291        VowelCombos {
292            hiatuses,
293            diphthongs,
294            triphthongs,
295        }
296    }
297
298    pub fn stress(&self) -> StressType {
299        let d = self.syllables.len() - 1 - self.stress_index;
300        match d {
301            0 => StressType::Oxytone,
302            1 => StressType::Paroxytone,
303            2 => StressType::Proparoxytone,
304            3.. => StressType::Superproparoxytone,
305            _ => panic!("Invalid stress count {}", d),
306        }
307    }
308
309    pub fn syllabize(&self, delimiter: &str) -> String {
310        return self
311            .syllables
312            .iter()
313            .map(|s| s.to_string())
314            .collect::<Vec<String>>()
315            .join(delimiter);
316    }
317}
318
319impl From<&str> for Word {
320    fn from(item: &str) -> Self {
321        let syllables = to_syllables(item);
322        match syllables {
323            Ok(s) => {
324                let stress_index = identify_stress(&s);
325                Word {
326                    syllables: s,
327                    stress_index,
328                }
329            }
330            Err(_e) => Word {
331                syllables: vec![],
332                stress_index: 0,
333            }
334        }
335    }
336}
337
338impl Display for Word {
339    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
340        let res = self
341            .syllables
342            .iter()
343            .map(|s| s.to_string())
344            .collect::<Vec<String>>()
345            .join("");
346        write!(f, "{}", res)
347    }
348}
349
350
351
352fn to_syllables(word: &str) -> Result<Vec<Syllable>> {
353    if word.is_empty() {
354        return Ok(vec![]);
355    }
356
357    let chars: Vec<char> = word.chars().collect();
358    let word_len = chars.len();
359
360    if word_len == 1 {
361        return Ok(vec![Syllable {
362            onset: "".to_string(),
363            nucleus: chars[0].to_string(),
364            coda: "".to_string(),
365        }]);
366    }
367
368    // Officially the longest word is 12 syllables, here we give some leeway
369    // shaves of 100ns
370    let mut syllables: Vec<Syllable> = Vec::with_capacity(32);
371
372    let mut index = 0;
373    let mut position = Position::None;
374    let mut syllable = Syllable {
375        onset: "".to_string(),
376        nucleus: "".to_string(),
377        coda: "".to_string(),
378    };
379
380    loop {
381        let curr_char = chars[index];
382        if !curr_char.is_vowel() {
383            if position == Position::None || position == Position::Onset {
384                if curr_char == 'y' {
385                    if syllable.onset.is_empty() {
386                        syllable.onset.push(curr_char);
387                        position = Position::Onset;
388                    } else {
389                        syllable.nucleus.push(curr_char);
390                        position = Position::Nucleus
391                    }
392                } else if curr_char == 'q' || curr_char == 'g' {
393                    syllable.onset.push(curr_char);
394                    position = Position::Onset;
395                    index += 1;
396                    if word_len <= index {
397                        return Err(InvalidWord)
398                    }
399                    let next_char = chars[index];
400                    if next_char == 'u' {
401                        index += 1;
402                        if word_len <= index {
403                            return Err(InvalidWord)
404                        }
405                        let after_next_char = chars[index];
406                        if after_next_char == 'i' || after_next_char == 'e' {
407                            syllable.onset.push(next_char);
408                            syllable.nucleus.push(after_next_char);
409                            position = Position::Nucleus;
410                        }
411                    } else {
412                        index -= 1;
413                    }
414                } else {
415                    syllable.onset.push(curr_char);
416                    position = Position::Onset;
417                }
418            } else if position == Position::Nucleus {
419                if curr_char == 'y'
420                    && (index == word_len - 1
421                        || (index + 1 < word_len && !chars[index + 1].is_vowel()))
422                {
423                    syllable.nucleus.push(curr_char);
424                } else if curr_char == 'h' {
425                    index += 1;
426                    if word_len <= index {
427                        return Err(InvalidWord)
428                    }
429                    if index == chars.len() {
430                        syllable.coda.push(curr_char);
431                        position = Position::Coda;
432                    } else {
433                        let next_char = chars[index];
434                        if next_char.is_vowel() {
435                            if syllable.nucleus.chars().count() == 1
436                                && can_form_hiatus(
437                                    syllable.nucleus.chars().next().unwrap(),
438                                    next_char,
439                                )
440                            {
441                                syllables.push(syllable);
442                                syllable = Syllable {
443                                    onset: curr_char.to_string(),
444                                    nucleus: next_char.to_string(),
445                                    coda: "".to_string(),
446                                };
447                                position = Position::Nucleus;
448                            } else {
449                                index += 1;
450                                if index == chars.len() {
451                                    syllable.nucleus.push(curr_char);
452                                    syllable.nucleus.push(next_char);
453                                    position = Position::Coda;
454                                } else {
455                                    let after_next_char = chars[index];
456                                    if after_next_char.is_vowel() {
457                                        if can_form_triphthong(
458                                            syllable.nucleus.chars().next().unwrap(),
459                                            next_char,
460                                            after_next_char,
461                                        ) {
462                                            // Could this happen?
463                                        } else {
464                                            syllables.push(syllable);
465                                            let mut nucleus = next_char.to_string();
466                                            nucleus.push(after_next_char);
467                                            syllable = Syllable {
468                                                onset: curr_char.to_string(),
469                                                nucleus,
470                                                coda: "".to_string(),
471                                            };
472                                            position = Position::Nucleus;
473                                        }
474                                    } else {
475                                        syllable.nucleus.push(curr_char);
476                                        syllable.nucleus.push(next_char);
477                                        syllable.coda.push(after_next_char);
478                                        position = Position::Coda;
479                                    }
480                                }
481                            }
482                        } else {
483                            syllable.coda.push(curr_char);
484                            syllables.push(syllable);
485                            syllable = Syllable {
486                                onset: next_char.to_string(),
487                                nucleus: "".to_string(),
488                                coda: "".to_string(),
489                            };
490                            position = Position::Onset;
491                        }
492                    }
493                } else {
494                    syllable.coda.push(curr_char);
495                    position = Position::Coda;
496                }
497            } else if position == Position::Coda {
498                if curr_char == 'y' {
499                    if syllable.coda.chars().count() == 1 {
500                        if index + 1 < word_len {
501                            if chars[index + 1].is_vowel() {
502                                syllables.push(syllable);
503                                syllable = Syllable {
504                                    onset: curr_char.to_string(),
505                                    nucleus: "".to_string(),
506                                    coda: "".to_string(),
507                                };
508                                position = Position::Onset;
509                            } else {
510                                let onset = syllable.coda.clone();
511                                syllable.coda.clear();
512                                syllables.push(syllable);
513                                syllable = Syllable {
514                                    onset,
515                                    nucleus: curr_char.to_string(),
516                                    coda: "".to_string(),
517                                };
518                                position = Position::Nucleus;
519                            }
520                        }
521                    } else if syllable.coda.chars().count() == 2 {
522                        if is_consonant_group(syllable.coda.as_str()) {
523                            let onset = syllable.coda.clone();
524                            syllable.coda = "".to_string();
525                            syllables.push(syllable);
526                            syllable = Syllable {
527                                onset,
528                                nucleus: curr_char.to_string(),
529                                coda: "".to_string(),
530                            };
531                            position = Position::Nucleus;
532                        } else {
533                            let chars: Vec<char> = syllable.coda.chars().collect();
534                            let onset = chars[1].to_string();
535                            syllable.coda = chars[0].to_string();
536                            syllables.push(syllable);
537                            syllable = Syllable {
538                                onset,
539                                nucleus: curr_char.to_string(),
540                                coda: "".to_string(),
541                            };
542                            position = Position::Nucleus;
543                        }
544                    } else {
545                        syllable.coda.push(curr_char);
546                    }
547                } else {
548                    syllable.coda.push(curr_char);
549                }
550            }
551        } else if position == Position::None || position == Position::Onset {
552            position = Position::Nucleus;
553            syllable.nucleus.push(curr_char);
554        } else if position == Position::Nucleus {
555            if syllable.nucleus.chars().count() == 1 {
556                if can_form_hiatus(syllable.nucleus.chars().next().unwrap(), curr_char) {
557                    syllables.push(syllable);
558                    syllable = Syllable {
559                        onset: "".to_string(),
560                        nucleus: curr_char.to_string(),
561                        coda: "".to_string(),
562                    };
563                } else {
564                    syllable.nucleus.push(curr_char);
565                }
566            } else if syllable.nucleus.chars().count() == 2 {
567                if can_form_triphthong(
568                    syllable.nucleus.chars().next().unwrap(),
569                    syllable.nucleus.chars().nth(1).unwrap(),
570                    curr_char,
571                ) {
572                    syllable.nucleus.push(curr_char);
573                } else {
574                    let last_nucleus = syllable.nucleus.chars().nth(1).unwrap();
575                    if last_nucleus.is_weak_vowel() {
576                        syllable.nucleus = syllable.nucleus.chars().next().unwrap().to_string();
577                        syllables.push(syllable);
578                        let mut last_nucleus = last_nucleus.to_string();
579                        last_nucleus.push(curr_char);
580                        syllable = Syllable {
581                            onset: "".to_string(),
582                            nucleus: last_nucleus,
583                            coda: "".to_string(),
584                        }
585                    } else {
586                        syllables.push(syllable);
587                        syllable = Syllable {
588                            onset: "".to_string(),
589                            nucleus: curr_char.to_string(),
590                            coda: "".to_string(),
591                        }
592                    }
593                }
594                position = Position::Nucleus;
595            }
596        } else if position == Position::Coda {
597            if syllable.coda.chars().count() == 1 {
598                let temp = syllable.coda.clone();
599                syllable.coda = "".to_string();
600                syllables.push(syllable);
601                syllable = Syllable {
602                    onset: temp,
603                    nucleus: curr_char.to_string(),
604                    coda: "".to_string(),
605                }
606            } else if syllable.coda.chars().count() == 2 {
607                let temp: String;
608                if is_consonant_group(syllable.coda.as_str()) {
609                    temp = syllable.coda.clone();
610                    syllable.coda = "".to_string();
611                } else {
612                    temp = syllable.coda.chars().nth(1).unwrap().to_string();
613                    syllable.coda = syllable.coda.chars().next().unwrap().to_string();
614                }
615                syllables.push(syllable);
616                syllable = Syllable {
617                    onset: temp,
618                    nucleus: curr_char.to_string(),
619                    coda: "".to_string(),
620                };
621            } else if syllable.coda.chars().count() == 3 {
622                let temp = syllable.coda.chars().skip(1).collect::<String>();
623                syllable.coda = syllable.coda.chars().next().unwrap().to_string();
624                syllables.push(syllable);
625                syllable = Syllable {
626                    onset: temp,
627                    nucleus: curr_char.to_string(),
628                    coda: "".to_string(),
629                }
630            } else if syllable.coda.chars().count() == 4 {
631                // indexing into &str should be fine because 4 char consonant
632                // clusters would only contain ascii letters, i.e., no `ñ`.
633                let temp = syllable.coda.as_str()[2..4].to_string();
634                syllable.coda = syllable.coda.as_str()[0..2].to_string();
635                syllables.push(syllable);
636                syllable = Syllable {
637                    onset: temp,
638                    nucleus: curr_char.to_string(),
639                    coda: "".to_string(),
640                }
641            }
642            position = Position::Nucleus;
643        }
644
645        index += 1;
646        if index > word_len - 1 {
647            syllables.push(syllable);
648            break;
649        }
650    }
651    Ok(syllables)
652}
653
654fn identify_stress(syllables: &[Syllable]) -> usize {
655    let syllable_count = syllables.len();
656    if syllable_count == 0 || syllable_count == 1 {
657        return 0;
658    }
659    if syllable_count > 1 && syllables[syllable_count - 1].has_accented_vowel() {
660        return syllable_count - 1;
661    }
662    if syllable_count >= 2 && syllables[syllable_count - 2].has_accented_vowel() {
663        return syllable_count - 2;
664    }
665    if syllable_count >= 3 && syllables[syllable_count - 3].has_accented_vowel() {
666        return syllable_count - 3;
667    }
668    if syllable_count >= 4 {
669        let mut index = syllable_count as i8 - 4;
670        while index >= 0 {
671            if syllables[index as usize].has_accented_vowel() {
672                return index as usize;
673            }
674            index -= 1;
675        }
676    }
677
678    let last_syllable = &syllables[syllable_count - 1];
679    if last_syllable.coda.is_empty() {
680        if last_syllable.nucleus.chars().count() == 3 {
681            return syllable_count - 1;
682        }
683    } else {
684        if last_syllable.coda != "n" && last_syllable.coda != "s" {
685            return syllable_count - 1;
686        }
687    }
688
689    syllable_count - 2
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695    use test::Bencher;
696
697    #[test]
698    fn test_hiato() {
699        let word: Word = "lee".into();
700        let vowel_combos = word.vowel_combos();
701        assert_eq!(vowel_combos.hiatuses.len(), 1);
702        assert_eq!(vowel_combos.diphthongs.len(), 0);
703        assert_eq!(vowel_combos.triphthongs.len(), 0);
704    }
705
706    #[test]
707    fn test_rhymes_with() {
708        let word: Word = "vida".into();
709        assert!(word.rhymes_with(&Word::from("frida"), None));
710        assert!(!word.rhymes_with(&Word::from("vía"), None));
711        assert!(word.assonant_rhymes_with(&Word::from("villa")));
712        assert!(word.assonant_rhymes_with(&Word::from("vía")));
713    }
714
715    #[test]
716    fn test_rhymes_with_yeismo() {
717        let word: Word = "callo".into();
718        assert!(word.rhymes_with(
719            &Word::from("cayo"),
720            Some(RhymeOptions {
721                yeismo: true,
722                seseo: false,
723                b_equals_v: true
724            })
725        ));
726        assert!(!word.rhymes_with(
727            &Word::from("cayo"),
728            Some(RhymeOptions {
729                yeismo: false,
730                seseo: false,
731                b_equals_v: true
732            })
733        ));
734    }
735
736    #[test]
737    fn test_rhymes_with_seseo() {
738        let word: Word = "cabeza".into();
739        assert!(word.rhymes_with(
740            &Word::from("besa"),
741            Some(RhymeOptions {
742                yeismo: true,
743                seseo: true,
744                b_equals_v: true
745            })
746        ));
747        assert!(!word.rhymes_with(
748            &Word::from("besa"),
749            Some(RhymeOptions {
750                yeismo: true,
751                seseo: false,
752                b_equals_v: true
753            })
754        ));
755    }
756
757    #[test]
758    fn test_rhymes_with_seseo_end() {
759        let word: Word = "estrés".into();
760        assert!(word.rhymes_with(
761            &Word::from("vez"),
762            Some(RhymeOptions {
763                yeismo: true,
764                seseo: true,
765                b_equals_v: true
766            })
767        ));
768        assert!(!word.rhymes_with(
769            &Word::from("vez"),
770            Some(RhymeOptions {
771                yeismo: true,
772                seseo: false,
773                b_equals_v: true
774            })
775        ));
776    }
777
778    #[test]
779    fn test_rhymes_b_v() {
780        let word: Word = "escribe".into();
781        assert!(word.rhymes_with(
782            &Word::from("declive"),
783            Some(RhymeOptions {
784                yeismo: true,
785                seseo: true,
786                b_equals_v: true
787            })
788        ));
789        assert!(!word.rhymes_with(
790            &Word::from("declive"),
791            Some(RhymeOptions {
792                yeismo: true,
793                seseo: true,
794                b_equals_v: false
795            })
796        ));
797    }
798
799    #[test]
800    fn test_rhymes_with_single_syllable() {
801        let word: Word = "vi".into();
802        assert!(word.rhymes_with(&Word::from("tí"), None));
803    }
804
805    #[test]
806    fn test_rhymes_with_y() {
807        let word: Word = "ley".into();
808        assert!(!word.rhymes_with(&Word::from("é"), None));
809    }
810
811    #[test]
812    fn test_rhymes_no_match() {
813        let word: Word = "vi".into();
814        assert!(!word.rhymes_with(&Word::from("sid"), None));
815    }
816
817    #[test]
818    fn test_diptongo() {
819        let word: Word = "DIALOGO".into();
820        let vowel_combos = word.vowel_combos();
821        assert_eq!(vowel_combos.hiatuses.len(), 0);
822        assert_eq!(vowel_combos.diphthongs.len(), 1);
823        assert_eq!(vowel_combos.triphthongs.len(), 0);
824    }
825
826    #[bench]
827    fn bench_wordify(b: &mut Bencher) {
828        b.iter(|| {
829            let _word: Word = "envergadura".into();
830        });
831    }
832
833    #[bench]
834    fn bench_vowel_combos(b: &mut Bencher) {
835        let word: Word = "envergadura".into();
836        b.iter(|| word.vowel_combos());
837    }
838
839    #[bench]
840    fn bench_rhyme(b: &mut Bencher) {
841        let word: Word = "envergadura".into();
842        b.iter(|| word.rhyme());
843    }
844
845    #[bench]
846    fn bench_syllabize(b: &mut Bencher) {
847        let word: Word = "envergadura".into();
848        b.iter(|| word.syllabize("-"));
849    }
850}
851
852#[cfg(doctest)]
853mod test_readme {
854    macro_rules! external_doc_test {
855        ($x:expr) => {
856            #[doc = $x]
857            unsafe extern "C" {}
858        };
859    }
860
861    external_doc_test!(include_str!("../README.md"));
862}