Current section
Files
Jump to
Current section
Files
lib/address_formatting.ex
defmodule AddressFormatting do
@moduledoc """
Documentation for `AddressFormatting`.
"""
alias AddressFormatting.Constants
alias AddressFormatting.Address
def render(variables) when is_map(variables) do
case get_template(variables) do
{nil, _, _} ->
variables
|> Map.delete("country_code")
|> Map.values()
|> Enum.join("\n")
|> Kernel.<>("\n")
data ->
render(data)
end
end
@spec render({String.t(), Address.t() | map, list}) :: String.t()
def render({template, address, postformat}) do
:bbmustache.compile(template, address, key_type: :atom)
|> String.trim()
|> run_postformat(postformat)
|> fix_duplicates(address)
|> Kernel.<>("\n")
end
def fix_duplicates(string, _variables) do
string
|> String.split("\n")
|> List.foldr([], fn current, acc ->
previous = List.first(acc)
if current == previous do
acc
else
[current | acc]
end
end)
|> Enum.join("\n")
end
def run_postformat(rendered, postformat) do
postformat
|> Enum.reduce(rendered, fn [from, to], rendered_acc ->
Regex.replace(from, rendered_acc, to)
end)
end
def run_replace(address, country_data) do
address_new =
country_data
|> Map.get("replace", [])
|> Enum.reduce(address, fn [field_from, to], variables ->
[field, from] =
case String.split(field_from, "=") do
[from | []] -> [:country, from]
["country_code", from] -> [:country_code, String.upcase(from)]
[field, from] -> [String.to_existing_atom(field), from]
end
case Map.get(variables, field) do
nil -> variables
^from -> Map.put(variables, field, to)
_ -> variables
end
end)
{:ok, address_new}
end
def get_template(variables) do
country_code = Map.get(variables, "country_code") |> upcase()
case Map.get(Constants.worldwide_data(), country_code) do
nil ->
{
Constants.fallback_template(),
struct(Address, variables),
[]
}
country_data ->
with address <- %Address{},
{:ok, address} <- check_country_case(address, variables, country_data),
{:ok, address} <- convert_component_aliases(address, variables),
{:ok, address} <- convert_constants(address),
{:ok, address} <- convert_country_numeric(address),
{:ok, address} <- add_codes(address, :county_code),
{:ok, address} <- add_codes(address, :state_code),
{:ok, address} <- run_replace(address, country_data),
{:ok, address} <- check_use_country(address, country_data),
{:ok, address} <- add_component(address, country_data),
{:ok, address} <- check_change_country(address, country_data),
{:ok, postformat} <- get_postformat(country_data) do
{
get_in(Constants.worldwide_templates(), [String.upcase(address.country_code)]),
address,
postformat
}
end
end
end
def default_postformat_regex() do
[
[~r/\'\;/, "'"],
[~r/[\},\s]+$/, ""],
[~r/^[,\s]+/, ""],
# linestarting with dash due to a parameter missing
[~r/^- /, ""],
[~r/\n\h-/, "\n"],
# multiple commas to one
[~r/,\s*,/, ", "],
# one horiz whitespace behind comma
[~r/\h+,\h+/, ", "],
# multiple horiz whitespace to one
[~r/\h\h+/, " "],
# horiz whitespace, newline to newline
[~r/\h\n/, "\n"],
# newline comma to just newline
[~r/\n,/, "\n"],
# multiple commas to one
[~r/,,+/, ","],
# comma newline to just newline
[~r/,\n/, "\n"],
# newline plus space to newline
[~r/\n\h+/, "\n"],
[~r/\n\n+/, "\n"]
]
end
def get_postformat(country_data) do
postformat =
case Map.get(country_data, "postformat_replace") do
nil ->
default_postformat_regex()
postformat_regex_list ->
postformat_regex_list
|> Enum.reduce(default_postformat_regex(), fn [from, to], postformat_acc ->
from = Regex.compile!(from)
to = String.replace(to, "$", "\\")
[[from, to] | postformat_acc]
end)
end
{:ok, postformat}
end
def add_component(address, country_data) do
case Map.get(country_data, "add_component") do
nil ->
{:ok, address}
add_component ->
case String.split(add_component, "=") do
["country_code", value] ->
{:ok, %Address{address | country_code: value}}
[field, value] ->
{:ok, Map.put(address, to_existing_atom(field, :attention), value)}
end
end
end
def convert_component_aliases(address, variables) do
components = Constants.components()
address =
variables
|> Enum.reduce(address, fn {key, v}, acc_add ->
atom_key = to_existing_atom(key, :attention)
case Map.get(components, atom_key) do
nil ->
%Address{acc_add | attention: v}
new_key ->
atom_key = to_existing_atom(new_key, :attention)
new_value = adjust_value(v, atom_key)
Map.put(acc_add, atom_key, new_value)
end
end)
{:ok, address}
end
def convert_constants(address) do
address =
case Map.get(address, :country_code) do
"UK" ->
Map.put(address, :country_code, "GB")
"NL" ->
state = Map.get(address, :state)
cond do
state == nil ->
address
state == "Curaçao" ->
%{address | country: "Curaçao", country_code: "CW"}
String.downcase(state) =~ "sint maarten" ->
%{address | country: "Sint Maarten", country_code: "SX"}
String.downcase(state) =~ "aruba" ->
%{address | country: "Aruba", country_code: "AW"}
true ->
address
end
_other ->
address
end
{:ok, address}
end
def convert_country_numeric(address) do
address =
if is_integer?(address.country) do
%Address{address | country: address.state, state: nil}
else
address
end
{:ok, address}
end
def add_codes(address, key) do
country_code = address.country_code
address =
case Map.get(address, key) do
nil ->
address
state ->
case get_in(Constants.get_codes_dict(key), [country_code, state]) do
nil -> address
code -> Map.put(address, key, code)
end
end
{:ok, address}
end
def check_change_country(address, country_data) do
case Map.get(country_data, "change_country") do
nil ->
{:ok, address}
change_country ->
{:ok, %Address{address | country: change_country}}
end
end
def check_country_case(address, variables, _country_data) do
country_code = Map.get(variables, "country_code")
{:ok, Map.put(address, :country_code, String.upcase(country_code))}
end
def check_use_country(address, country_data) do
case Map.get(country_data, "use_country") do
nil -> {:ok, address}
country_code -> {:ok, %Address{address | country_code: String.upcase(country_code)}}
end
end
def upcase(nil), do: nil
def upcase(string), do: String.upcase(string)
def is_integer?(val) when is_integer(val), do: true
def is_integer?(val) when is_bitstring(val) do
Regex.match?(~r{\A\d*\z}, val)
end
def is_integer?(_), do: false
def adjust_value(value, :postcode) when is_bitstring(value) and byte_size(value) > 9, do: nil
def adjust_value(value, _key), do: value
def to_existing_atom(key, _) when is_atom(key), do: key
def to_existing_atom(key, default) do
try do
String.to_existing_atom(key)
rescue
_e ->
IO.puts("#{key}: is not an existing atom")
default
end
end
end