From e14331cfc096b2b941178c7b6e1771f37aebfc4b Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 17 Dec 2024 13:42:54 -0700 Subject: [PATCH] feat(glossy): add utility for determining indefinite articles in English --- glossy/src/utils.typ | 125 ++++++++++ glossy/tests/determine-article/ref/1.png | Bin 0 -> 14328 bytes glossy/tests/determine-article/test.typ | 289 +++++++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 glossy/tests/determine-article/ref/1.png create mode 100644 glossy/tests/determine-article/test.typ 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 0000000000000000000000000000000000000000..d16c201654131b828d3a3e62f03acd7e1eae6cea GIT binary patch literal 14328 zcmd^mYgChWws$C1S})C5+tOAg)*Itk#cC@mAsI(oYFlNhqqJV2j#b7%iHMp6a`9S9 zsiLAqMTxDo)OwU!hXN835ac44*jf#ikc0#h6d^Yt3Aw#{Z{}^g&YAVj`SN}^>-eEd zDM_B^|LpzSzspYMpO?ps9`WJ`27@tr>6=SdFc?8!z~2`>F&zGDU%I}Z!H85YU9#ve zf}kFK(2*%ik4;(nbk;Yg%3_u**}Qr4>Q(TEkAIwv|3Cih#J7%%!%x|~`HeT094N~> zHA3~y<~aT?K0j84ANkG~Tk|emx&H8*OIHRhd;b^y-5>k@<<%mUQ^b_^$4PG{`0ggy zg1OdUwzr*W&QI@c&yaLwxGpINXXINCgj)-wg9TpRz)uC76Rd!ZWiIqTESx`ozTPbL zo86Wj4}P4guy_>N%E3!Y11~VZbKgzyty+F&e&wpb0)-<=nWOYJC_C^~E_*~^M||h1 z_=A_OxH=R3nN#|{Hf1K@l!k@AD`zGlZZEfp0-Pbr-QdfpluPIF(9~Ne> zi{TvOOPzf4dTr-=SHM)J=q_`~RUWnkZsSA-*W2!0E7F?9VN!Rlv_97NQ>@MVk9)4K zc!Se8j=9{Nw{vINm;o!3J@`YmN#W@=u2podb!k+-b=R*LyShFOJUMIq_3BztW3Bay zW$=pJ->u|Cd{e;bzbLAT6b0isHJxYq{ubseskP}k_4+*)pVrbjmeW4=y;-^IO1AGc z)@D1Tcln}Pz7;=6)EOt48knGPrYmz*-bPi&Kr7E2DKSP0deo9_SFRg!;Y!^%BD{Z+ z1e{Wt%BN9v-r@T0xQo;97m}|(n5GDLl(N8*y6@ewI|d%TA8+Z8&yGyL7HM>F1CHqz ztR3OpQ+%5z4lyFuiWovi2w%fK=*}r!D`0?Lfy;`Zf zS6LZUR#v9jd1sk7cEFm!J(I{bzMv7B&#p^tddpbN;s{iXneVC&x1X$Kf3#vmXhi&nNl96ODR?Y)D5OqNlUg*o zSSN{M{sS)jd|3bQd&`~iex-1Kq##)0h?M3Ox=$CjS8=^nZuQ`Qs0LrA(~E*D8FtQg&-U%lkpE^OHi?C+yDYvF$ewj`YUCA-JE_wolOR zg&!B{gE{(O!NgWC6w&y?!EXw0J-AcgR$n)rPQpWTV-NYu%I~f%f0pY^<<|c2!x)GA zjP#Jsbx5b*rW@Gi{vu*<5~pu5_jsT_Ea0k5^?QZ!P-kZYvt<46%Wh(iIXudMM}F&) zd_-JyYoYJwLfad>zBfemxuPd!)+c2Z%9|@?gMSILeH=3}^X}hYSMTzMFM<|2riMf5 zz>D_j0@13TGNq?XSj%-~P<3btM(j6FlENUgv(UYri|+ z;ttaqqqX}3Z>ra&j*ip{CKJ#|i;L4P6&uWwUi18g3;)WIISOrOb?&orsBx~&-=k~x z0FvyuJJR_++`PzdmU7_Em8wf{^V{!U^9>dT7V=yl@#=En=Hbq*PkPq^E_`?GT2*~c z_=5C-mjjtcwmD=S(eUZ6y)0LdM!euhW@-yzn-b zrF?eitabX1`W*ShQ}uUb{+)w)`Y_g{?dtYk`N zJJWB*iC)jQA1~P6eq&htUoPGKWG%1!#L9x@DW=93@?}vLc}`Mc`DbfpboZa$a=JyM zd0Zc*bVLaOC8QF4h-436n9FZ3y{t~cGEXk;d@f(+=`G9tSl{>H4!bz%O6f=Ac=Dr! ztnT+`@FN?Rj(=_BD{jw*GV@`Q_; z&qmdXIEhI~BlqCJ%}s2cm>FtcOKBWM3UeYiK#5a<--#+8t5V@u zwMXk5(JiJVg(=Ckhv{N4IJU9eqrAbdcwZFNP`6)Yd9Jc==Sm*r+M#iMR78MC%BHIu zYoYjE!{Z&VCFFQ~a5b;71~#y2GRKC)`0?IM@D8JYo5fo5m@$k`jdkIo8{yXD1?+=Z z5ZJi#%5m}_S>ICG67?>PC6!}Nbz%F*cNfKHLFaPy%~6&Pyi5V&CL_75XC5{3{jD`s zEdO=|U_oB4ATw7V>+gsy(K)37r$YY!PsXZitOa($|DjS6#|tlbXeVNMPe z?km~CQ4E>4Ue>7cKT_GN!#PLsnmTORQZ_<=^XyUH2QvS3dh={wOSbJmID0StA5%M1 zI-g~KiFJeJ`++rPj0oWMWt^d^2)`l1PL&H7cfpd*ap!PPso>KY`omGZG$vgz#?pJ` zj^(rB<#kxgMy`J&Rlm}mr)+Q1K5BCLEddEpwZgz)VWLzjgL7bdE12|@*tgH}we=yQ zijeI9E{x>z{?(j;joic-7;^9TM>V9S)bGJ&6YMc!Hx+)|CeXoIWviyDeWBN=9u%)FS}x0s$TvXK0~FT-uX%O27+3y;lzT;Hwe?RGU= zyvw1={d@67&7JYRovzzV-=FcF4r+v*tjvQ3Q97h zf(Bq$4EM!7vEJ&~4xFfhA9M1nE#78}6}yhgxN5c)iG{6(NOhd(W}LMWnvfx{Nsg)wS}PVBT5+Crta zP&r&jgcO)&@SbhS3ioCL&q@2t1B+OFi$tJvqGH7lR|>8BS&hDU+p`Hr3xHH&eTHs^ znU%rrcuCZ2mdru~A6|jA>%i+J;(5LMO7I7z>+HyQM0X;^V_{hH8ft2WXNweRgP?>Lm+chDjryqk%(jN32GS(G~TZ?T_ZtZlHM74YpK%A3!yC#GI0CmBWjCjM; z_~gxgI~0JS-eZ22+$dG6qOH&C-Q&^40%>a@%1 z$MiF`hM6sGZJqe`4A)_c>nVs*n6feW{FQ^2=3#}sx zX;u?A4Bn1s+fvK<424|hM()7IAQzziPGIYbk7b@xnQ@t}Z&?swhSm|S%dx=2ZAa<{ zErarBv{pZwW_G;swfNvY_zXL+x`bXMOVt#qYlJe#w{^n^SWOHV$V-^JF2;^6?ESgW z_hRkg32@!UxxjQ?Be1f<_TXWe7BSO)$Me?k8W9I=CZ69!+IccSW+`xg0i47}q9`O^ z-_1^YhEc3dn}Hg*c|E`F=8n=2=jCn4BVoJ>1hD`I!$y|pJpe)NrwH7pl*wx=U1%j6AZ)~N~h zsSHqKR=Ay?y&#pQ0_T@jAf>kM(?~o$au8n&otji8&pMn}ks)r$uubFHr+I;}&crc# z3r-2=Jc3Vf1s<#oVe!m@)Q@7KAHFNEO_zu01~2Pw<4w{Eo}(-uLk zy#fT(Qi~OwqNJA|(smtk!Fw-@@3|X4dJiI-8KFaf6B`I$9b7RLX2*(_ePc4mx;m@k`(OBzGYr-=KvcmNbp5aRKiQG#- z!YgZQrQX^hxG=qv-HzoWfLDu&hpJ)N86`|{*Wyxub7;yd68cg=kKB#E8w{esL>Q0k7&>Y3Y*{uc_Bn zVIkr5rzpvCq5?CFmA$!yVZ}-(bBHJaKEeB_Gn=h~*r?9=5F zzZas~1CEDMx*@pyeqQ+tO2E%*as$TiA=4ogJP*O(9ocgAE(Wo$Lhr4@#yt3C8_r9{ z5aj@btN%rYv2Qmh9yRQ!8XajxFwj_tS$v&6_QO=1kWXtY_z`4|zyy_KMy8#QG)M?35dLnj zS^TuK3>44;e(!j|2&syk7w3;qIY7m{!N&PJl(FvPvF+%zA)RRX`RB*<^|6vUM6plA z37BjRiTLu)A22 zM5v1+C0LI{R&X#*Ff6|P+4#L&pt0?eQ}r|CH)dGlz{D7m{{1T=hE?DtIL?Jce=KvK zlC?{S<>05=hb35t5vDOvQq`pcL|W0eLe?;K5*&V@Kfyd&XB`bH()~1{w?_;UV=}f{ z^KZ@`-;G$Z!Z1UzFJ!PQ+y6JTd8`|NC3wUC1SZc>w9`ZbiPCxKpEa0F(*O;nO^aEB ze`WwbQS3+dJf!Gq`J$L!CjukP1^GzAY_W$Xl|?pNEr3s8}n% zxz?)r)s{r7+1(NA|1hlYT!>B6EaR%8%I`$ky1_)0e~tgd;f%L%rPTCdFJQ6LR#F>% zc<55l#rn5s;!@9Nv%#V4h!H?}Nq?1}5`3cdQSz!N;;eMn|Fp({G$s7%t?Z@pLC2Zt zKEiAtr)VF?Agu`osP+I3a?@mvVX|NYUSqP{_6GWiBlZAFgB+iqY8r(obMmXql<}ot zsbgn1BVn|o1IeJ7ptR3W-T?0Ev7?}6_kGUnOt60$9-WC3h>$b^gnm`yUN~HKsWG6CV#=js%B{~`{NDT-QK_Vuep7zxi1NyQFVoOdoRZl)w6bHdY`- z3qt!dPdT#pz2?CIIYz_Pfy<}xwT zueSX4^fdGr$I4tw;=z19g*{a<4~f}@gux4Lpgiltx~@T1KuG|&asE9+zK}4hSTIro z1mfuV^OHAUd%+n6(z@m9#xo0deUjc=n&FAYVR@yj*8ow##(8I?8g7(e|&`ofvZcIX!m-$>^dtW2xDZwt;ygdW^2xl*ik= zapV31uJ6H5Yk~xb*_0{d@f4>*2Js3A7M`+LpBjRNms;gCWw6A^zrabvfLKBlLX?7- z2hj4v8cU3{F9ys+VW^^it?~r031_gLzzN;1-V);`dA}PNaj`~b+&1H+FZaM z=78IC)3?=;Y#B)Q!eg8m@eP8p31^nu5aIvpThrOMv2(YZ&+>PGj{o+(mjz%;{~O2t zZ76Z%M{e+`;s+IGbU)y56K)XI-Id@3BP?8M4;Nyp0Y|UciI}7?;b_=K6u~RM6=GMOh zoSV~$QddbqwVA39% z!XWRQpH{#(P)vqqWqvEN5-aKh5kzbYz^P<4_$Ue@LzQR7A` zKPRVg3LF}=vRLOpS2KoVKPqo(m5=6vhgumdzLzL|4n%{j_P3P5_8hMg>*%|6vw&BG zq@mc)k$FEv7YZCRMm7i=G~KW)DLlHkvOoY>w~3&{Jn(f)^e5q5cfBU1c7kSm)o3r; zL1<+*kh_YF%Dzo58BV=laa%2JQ`_EU^}p){W9N4~$2{I*=ns4(0|WDpF@EiT6!S)u z?81N5|I9v8{lFlHA$~W~5J8eD0;}~|Fk5u0R1z1201Lsp8bW}55TMS<$qA3OYa~Dq zhDpeO!~uVR9Lma-pk^$Ac=1PO5xSA`AC<~*w6dL!4)k(iI3R!7Mz=A{@%-V# zhXplr^DBdUdU{e&mm$r>dNdEbRoM3yQTULMkUp^rnYe?5D~?*43c)@|n;(ZkYN@~r zv!n4zran=jUyepT8*r9;1BtO6ry}`ks-FMqDd_j2Pfg7U2Yky5Qz#5Fe9KL;_(p)| z#V#%{kDeD931L3PM^8fhBKer4Gx6@^Sdn2Y<+7pTA418hW{{3S!$3vO-cUM$&qRpO zPEQpTRfrxrstbiL$8(LFhlae~P5u{JEh)xBkvqCC7q5W4ONfRXjS*BKrU?*n3l=GW zKO94B>y#*=#4B(R*fEhH7n0OJWNQLUco=Jv2#V?V$S8-AZSbxotRQz6UEC=)4LRZ{ zhlooeIT8M5h@%^@IL!XLf5z^(mjLrXKAVID5mCG^_Q2KbVR|I4WLx+(1L??HY5!{2vs5n=;TS%vX4Wo3VjD#F|k@m=TCz9o}fR7 zZr(3RK4ZvBPP>?EfS45r0@RShf!d^%r#y?}?Xik^*syhI%hTeF9A-LOrNYVP_y*i| z`J)EK%W%`u=HPOOJrX7kxCPmk*gr%A=N7s8K`GXa@u$wtZQl5j)_=yZQVDkg*o6PFDJM zH=kKh<+!$P)_tw^tL}vZPUpoU@BzS&Fpq@#a05<>Bq!c_kw z@_OWCZqP*Fv!TT4+)+O_-!>}06;&Z-U2S|_6AbAR-K)eOFi0l1l0niJ4T~YC92X>* zedI7=3^62b%QX!)G6leki7FiUdtnVlI5)hournK&r={kXj$#+%%^S&X{6(po2j;;n zrwkE<0Jj+-jPiePB&PYs_NzUF*D&mZ z=ZBM80$Gz_Ajb;RUIs~zIPfGs00}^QHZL)SPcRxfkC7FP8!|_$F;BK*%0-HnIOz~C z!%%=0L6B9tk;D-kVj79t4;l;NkPy9rN&7OQA8nv>KqsD2apCP;jLiua$=E>QH{=u8 z!=*QT3P9Kf#Y4OZ-$aE>i= zXqSXe9?f|OP#lqRc<1@fblgS3)s}nRuSGPj1szOQ*e}K`1FeTTvYlb$BwvJt-RYqo z$H7TiC#p4JGP!%?!=U8|`(2_@#`!<)n!v!kriv~HqbaaDL`(*Z2I8H#Ack=(>BwFu z&T3jzft85~4A^MERD}ItX{#j0E*2IsRg4M`To8F2fAtK;3JcgL$>}02IweWH717++*1?b zhQ=`l`u^kw>4n3%$NZG;Ze{p1fXtb=@o>h7A~l#(}1{BSqC%(2|3#st!D# zAQ!yrEI3DPHtjfVgL2p0-F(c381kp)g>8y+VdtEJLM zD^18>aX$mx_+CED<-;s)GY*BYF3NsE&QC?~Kr@9w$rXkxo%H{ZGyeTDWBh~(HY_HZ zGz=1v#G}xU5<#3bp8<<02JCn@ly%5ZzUcXuX*@V&1|Vy2m{qCp;k{0>=_*4-L9{K1 zDa#PX?c9l5KZ0Zkmvq%65=DRQX} z$Kw55@%yx^frC0k55x^u6g7}d(zY78eLftADX>C`c4N>U;=KDgUHgZEormW+pO^=b zQxeUiwk8^^0l1cAp|!vGX)+l}A9_W=z-AUKKWiXOO|it|V4p=sw(T4XLxnPEU_gb{ z;1g1*pjl)`KSB-;<0NM*tVQAJ5ko*tdwbUe8c-C>yas+016o%;3g_2T6%-7kcZIAS z_1}i{^gP;_=uHTIIv;kE<_;1Hh|0DrNlIaGRtn2%0<^SobqCH-k%7Bp zw0VQ@LFWeqsHt1t+&vr47;J`3bjDy@wnAH3iM7Fw!yNM3(Kt!1oxr!EtssJNKWR)Q zQ3Z@VF#<#jPYxntYHa$?cPMnyLfagV>MhxiDUk>+Wd`m4A>)|=KDHgJh#YyMwV3G8 zp+pRR5n(t;tHYGw<0dVdEZFks0KJ_1HpUcZuXMX`b%66Ahm(m3I(WRxO zk1#i%1sB~I1uJ-zf|76T2d;3WmNRgb`WTXl!W)p54bcw-T01$}IKyDMQCClfI;{ia z;31C>_mCXO5|GJuB9ep2?EW3K1!@R09#sh}str9LXp>xcKtv(1+Ato)j9TQam9pW9 zV62Aj1FW-<;mJ=g=5K_O$sYVV+kk^Wczb_XHEz$47f&Ph#~I~M(;fkU9bBqwBn_on zw&@iHO<0t|_aVYZB7$Xu?rlxKIzfd!`%)(PJ?LITE`WzlbQyQu+{nhkqao6h?eEIA zBcG#~n89z`6$)U(y%{6^DP}_+Y13_5(>4K`GGP5-%ZYvn6^|*jy;xX@NH;gHX-Eo_ z>kbRH2FWihZ->2ODeWQrA|qX=3*9f`{w7WZY@l8zjSG<6GjK9s>?%N^L#PX@w(w*X z`wtx&O<6sy(ooKa!~@++V(hRw$$-Z6@1qTF+$5m}#6xtz=be7juouH+pu0j)or^A? zFz@#C6jL$;2nQsp%ai}lO=UzG+O@v7me&cp;4wo%J+33A4EAlwZ-p%sH>_!F@WZ0R zaavk|!xmp{3K#zvPG%BW_j_ps zQa!LD_&V8I*q=x#^+;|Zs0gd{Nr0XlN280DEv?og$g63=D}4DGhF+U)=|nf61Uje$ zwm|MfL+pY~hund%E>vakq3$uX=Yi{np&e*sL*6`f62u@YMxYCFD>xccbzCs~x@g5) zpBL4?%yyYkpqEg0xqa*2JatouyVZd zbNAlb3_J<5@LK@j!4)VrNX#L1-?_e)HnVV-g5ia+_8j8O+Ys5pdL9wc9TCnQ3=A?T zO~=Lt(-C)K47l?RhxWK0h6H4CUIq^4;Ll}Jj(F>zpbEn*rZM2hh2P#i#R^KlG&A0b_7 zG>UV2Inc3{6!l}AL2LQUK70hE?0IN66S`wZoSC1pkHRPd9tqB52}q}7G_G-Il@X2UhtOLAc(2Pk>Psh`Lq=hvzNRaC+_3=H7!qTe6*11?3?(IH`w zEdu*!LZA&pK2-NHV4+)yBw&CX2%gw#f-h=ae;7Ci6A+8QhTD1AVkA9r--`?y)o$22 zci_$>k&*iE)zS=unrg*cE5TWk-L^ob8!*TF%>`B;6(Gw5p8t6u`t zDFN@th2IYHUjyS!JVphs;@3sTPC;{^(stk%L%jUDcffE^e7ZKd^dMD=^mqS!LpXjZ h^MB&s{bPzYDK@AhHzdM0`WQ=JU%uqZpFY_4e*pV8XyO0> literal 0 HcmV?d00001 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!