Skip to content

Commit

Permalink
Adds a function to convert text to NameCase (#94)
Browse files Browse the repository at this point in the history
* Adds a function to convert text to NameCase

- updates Recase.Replace.replace spec to show it also works with functions

Signed-off-by: Will Read <will@geometer.io>

* Addresses code coverage issues

Signed-off-by: Will Read <will@geometer.io>

Co-authored-by: Matt Sloane <matt@geometer.io>
  • Loading branch information
TildeWill and Matt Sloane committed Jul 2, 2020
1 parent 2f5f42d commit b1feda3
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 1 deletion.
15 changes: 15 additions & 0 deletions lib/recase.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Recase do
DotCase,
HeaderCase,
KebabCase,
NameCase,
PascalCase,
PathCase,
SentenceCase,
Expand Down Expand Up @@ -160,4 +161,18 @@ defmodule Recase do
"""
@spec to_header(String.t()) :: String.t()
def to_header(value), do: HeaderCase.convert(value)

@doc """
Converts string to Name Case
## Examples
iex> Recase.to_name("mccarthy o'donnell")
"McCarthy O'Donnell"
iex> Recase.to_name("von streit")
"von Streit"
"""
@spec to_name(String.t()) :: String.t()
def to_name(value), do: NameCase.convert(value)
end
90 changes: 90 additions & 0 deletions lib/recase/cases/name_case.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
defmodule Recase.NameCase do
@moduledoc """
Module to convert strings to `Name Case`.
This module should not be used directly.
## Examples
iex> Recase.to_name "mccarthy o'donnell"
"McCarthy O'Donnell"
Read about `Name Case` here:
https://metacpan.org/pod/Lingua::EN::NameCase
"""
import Recase.Replace

@spec convert(String.t()) :: String.t()
def convert(value) when is_binary(value) do
value
|> String.downcase()
|> replace(~r|\b\w|, fn first_char_of_word ->
String.upcase(first_char_of_word)
end)
|> replace(~r|\'\w\b|, fn apostophe_ess ->
String.downcase(apostophe_ess)
end)
|> replace_irish()
|> replace(~r|\bVon\b|, "von")
|> replace(~r|\bVan(?=\s+\w)|, "van")
|> replace(~r|\bAp\b|, "ap")
|> replace(~r|\bAl(?=\s+\w)|, "al")
|> replace(~r|\bEl\b|, "el")
|> replace(~r|\bLa\b|, "la")
|> replace(~r|\bBen(?=\s+\w)|, "ben")
|> replace(~r/\b(Bin|Binti|Binte)\b/, fn bin_prefix ->
String.downcase(bin_prefix)
end)
|> replace(~r|\bD([aeiou])\b|, fn da_prefix ->
String.downcase(da_prefix)
end)
|> replace(~r|\bD([ao]s)\b|, fn das_prefix ->
String.downcase(das_prefix)
end)
|> replace(~r|\bDell([ae])\b|, fn dell_prefix ->
String.downcase(dell_prefix)
end)
|> replace(~r|\bDe([lr])\b|, fn del_prefix ->
String.downcase(del_prefix)
end)
|> replace(~r|\bL([eo])\b|, fn le_prefix -> String.downcase(le_prefix) end)
|> replace_roman_numerals()
|> replace(~r|\b([YEI])\b|, fn conjunction ->
String.downcase(conjunction)
end)
end

defp replace_roman_numerals(string) do
replace(
string,
~r/\b ( (?: [Xx]{1,3} | [Xx][Ll] | [Ll][Xx]{0,3} )? (?: [Ii]{1,3} | [Ii][VvXx] | [Vv][Ii]{0,3} )? ) \b /x,
fn numeral -> String.upcase(numeral) end
)
end

defp replace_irish(string) do
replace(string, ~r|\b(Mc)([A-Za-z]+)|, fn _, mc_prefix, rest_of_word ->
mc_prefix <> String.capitalize(rest_of_word)
end)
|> replace(
~r|\b(Ma?c)([A-Za-z]{2,}[^aciozj])\b|,
fn _, mc_prefix, rest_of_word ->
mc_prefix <> String.capitalize(rest_of_word)
end
)
|> replace(~r/\bMacEdo/, "Macedo")
|> replace(~r/\bMacEvicius/, "Macevicius")
|> replace(~r/\bMacHado/, "Machado")
|> replace(~r/\bMacHar/, "Machar")
|> replace(~r/\bMacHin/, "Machin")
|> replace(~r/\bMacHlin/, "Machlin")
|> replace(~r/\bMacIas/, "Macias")
|> replace(~r/\bMacIulis/, "Maciulis")
|> replace(~r/\bMacKie/, "Mackie")
|> replace(~r/\bMacKle/, "Mackle")
|> replace(~r/\bMacKlin/, "Macklin")
|> replace(~r/\bMacKmin/, "Mackmin")
|> replace(~r/\bMacQuarie/, "Macquarie")
|> replace(~r/\bMacmurdo/, "MacMurdo")
end
end
3 changes: 2 additions & 1 deletion lib/recase/utils/replace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ defmodule Recase.Replace do
This function accepts `value` as the first argument and
then passes it to `Regex.replace/3` as the second one.
"""
@spec replace(String.t(), Regex.t(), String.t()) :: String.t()
@spec replace(String.t(), Regex.t(), String.t() | (... -> String.t())) ::
String.t()
def replace(value, regex, new_value) do
Regex.replace(regex, value, new_value)
end
Expand Down
73 changes: 73 additions & 0 deletions test/recase_test/name_case_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule Recase.NameCaseTest do
use ExUnit.Case

import Recase.NameCase

doctest Recase.NameCase

test "should name case usual text" do
assert convert("keith") == "Keith"
assert convert("leigh-williams") == "Leigh-Williams"
assert convert("mccarthy") == "McCarthy"
assert convert("o'callaghan") == "O'Callaghan"
assert convert("st. john") == "St. John"
assert convert("von streit") == "von Streit"
assert convert("van dyke") == "van Dyke"
assert convert("van") == "Van"
assert convert("ap llwyd dafydd") == "ap Llwyd Dafydd"
assert convert("al fahd") == "al Fahd"
assert convert("al") == "Al"
assert convert("el grecco") == "el Grecco"
assert convert("bin friendly") == "bin Friendly"
assert convert("ben gurion") == "ben Gurion"
assert convert("ben") == "Ben"
assert convert("dos brasileiros") == "dos Brasileiros"
assert convert("da vinci") == "da Vinci"
assert convert("di caprio") == "di Caprio"
assert convert("du pont") == "du Pont"
assert convert("de legate") == "de Legate"
assert convert("del crond") == "del Crond"
assert convert("della crond") == "della Crond"
assert convert("der sind") == "der Sind"
assert convert("van der Post") == "van der Post"
assert convert("von trapp") == "von Trapp"
assert convert("la poisson") == "la Poisson"
assert convert("le figaro") == "le Figaro"
assert convert("mack knife") == "Mack Knife"
assert convert("dougal macdonald") == "Dougal MacDonald"
assert convert("ruiz y picasso") == "Ruiz y Picasso"
assert convert("dato e iradier") == "Dato e Iradier"
assert convert("mas i gavarró") == "Mas i Gavarró"
assert convert("parson's") == "Parson's"

# Mac expectations
assert convert("machin") == "Machin"
assert convert("machlin") == "Machlin"
assert convert("machar") == "Machar"
assert convert("mackle") == "Mackle"
assert convert("macklin") == "Macklin"
assert convert("mackie") == "Mackie"
assert convert("macquarie") == "Macquarie"
assert convert("machado") == "Machado"
assert convert("macevicius") == "Macevicius"
assert convert("maciulis") == "Maciulis"
assert convert("macias") == "Macias"
assert convert("macmurdo") == "MacMurdo"

# Roman Numerals
assert convert("henry viii") == "Henry VIII"
assert convert("louis iii") == "Louis III"
assert convert("louis xiv") == "Louis XIV"
assert convert("charles ii") == "Charles II"
assert convert("fred xlix") == "Fred XLIX"
assert convert("yusof bin ishak") == "Yusof bin Ishak"
end

test "should return single letter" do
assert convert("a") == "A"
end

test "should return empty string" do
assert convert("") == ""
end
end
8 changes: 8 additions & 0 deletions test/utils_test/replace_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule RecaseReplaceTest do
use ExUnit.Case

test "replace has the same behavior of Regex.replace with different argument ordering" do
assert Recase.Replace.replace("foo", ~r|oo|, "aa") ==
Regex.replace(~r|oo|, "foo", "aa")
end
end

0 comments on commit b1feda3

Please sign in to comment.