defmodule NameCase do
@mac_prefix_name_exceptions [
# Lithuanian
{~r/\bMacEvicius/, "Macevicius"},
# Portuguese
{~r/\bMacHado/, "Machado"},
{~r/\bMacEdo/, "Macedo"},
{~r/\bMacHar/, "Machar"},
{~r/\bMacHin/, "Machin"},
{~r/\bMacHlin/, "Machlin"},
{~r/\bMacIas/, "Macias"},
{~r/\bMacIulis/, "Maciulis"},
{~r/\bMacKie/, "Mackie"},
{~r/\bMacKle/, "Mackle"},
{~r/\bMacKlin/, "Macklin"},
{~r/\bMacKmin/, "Mackmin"},
{~r/\bMacQuarie/, "Macquarie"},
{~r/\bMacOmber/, "Macomber"},
{~r/\bMacIn/, "Macin"},
{~r/\bMacKintosh/, "Mackintosh"},
{~r/\bMacKen/, "Macken"},
{~r/\bMacHen/, "Machen"},
{~r/\bMacisaac/, "MacIsaac"},
{~r/\bMacHiel/, "Machiel"},
{~r/\bMacIol/, "Maciol"},
{~r/\bMacKell/, "Mackell"},
{~r/\bMacKlem/, "Macklem"},
{~r/\bMacKrell/, "Mackrell"},
{~r/\bMacLin/, "Maclin"},
{~r/\bMacKey/, "Mackey"},
{~r/\bMacKley/, "Mackley"},
{~r/\bMacHell/, "Machell"},
{~r/\bMacHon/, "Machon"}
]
@general_replacements [
# al Arabic or forename Al.
{~r/\bAl(?=\s+\w)/, "al"},
# ap Welsh.
{~r/\bAp\b/, "ap"},
# bin, binti, binte Arabic.
{~r/\b(Bin|Binti|Binte)\b/, "bin"},
# ben Hebrew or forename Ben.
{~r/\bBen(?=\s+\w)/, "ben"},
# bat Hebrew or forename Bat.
{~r/\bBat(?=\s+\w)/, "bat"},
# della and delle Italian.
{~r/\bDell([ae])\b/, "dell\\1"},
# da, de, di Italian; du French; do Brasil.
{~r/\bD([aeiou])\b/, "d\\1"},
# das, dos Brasileiros.
{~r/\bD([ao]s)\b/, "d\\1"},
# del Italian; der/den Dutch/Flemish.
{~r/\bDe([lrn])\b/, "de\\1"},
# el Greek or El Spanish.
{~r/\bEl\b/, "el"},
# la French or La Spanish.
{~r/\bLa\b/, "la"},
# lo Italian; le French.
{~r/\bL([eo])\b/, "l\\1"},
# ten, ter Dutch/Flemish.
{~r/\bTe([rn])\b/, "te\\1"},
# van German or forename Van.
{~r/\bVan(?=\s+\w)/, "van"},
# von Dutch/Flemish.
{~r/\bVon\b/, "von"}
]
@spanish_conjunctions ~w(Y E I)
@roman_regex ~r/\b((?:[Xx]{1,3}|[Xx][Ll]|[Ll][Xx]{0,3})?(?:[Ii]{1,3}|[Ii][VvXx]|[Vv][Ii]{0,3})?)\b/
@post_nominals ~w(
ACILEx ACSM ADC AEPC AFC AFM AICSM AKC AM ARBRIBA ARCS ARRC ARSM AUH AUS
BA BArch BCh BChir BCL BDS BEd BEM BEng BM BS BSc BSW BVM&S BVScBVetMed
CB CBE CEng CertHE CGC CGM CH CIE CMarEngCMarSci CMarTech CMG CMILT CML CPhT CPLCTP CPM CQSW CSciTeach CSI CTL CVO
DBE DBEnv DC DCB DCM DCMG DConstMgt DCVO DD DEM DFC DFM DIC Dip DipHE DipLP DipSW DL DLitt DLP DPhil DProf DPT DREst DSC DSM DSO DSocSci
ED EdD EJLog EMLog EN EngD EngTech ERD ESLog
FADO FAWM FBDOFCOptom FCEM FCILEx FCILT FCSP. FdAFdSc FdEng FFHOM FFPM FRCAFFPMRCA FRCGP FRCOG FRCP FRCPsych FRCS FRCVS FSCR.
GBE GC GCB GCIE GCILEx GCMG GCSI GCVO GM
HNC HNCert HND HNDip
ICTTech IDSM IEng IMarEng IOMCPM ISO
J JP JrLog
KBE KC KCB KCIE KCMG KCSI KCVO KG KP KT
LFHOM LG LJ LLB LLD LLM Log LPE LVO
MA MAcc MAnth MArch MarEngTech MB MBA MBChB MBE MBEIOM MBiochem MC MCEM MCGI MCh. MChem MChiro MClinRes MComp MCOptom MCSM MCSP MD MEarthSc MEng MEnt MEP MFHOM MFin MFPM MGeol MILT MJur MLA MLitt MM MMath MMathStat MMORSE MMus MOst MP MPAMEd MPharm MPhil MPhys MRCGP MRCOG MRCP MRCPath MRCPCHFRCPCH MRCPsych MRCS MRCVS MRes MSc MScChiro MSci MSCR MSM MSocSc MSP MSt MSW MSYP MVO
NPQH
OBE OBI OM OND
PgC PGCAP PGCE PgCert PGCHE PgCLTHE PgD PGDE PgDip PhD PLog PLS
QAM QC QFSM QGM QHC QHDS QHNS QHP QHS QPM QS QTSCSci
RD RFHN RGN RHV RIAI RIAS RM RMN RN RN1RNA RN2 RN3 RN4 RN5 RN6 RN7 RN8 RN9 RNC RNLD RNMH ROH RRC RSAW RSci RSciTech RSCN RSN RVM RVN
SCHM SCJ SCLD SEN SGM SL SPANSPMH SPCC SPCN SPDN SPHP SPLD SrLog SRN SROT
TD
UD
V100 V200 V300 VC VD VetMB VN VRD
)
@downcase_words ~w(The Of And)
# Most two-letter words with no vowels should be kept in all caps as initials
@initial_name_regex ~r/\b(Aj|[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]{2})\s/
@initial_name_exceptions ~w(Mr Dr St Jr Sr Ms Lt)
@doc """
Returns a properly namecased `name`.
## Options
* `:lazy` - whether to skip mixed case names (defaults to `true`)
* `:mac_prefix` - whether to fix Mac/Mc prefix names (defaults to `true`)
* `:spanish` - whether to fix Spanish names (defaults to `true`)
* `:roman` - whether to fix roman numeral cases, eg. XI, XII (defaults to `true`)
* `:post_nominals` - whether to fix post-nominal letter cases, eg. OBE, PhD (defaults to `true`)
## Examples
iex> NameCase.nc("MCDONALDS")
"McDonalds"
iex> NameCase.nc("LEIGH-WILLIAMS")
"Leigh-Williams"
iex> NameCase.nc("ST. jOhN", lazy: false)
"St. John"
"""
def nc(name, opts \\ []) when is_binary(name) do
lazy? = Keyword.get(opts, :lazy, true)
if lazy? && mixed_case?(name) do
name
else
capitalize(name)
|> update_general_replacements()
|> update_initial_names()
|> update_downcase_words()
|> maybe_update_mac_prefix(opts)
|> maybe_update_roman(opts)
|> maybe_update_spanish_conjunctions(opts)
|> maybe_update_post_nominals(opts)
end
end
defp mixed_case?(name) do
first_letter_lower? = String.match?(name, ~r/^[a-z]/)
all_lower_or_upper? = String.downcase(name) == name || String.upcase(name) == name
!(first_letter_lower? || all_lower_or_upper?)
end
defp capitalize(name) do
name
|> String.downcase()
|> String.replace(~r/\b\w/, &String.upcase/1)
# Lowercase 's
|> String.replace(~r/'\w\b/, &String.downcase/1)
end
defp update_general_replacements(name) do
Enum.reduce(@general_replacements, name, fn {pattern, replacement}, acc ->
String.replace(acc, pattern, replacement)
end)
end
def update_initial_names(name) do
String.replace(name, @initial_name_regex, fn initial_name ->
if Enum.member?(@initial_name_exceptions, String.trim(initial_name)) do
initial_name
else
String.upcase(initial_name)
end
end)
end
defp update_downcase_words(name) do
Enum.reduce(@downcase_words, name, fn word, acc ->
word_regex = Regex.compile!("\\b#{word}\\b")
String.replace(acc, word_regex, String.downcase(acc))
end)
end
defp maybe_update_roman(name, opts) do
update_roman? = Keyword.get(opts, :roman, true)
if update_roman? do
Regex.replace(@roman_regex, name, &String.upcase/1)
else
name
end
end
defp maybe_update_mac_prefix(name, opts) do
update_mac_prefix? = Keyword.get(opts, :mac_prefix, true)
if update_mac_prefix? do
name =
if String.match?(name, ~r/.*?\bMac[A-Za-z]{2,}[^aciozj]\b/) ||
String.match?(name, ~r/.*?\bMc/) do
fix_mac_prefix_exceptions(name)
else
name
end
String.replace(name, "Macmurdo", "MacMurdo")
else
name
end
end
defp fix_mac_prefix_exceptions(name) do
name =
Regex.replace(~r/\b(Ma?c)([A-Za-z]+)/, name, fn _, mac_prefix, rest ->
mac_prefix <> String.capitalize(rest)
end)
Enum.reduce(@mac_prefix_name_exceptions, name, fn {pattern, replacement}, acc ->
String.replace(acc, pattern, replacement)
end)
end
defp maybe_update_spanish_conjunctions(name, opts) do
update_spanish_conjunctions? = Keyword.get(opts, :spanish, true)
if update_spanish_conjunctions? do
Enum.reduce(@spanish_conjunctions, name, fn conjunction, acc ->
conjunction_regex = Regex.compile!("\\b#{conjunction}\\b")
String.replace(acc, conjunction_regex, String.downcase(conjunction))
end)
else
name
end
end
defp maybe_update_post_nominals(name, opts) do
update_post_nominals? = Keyword.get(opts, :post_nominals, true)
if update_post_nominals? do
Enum.reduce(@post_nominals, name, fn post_nominal, acc ->
post_nominal_regex = Regex.compile!("\\b#{post_nominal}\\b", "ix")
String.replace(acc, post_nominal_regex, post_nominal)
end)
else
name
end
end
end