1use std::borrow::Cow;
2
3use itertools::Itertools;
4
5use crate::case::Case::Upper;
6use crate::char_ext::CharExt;
7use crate::{CaseIterExt, Dialect};
8
9#[derive(Debug, PartialEq)]
10pub enum InitialSound {
11 Vowel,
12 Consonant,
13 Either,
14}
15
16pub fn starts_with_vowel(word: &[char], dialect: Dialect) -> Option<InitialSound> {
22 if word.is_empty() {
23 return None;
24 }
25
26 if matches!(word, ['L', 'E', 'D'] | ['S', 'Q', 'L'] | ['U', 'R', 'L']) {
27 return Some(InitialSound::Either);
28 }
29
30 let word = {
35 let word_casing = word.get_casing_unfiltered();
36 match word_casing.as_slice() {
37 [Some(first_char_case), Some(Upper), ..] => {
39 &word[0..word_casing
40 .iter()
41 .position(|c| *c != Some(*first_char_case))
42 .unwrap_or(word.len())]
43 }
44 _ => word,
46 }
47 };
48
49 let is_likely_initialism = word.iter().all(|c| !c.is_alphabetic() || c.is_uppercase());
50
51 if word.len() == 1 || (is_likely_initialism && !is_likely_acronym(word)) {
52 return Some(
53 if matches!(
54 word[0].to_ascii_uppercase(),
55 'A' | 'E' | 'F' | 'H' | 'I' | 'L' | 'M' | 'N' | 'O' | 'R' | 'S' | 'X'
56 ) {
57 InitialSound::Vowel
58 } else {
59 InitialSound::Consonant
60 },
61 );
62 }
63
64 let word = to_lower_word(word);
65 let word = word.as_ref();
66
67 if matches!(word, ['u', 'b', 'i', ..]) {
68 return Some(InitialSound::Either);
69 }
70
71 if matches!(word, ['m', 'p', digit, ..] if digit.is_ascii_digit()) {
74 return Some(InitialSound::Vowel);
75 }
76
77 if matches!(word, ['n', 'p', 'm']) {
79 return Some(InitialSound::Vowel);
80 }
81
82 if matches!(word, ['e', 'u', 'l', 'e', ..]) {
83 return Some(InitialSound::Vowel);
84 }
85
86 if matches!(
87 word,
88 ['u', 'k', ..]
89 | ['u', 'd', 'e', ..] | ['e', 'u', 'p', 'h', ..]
91 | ['e', 'u', 'g' | 'l' | 'c', ..]
92 | ['o', 'n', 'e', ..]
93 | ['o', 'n', 'c', 'e']
94 ) {
95 return Some(InitialSound::Consonant);
96 }
97
98 if matches!(
99 word,
100 ['h', 'o', 'u', 'r', ..]
101 | ['u', 'n', 'i', 'n' | 'm', ..]
102 | ['u', 'n', 'a' | 'u', ..]
103 | ['u', 'r', 'b', ..]
104 | ['i', 'n', 't', ..]
105 ) {
106 return Some(InitialSound::Vowel);
107 }
108
109 if matches!(word, ['h', 'e', 'r', 'b', ..] if dialect == Dialect::American || dialect == Dialect::Canadian)
110 {
111 return Some(InitialSound::Vowel);
112 }
113
114 if matches!(word, ['u', 'n' | 's', 'i' | 'a' | 'u', ..]) {
115 return Some(InitialSound::Consonant);
116 }
117
118 if matches!(word, ['u', 'n', ..]) {
119 return Some(InitialSound::Vowel);
120 }
121
122 if matches!(word, ['u', 'r', 'g', ..]) {
123 return Some(InitialSound::Vowel);
124 }
125
126 if matches!(word, ['u', 't', 't', ..]) {
127 return Some(InitialSound::Vowel);
128 }
129
130 if matches!(
131 word,
132 ['u', 't' | 'r' | 'n', ..] | ['e', 'u', 'r', ..] | ['u', 'w', ..] | ['u', 's', 'e', ..]
133 ) {
134 return Some(InitialSound::Consonant);
135 }
136
137 if matches!(word, ['o', 'n', 'e', 'a' | 'e' | 'i' | 'u', 'l' | 'd', ..]) {
138 return Some(InitialSound::Vowel);
139 }
140
141 if matches!(word, ['o', 'n', 'e', 'a' | 'e' | 'i' | 'u' | '-' | 's', ..]) {
142 return Some(InitialSound::Consonant);
143 }
144
145 if matches!(
146 word,
147 ['s', 'o', 's']
148 | ['r', 'z', ..]
149 | ['n', 'g', ..]
150 | ['n', 'v', ..]
151 | ['x', 'b', 'o', 'x']
152 | ['h', 'e', 'i', 'r', ..]
153 | ['h', 'o', 'n', 'o', 'r', ..]
154 | ['h', 'o', 'n', 'e', 's', ..]
155 ) {
156 return Some(InitialSound::Vowel);
157 }
158
159 if matches!(
160 word,
161 ['j', 'u' | 'o', 'n', ..] | ['j', 'u', 'r', 'a' | 'i' | 'o', ..]
162 ) {
163 return Some(InitialSound::Consonant);
164 }
165
166 if matches!(word, ['x', '-' | '\'' | '.' | 'o' | 's', ..]) {
167 return Some(InitialSound::Vowel);
168 }
169
170 if word[0].is_vowel() {
171 return Some(InitialSound::Vowel);
172 }
173
174 Some(InitialSound::Consonant)
175}
176
177fn to_lower_word(word: &[char]) -> Cow<'_, [char]> {
178 if word.iter().any(|c| c.is_uppercase()) {
179 Cow::Owned(
180 word.iter()
181 .flat_map(|c| c.to_lowercase())
182 .collect::<Vec<_>>(),
183 )
184 } else {
185 Cow::Borrowed(word)
186 }
187}
188
189fn is_likely_acronym(word: &[char]) -> bool {
190 fn word_contains_false_positive_sequence(word: &[char]) -> bool {
192 let likely_false_positive_sequences = [['V', 'C']];
193 for fp_sequence in likely_false_positive_sequences {
194 if word
195 .windows(fp_sequence.len())
196 .any(|subslice| subslice == fp_sequence)
197 {
198 return true;
199 }
200 }
201 false
202 }
203
204 const MIN_LEN: usize = 3;
206
207 if let Some(first_chars) = word.get(..MIN_LEN)
208 && first_chars.iter().copied().all(char::is_alphabetic)
210 && !word_contains_false_positive_sequence(word)
211 {
212 let vowel_map = first_chars
213 .iter()
214 .map(CharExt::is_vowel)
215 .collect_array::<MIN_LEN>()
216 .unwrap();
217 matches!(vowel_map, [false, true, false] | [false, true, true])
218 } else {
219 false
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_npm_4139() {
229 assert_eq!(
230 starts_with_vowel(&['n', 'p', 'm'].to_vec(), Dialect::American),
231 Some(InitialSound::Vowel)
232 );
233 }
234}