diff --git a/glossy/src/utils.typ b/glossy/src/utils.typ index 0acab16..ea57668 100644 --- a/glossy/src/utils.typ +++ b/glossy/src/utils.typ @@ -36,3 +36,128 @@ word + "s" } } + +// Determines the appropriate indefinite article ("a" or "an") for a given English word. +// +// Heuristic rules: +// 1. If word looks like an acronym (all uppercase or letters separated by dots): +// - Use "an" if the first letter's name starts with a vowel sound, e.g. A (ay), E (ee), F (eff), H (aitch), I (eye), L (el), M (em), N (en), O (oh), R (ar), S (ess), X (ex). +// - Otherwise "a". +// +// 2. Otherwise, lowercase the word and apply standard rules: +// - Starts with a vowel (a, e, i, o): "an". +// - Starts with "u": +// * If it begins with "uni", "use", "user", or "eu" (e.g. "university", "use", "Europe"), we treat it as a "yoo" sound -> "a". +// * Otherwise -> "an". +// - Special silent 'h' words: if it starts with "hour", "honest", "honor", "honour", or other, use "an". +// Otherwise if starts with 'h', use "a". +// - Default: "a". +// +// Notes: +// - This is a best-effort heuristic and won't be perfect. +// - These are for singular forms. If plural form is requested alongside +// articles, the caller should panic before calling this function. +// +// Returns: +// "a" or "an" +// +// Example: +// __determine_article("RF") -> "an" (Ar-Ef) +// __determine_article("radio") -> "a" +// __determine_article("hour") -> "an" +// __determine_article("honest") -> "an" +// __determine_article("university") -> "a" +// __determine_article("umbrella") -> "an" +// __determine_article("Europe") -> "a" (because we assume "yoo-ruhp") +#let __determine_article(word) = { + // Check input + if word == none or word.trim() == "" { + // If invalid, default to none + return none + } + + // Normalize the word + let upper_word = word.trim() + let lower_word = lower(upper_word) + + // When these letters are spelled out, they get an "an" (i.e. "an RFC") + let acronym_vowels = ( + "A", // "ay" + "E", // "ee" + "F", // "eff" + "H", // "aitch" + "I", // "eye" + "L", // "el" + "M", // "em" + "N", // "en" + "O", // "oh" + "R", // "ar" + "S", // "ess" + "X", // "ex" + ) + + // Helper to detect if word looks like an acronym + let is_acronym = { + // All uppercase or letters separated by non-alphabetical chars + let cleaned = upper_word.replace(".", "").replace("-", "") + cleaned.codepoints().all(c => c == upper(c)) and cleaned.len() > 0 + } + + if is_acronym { + // Use acronym rules + let first_char = upper_word.slice(0,1) + if acronym_vowels.contains(first_char) { + "an" + } else { + "a" + } + } else { + // Non-acronym rules + let first_char = lower_word.slice(0,1) + + // Special cases for words matching a set of known 'silent h' prefixes + let silent_h_words = ( + "heir", // "air" + "herb", // "erb" (in American English) + "homage", // "oh-mij" or "ah-mij" + "honest", // "on-est" + "honor", // "on-or" (American English) + "honour", // "on-our" (British English) + "hour", // "our" + ) + if silent_h_words.any(w => lower_word.starts-with(w)) { + return "an" + } + + // Words starting with 'eu', which has the 'yoo' sound + if lower_word.starts-with("eu") { + return "a" + } + + // Words starting with 'u', that get the 'yoo' sound + if first_char == "u" { + let yoo_sounds = ( + "ubi", // ubiquitous + "uni", // unicycle, uniform, universe, university, unilateral, unique, union, etc. + "usa", // usage, usability + "use", // use, useful, useless + "usu", // usual, usually + "usur", // usurp + "ute", // utensil, uterus + "uti", // utility, utilize, utilization + "uto", // utopia, utopian + ) + if yoo_sounds.any(w => lower_word.starts-with(w)) { + return "a" + } + } + + // Vowel start + if "aeiou".contains(first_char) { + return "an" + } + + // the default case, plus 'h' words not in the silent list + "a" + } +} diff --git a/glossy/tests/determine-article/ref/1.png b/glossy/tests/determine-article/ref/1.png new file mode 100644 index 0000000..d16c201 Binary files /dev/null and b/glossy/tests/determine-article/ref/1.png differ diff --git a/glossy/tests/determine-article/test.typ b/glossy/tests/determine-article/test.typ new file mode 100644 index 0000000..6440e4c --- /dev/null +++ b/glossy/tests/determine-article/test.typ @@ -0,0 +1,289 @@ +#import "/src/utils.typ": * + +#let tests = ( + // Main tests (covers most of English language) + ("Europe","a"), + ("European","a"), + ("Hannah","a"), + ("T-shirt","a"), + ("Xerox","a"), + ("Zebra","a"), + ("aardvark","an"), + ("apple","an"), + ("boat","a"), + ("car","a"), + ("cat","a"), + ("chair","a"), + ("dog","a"), + ("egg","an"), + ("elephant","an"), + ("emu","an"), + ("eucalyptus","a"), + ("eulogy","a"), + ("euphemism","a"), + ("exhausted","an"), + ("ghost","a"), + ("hat","a"), + ("heir","an"), + ("heiress","an"), + ("herb","an"), + ("herbal","an"), + ("historic","a"), + ("history","a"), + ("homage","an"), + ("honest","an"), + ("honestly","an"), + ("honor","an"), + ("honorary","an"), + ("honour","an"), + ("horse","a"), + ("hour","an"), + ("hourglass","an"), + ("house","a"), + ("iris","an"), + ("orange","an"), + ("owl","an"), + ("table","a"), + ("ugly","an"), + ("umbrella","an"), + ("umpire","an"), + ("unicorn","a"), + ("unique","a"), + ("university","a"), + ("urn","an"), + ("usage","a"), + ("user","a"), + ("utensil","a"), + + // Acronym tests + ("ASCII","an"), + ("CFO","a"), + ("F.A.Q.","an"), + ("FBI","an"), + ("GIF","a"), + ("HIV","an"), + ("JPEG","a"), + ("LED","an"), + ("MRI","an"), + ("NATO","an"), + ("PNG","a"), + ("Q&A","a"), + ("RF","an"), + ("RNG","an"), + ("SAS","an"), + ("SMS","an"), + ("SOS","an"), + ("SQL","an"), + ("UFO","a"), + ("URL","a"), + ("VCR","a"), + ("WWW","a"), + ("X.Y.Z.","an"), + ("YAML","a"), + + // Extra tests (courtesy of LLM) + + // Common English words starting with vowels + ("idea","an"), + ("island","an"), + ("offer","an"), + ("option","an"), + ("operation","an"), + ("appletree","an"), + ("iguana","an"), + ("octopus","an"), + ("atom","an"), + ("item","an"), + + // Words starting with vowel but "yoo" sound (thus 'a') + ("universe","a"), + ("unicycle","a"), + ("uniform","a"), + ("union","a"), + ("united","a"), + ("unique","a"), // already tested, reaffirmed + ("usual","a"), + ("useful","a"), + ("utility","a"), + ("utopia","a"), + + // More common vowel words with a/an + ("umbrella","an"), // repeated but reaffirming + ("eggplant","an"), + ("insect","an"), + ("eleven","an"), + ("olive","an"), + ("impulse","an"), + ("ox","an"), + ("aroma","an"), + ("ear","an"), + ("ocean","an"), + + // Words starting with 'h' but pronounced, so 'a' + ("house","a"), // repeated for emphasis + ("hammer","a"), + ("helmet","a"), + ("history","a"), // repeated + ("hero","a"), + ("hill","a"), + ("hobby","a"), + ("holiday","a"), + ("harmony","a"), + ("hover","a"), + + // More silent 'h' words (already in set, but variations) + ("honorific","an"), + ("honorarium","an"), + ("honesties","an"), + ("hourly","an"), + ("heirloom","an"), + + // Words starting with 'e' + 'u' combo with "yoo" sound + ("Europe","a"), // repeated + ("European","a"), // repeated + ("euphoric","a"), + ("eureka","a"), + ("euphemistic","a"), + ("euphonious","a"), + ("eucalyptus","a"), // repeated + ("euthanasia","a"), + ("eugenics","a"), + ("Eurasia","a"), + + // Common consonant-start words + ("book","a"), + ("pen","a"), + ("fork","a"), + ("spoon","a"), + ("knife","a"), + ("ball","a"), + ("tree","a"), + ("river","a"), + ("mountain","a"), + ("cloud","a"), + + // More vowel-start words + ("ostrich","an"), + ("amphibian","an"), + ("eleven","an"), // repeated + ("issue","an"), + ("incense","an"), + ("antenna","an"), + ("echo","an"), + ("aisle","an"), + ("oracle","an"), + ("overcoat","an"), + + // More acronym/initialism tests: + ("ATM","an"), // A = 'ay' + ("EPA","an"), // E = 'ee' + ("FAQ","an"), // F = 'eff', repeated as F.A.Q. + ("HTML","an"), // H = 'aitch' + ("IBM","an"), // I = 'eye' + ("LAN","an"), // L = 'el' + ("NFL","an"), // N = 'en' + ("OSHA","an"), // O = 'oh' + ("RAM","an"), // R = 'ar' + ("SMS","an"), // repeated, S = 'ess' + + // Acronyms starting with consonant sound letters + ("GIF","a"), // G = 'jee', repeated + ("CFO","a"), // C = 'see', repeated (consonant sound 's') + ("CEO","a"), // C = 'see' + ("DNA","a"), // D = 'dee' starts with 'd' consonant sound + ("PGA","a"), // P = 'pee' starts with 'p' consonant sound + ("QVC","a"), // Q = 'kyoo' starts with 'k' consonant sound + ("TNT","a"), // T = 'tee' starts with 't' consonant sound + ("UPS","a"), // U = 'yoo' sound, hence 'a' + ("VIP","a"), // V = 'vee' starts with 'v' consonant sound + ("WIPO","a"), // W = 'double-u' starts with 'd' consonant sound + + // More tricky 'u' words with 'yoo' sound for 'a' + ("unify","a"), + ("unilateral","a"), + ("ubiquitous","a"), // 'yoo-bik...' + ("usurp","a"), + ("usuary","a"), // 'yoo-zhoo-ary' (rare, but treat like usual) + ("uterus","a"), // 'yoo-terus' + ("usher","an"), // 'uh-sher' vowel sound + ("ugly","an"), // repeated, 'uh-glee' + ("upper","an"), // 'uh-pur' (vowel sound but actually 'uh' sound = 'an'? Actually "upper" starts with a simple vowel sound 'uh', so should it be 'an'? It's commonly "an upper hand". + ("update","an"), // 'up-date' starts with 'uh' vowel sound → 'an update' normally. Actually we say "an update" commonly. Correcting to 'an': + + // Words starting with 'u' that actually take "an" + // (because they start with a vowel sound 'uh'): + ("under","an"), + ("understand","an"), + ("unusual","an"), + ("unreal","an"), + ("unemployed","an"), + ("unequal","an"), + ("unopened","an"), + ("unarmed","an"), + ("uneasy","an"), + ("united","a"), // 'yoo-nited' is 'a united front' + ("universal","a"), + ("umbrella","an"), // repeated + ("upset","an"), // 'uh-pset' vowel start + + // More vowel words to ensure coverage: + ("animal","an"), + ("image","an"), + ("order","an"), + ("arrow","an"), + ("ace","an"), + ("acre","an"), + ("agent","an"), + ("artist","an"), + ("idea","an"), // repeated + ("aim","an"), + ("item","an"), // repeated + ("isotope","an"), + ("opera","an"), + + // Words starting with vowels but pronounced with consonant sound 'y' → 'a' + ("euphemistic","a"), // repeated + ("euphoric","a"), + ("eulogy","a"), // repeated + ("Europe","a"), // repeated + ("European","a"), // repeated + ("eugenics","a"), // repeated + ("euphemism","a"), // repeated + ("eureka","a"), // repeated + ("euphonious","a"), + ("Eurasia","a"), + + // Common consonant-start words: + ("milk","a"), + ("road","a"), + ("stone","a"), + ("book","a"), // repeated + ("pen","a"), // repeated + ("fork","a"), // repeated + ("spoon","a"), // repeated + ("knife","a"), // 'knife' actually starts with /n/ sound because 'k' is silent. "a knife" or "an knife"? It's "a knife" (starts with 'n' consonant sound?), actually 'kn' in 'knife' is silent, so 'knife' is pronounced 'nife' with a consonant sound 'n', so "a knife" is correct. + ("glass","a"), + ("window","a"), + + // Final checks with tricky silent letters: + ("gnome","a"), // 'g' silent, pronounced 'nome' with 'n' consonant sound → 'a gnome' + ("gnat","a"), // 'nat' consonant sound → 'a gnat' + ("xylophone","a"), // 'zylophone' → 'z' consonant sound → 'a xylophone' + ("xenon","a"), // 'zee-non' consonant sound 'z' → 'a xenon' + ("honorific","an"), // repeated but reaffirming + ("hourly","an"), // repeated but reaffirming + ("honorably","an"), + ("honorifics","an"), + ("honors","an"), +) + +#for (word, article) in tests { + let chosen = __determine_article(word) + if chosen != article { + panic("bad choice! word: " + word + ", article: " + article + ", chosen: " + chosen) + } +} + +#set page(height: auto, width: auto, margin: 1em) + +Success! All articles determined properly!