Initial commit

This commit is contained in:
2024-03-05 06:02:58 -05:00
commit a32c7a5b74
42 changed files with 1395 additions and 0 deletions

18
lib/iban_ex.ex Normal file
View File

@@ -0,0 +1,18 @@
defmodule IbanEx do
@moduledoc """
Documentation for `IbanEx`.
"""
@doc """
Hello world.
## Examples
iex> IbanEx.hello()
:world
"""
def hello do
:world
end
end

View File

@@ -0,0 +1,16 @@
defmodule IbanEx.Commons do
@spec normalize(binary()) :: binary()
def normalize(string) do
string
|> to_string()
|> String.replace(~r/\s*/i, "")
|> String.upcase()
end
@spec normalize_and_slice(binary(), Range.t()) :: binary()
def normalize_and_slice(string, range) do
string
|> normalize()
|> String.slice(range)
end
end

52
lib/iban_ex/country.ex Normal file
View File

@@ -0,0 +1,52 @@
defmodule IbanEx.Country do
import IbanEx.Commons, only: [normalize: 1]
@type country_code() :: <<_::16>> | atom()
@type error_tuple() :: {:error, atom()}
@supported_countries %{
"AT" => IbanEx.Country.AT,
"BE" => IbanEx.Country.BE,
"BG" => IbanEx.Country.BG,
"CH" => IbanEx.Country.CH,
"CY" => IbanEx.Country.CY,
"CZ" => IbanEx.Country.CZ,
"DE" => IbanEx.Country.DE,
"DK" => IbanEx.Country.DK,
"ES" => IbanEx.Country.ES,
"EE" => IbanEx.Country.EE,
"FR" => IbanEx.Country.FR,
"FI" => IbanEx.Country.FI,
"GB" => IbanEx.Country.GB,
"HR" => IbanEx.Country.HR,
"LT" => IbanEx.Country.LT,
"LU" => IbanEx.Country.LU,
"LV" => IbanEx.Country.LV,
"MT" => IbanEx.Country.MT,
"NL" => IbanEx.Country.NL,
"PL" => IbanEx.Country.PL,
"PT" => IbanEx.Country.PT,
"UA" => IbanEx.Country.UA
}
@supported_country_codes Map.keys(@supported_countries)
@spec supported_countries() :: map()
defp supported_countries(), do: @supported_countries
@spec supported_country_codes() :: [country_code()] | []
def supported_country_codes(), do: @supported_country_codes
@spec country_module(country_code) :: Module.t() | error_tuple()
def country_module(country_code) when is_binary(country_code) or is_atom(country_code) do
normalized_country_code = normalize(country_code)
case is_country_code_supported?(normalized_country_code) do
true -> supported_countries()[normalized_country_code]
_ -> {:error, :unsupported_country_code}
end
end
@spec is_country_code_supported?(country_code()) :: boolean()
def is_country_code_supported?(country_code) when is_binary(country_code) or is_atom(country_code),
do: Enum.member?(@supported_country_codes, normalize(country_code))
end

34
lib/iban_ex/country/at.ex Normal file
View File

@@ -0,0 +1,34 @@
defmodule IbanEx.Country.AT do
@moduledoc """
Austria IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 20
@rule ~r/^(?<bank_code>[0-9]{5})(?<account_number>[0-9]{11})$/i
@spec size() :: 20
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/be.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.BE do
@moduledoc """
Belgium IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 16
@rule ~r/^(?<bank_code>[0-9]{3})(?<account_number>[0-9]{7})(?<national_check>[0-9]{2})$/i
@spec size() :: 16
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number, national_check]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/bg.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.BG do
@moduledoc """
Bulgaria IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 22
@rule ~r/^(?<bank_code>[A-Z]{4})(?<branch_code>[0-9]{4})(?<account_number>[0-9]{2}[0-9A-Z]{8})$/i
@spec size() :: 22
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number]
|> Enum.join(joiner)
end
end

34
lib/iban_ex/country/ch.ex Normal file
View File

@@ -0,0 +1,34 @@
defmodule IbanEx.Country.CH do
@moduledoc """
Switzerland IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 21
@rule ~r/^(?<bank_code>[0-9]{5})(?<account_number>[0-9A-Z]{12})$/i
@spec size() :: 21
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/cy.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.CY do
@moduledoc """
Cyprus IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 28
@rule ~r/^(?<bank_code>[0-9]{3})(?<branch_code>[0-9]{5})(?<account_number>[0-9A-Z]{16})$/i
@spec size() :: 28
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/cz.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.CZ do
@moduledoc """
Czech Republic IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 24
@rule ~r/^(?<bank_code>[0-9]{4})(?<account_number>[0-9]{16})$/i
@spec size() :: 24
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/de.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.DE do
@moduledoc """
Germany IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 22
@rule ~r/^(?<bank_code>[0-9]{8})(?<account_number>[0-9]{10})$/i
@spec size() :: 22
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/dk.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.DK do
@moduledoc """
Denmark IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 18
@rule ~r/^(?<bank_code>[0-9]{4})(?<account_number>[0-9]{10})$/i
@spec size() :: 18
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/ee.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.EE do
@moduledoc """
Estonian IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 20
@rule ~r/^(?<bank_code>[0-9]{2})(?<branch_code>[0-9]{2})(?<account_number>[0-9]{11})(?<national_check>[0-9]{1})$/i
@spec size() :: 20
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number, national_check]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/es.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.ES do
@moduledoc """
Spain IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 24
@rule ~r/^(?<bank_code>[0-9]{4})(?<branch_code>[0-9]{4})(?<national_check>[0-9]{2})(?<account_number>[0-9]{10})$/i
@spec size() :: 24
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, national_check, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/fi.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.FI do
@moduledoc """
Finland IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 18
@rule ~r/^(?<bank_code>[0-9]{6})(?<account_number>[0-9]{7})(?<national_check>[0-9]{1})$/i
@spec size() :: 18
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number, national_check]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/fr.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.FR do
@moduledoc """
France IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 27
@rule ~r/^(?<bank_code>[0-9]{5})(?<branch_code>[0-9]{5})(?<account_number>[0-9A-Z]{11})(?<national_check>[0-9]{2})$/i
@spec size() :: 27
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number, national_check]
|> Enum.join(joiner)
end
end

35
lib/iban_ex/country/gb.ex Normal file
View File

@@ -0,0 +1,35 @@
defmodule IbanEx.Country.GB do
@moduledoc """
United Kingdom IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 22
@rule ~r/^(?<bank_code>[A-Z]{4})(?<branch_code>[0-9]{6})(?<account_number>[0-9]{8})$/i
@spec size() :: 22
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/hr.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.HR do
@moduledoc """
Croatia IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 21
@rule ~r/^(?<bank_code>[0-9]{7})(?<account_number>[0-9]{10})$/i
@spec size() :: 21
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/lt.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.LT do
@moduledoc """
Lithuanian IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 20
@rule ~r/^(?<bank_code>[0-9]{5})(?<account_number>[0-9]{11})$/i
@spec size() :: 20
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/lu.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.LU do
@moduledoc """
Luxembourg IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 20
@rule ~r/^(?<bank_code>[0-9]{3})(?<account_number>[0-9A-Z]{13})$/i
@spec size() :: 20
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/lv.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.LV do
@moduledoc """
Latvian IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 21
@rule ~r/^(?<bank_code>[A-Z]{4})(?<account_number>[0-9A-Z]{13})$/i
@spec size() :: 21
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/mt.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.MT do
@moduledoc """
Malta IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 31
@rule ~r/^(?<bank_code>[A-Z]{4})(?<branch_code>[0-9]{5})(?<account_number>[0-9A-Z]{18})$/i
@spec size() :: 31
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/nl.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.NL do
@moduledoc """
Netherlands IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 18
@rule ~r/^(?<bank_code>[A-Z]{4})(?<account_number>[0-9]{10})$/i
@spec size() :: 18
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/pl.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.PL do
@moduledoc """
Poland IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 28
@rule ~r/^(?<bank_code>[0-9]{3})(?<branch_code>[0-9]{4})(?<national_check>[0-9]{1})(?<account_number>[0-9]{16})$/i
@spec size() :: 28
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, national_check, account_number]
|> Enum.join(joiner)
end
end

36
lib/iban_ex/country/pt.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.PT do
@moduledoc """
Portugal IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 25
@rule ~r/^(?<bank_code>[0-9]{4})(?<branch_code>[0-9]{4})(?<account_number>[0-9]{11})(?<national_check>[0-9]{2})$/i
@spec size() :: 25
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
account_number: account_number,
national_check: national_check
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number, national_check]
|> Enum.join(joiner)
end
end

View File

@@ -0,0 +1,12 @@
defmodule IbanEx.Country.Template do
alias IbanEx.Iban
@type size() :: non_neg_integer()
@type rule() :: Regex.t()
@type country_code() :: <<_::16>> | atom()
@type joiner() :: String.t()
@callback size() :: size()
@callback rule() :: rule()
@callback to_s(Iban.t(), joiner()) :: String.t()
@callback to_s(Iban.t()) :: String.t()
end

36
lib/iban_ex/country/ua.ex Normal file
View File

@@ -0,0 +1,36 @@
defmodule IbanEx.Country.UA do
@moduledoc """
Ukrainian IBAN parsing rules
"""
alias IbanEx.Iban
@behaviour IbanEx.Country.Template
@size 29
@rule ~r/^(?<bank_code>[0-9]{6})(?<account_number>[0-9A-Z]{19})$/i
@spec size() :: 29
def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@spec to_s(Iban.t()) :: binary()
@spec to_s(Iban.t(), binary()) :: binary()
def to_s(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: _branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, account_number]
|> Enum.join(joiner)
end
end

View File

@@ -0,0 +1,49 @@
defprotocol IbanEx.Deserialize do
@type iban_or_error() :: IbanEx.Iban.t() | {:error, :can_not_parse_map | atom()}
@spec to_iban(t()) :: iban_or_error()
def to_iban(value)
end
defimpl IbanEx.Deserialize, for: [BitString, String] do
alias IbanEx.{Parser, Error}
@type iban_or_error() :: IbanEx.Iban.t() | {:error, atom()}
@spec to_iban(String.t()) :: iban_or_error()
@spec to_iban(binary()) :: IbanEx.Iban.t()
def to_iban(string) do
case Parser.parse(string) do
{:ok, iban} -> iban
{:error, error_code} -> {error_code, Error.message(error_code)}
end
end
end
defimpl IbanEx.Deserialize, for: Map do
alias IbanEx.Iban
@type iban_or_error() :: IbanEx.Iban.t() | {:error, :can_not_parse_map}
@spec to_iban(map()) :: iban_or_error()
def to_iban(
%{
country_code: _country_code,
check_digits: _check_sum_digits,
bank_code: _bank_code,
account_number: _account_number
} = map
) do
struct(Iban, map)
end
def to_iban(
%{
"country_code" => _country_code,
"check_digits" => _check_sum_digits,
"bank_code" => _bank_code,
"account_number" => _account_number
} = map
) do
atomized_map = for {key, val} <- map, into: %{}, do: {String.to_atom(key), val}
to_iban(atomized_map)
end
def to_iban(map) when is_map(map), do: {:error, :can_not_parse_map}
end

33
lib/iban_ex/error.ex Normal file
View File

@@ -0,0 +1,33 @@
defmodule IbanEx.Error do
@moduledoc """
"""
@type error() ::
:unsupported_country_code
| :invalid_format
| :invalid_length
| :invalid_checksum
| :can_not_parse_map
| atom()
@type errors() :: [error()]
@errors [
:unsupported_country_code,
:invalid_format,
:invalid_length,
:invalid_checksum,
:can_not_parse_map
]
@messages [
unsupported_country_code: "Unsupported country code",
invalid_format: "IBAN violates required format",
invalid_length: "IBAN violates the required length",
invalid_checksum: "IBAN's checksum is invalid",
can_not_parse_map: "Can't parse map to IBAN struct"
]
@spec message(error()) :: String.t()
def message(error) when error in @errors, do: @messages[error]
def message(_error), do: "Undefined error"
end

42
lib/iban_ex/formatter.ex Normal file
View File

@@ -0,0 +1,42 @@
defmodule IbanEx.Formatter do
alias IbanEx.Country
import IbanEx.Commons, only: [normalize: 1]
@available_formats [:compact, :pretty, :splitted]
@type iban() :: IbanEx.Iban.t()
@type available_format() :: :compact | :pretty | :splitted
@type available_formats_list() :: [:compact | :pretty | :splitted ]
@spec available_formats() :: available_formats_list()
def available_formats(), do: @available_formats
@spec pretty(IbanEx.Iban.t()) :: binary()
def pretty(iban), do: format(iban, :pretty)
@spec compact(IbanEx.Iban.t()) :: binary()
def compact(iban), do: format(iban, :compact)
@spec splitted(IbanEx.Iban.t()) :: binary()
def splitted(iban), do: format(iban, :splitted)
@spec format(iban()) :: String.t()
@spec format(iban(), available_format()) :: String.t()
def format(iban, format \\ :compact)
def format(iban, :compact),
do: format(iban, :pretty) |> normalize()
def format(iban, :pretty) do
country_module = Country.country_module(iban.country_code)
country_module.to_s(iban)
end
def format(iban, :splitted) do
compact = format(iban, :compact)
~r/.{1,4}/
|> Regex.scan(compact)
|> List.flatten()
|> Enum.join(" ")
end
end

29
lib/iban_ex/iban.ex Normal file
View File

@@ -0,0 +1,29 @@
defmodule IbanEx.Iban do
alias IbanEx.Formatter
alias IbanEx.{Serialize}
@type t :: %__MODULE__{
country_code: <<_::16>>,
check_digits: String.t(),
bank_code: String.t(),
branch_code: String.t() | nil,
national_check: String.t() | nil,
account_number: String.t()
}
defstruct country_code: "UA", check_digits: nil, bank_code: nil, branch_code: nil, national_check: nil, account_number: nil
@spec to_map(IbanEx.Iban.t()) :: map()
defdelegate to_map(iban), to: Serialize
@spec to_string(IbanEx.Iban.t()) :: binary()
defdelegate to_string(iban), to: Serialize
@spec pretty(IbanEx.Iban.t()) :: binary()
defdelegate pretty(iban), to: Formatter
@spec splitted(IbanEx.Iban.t()) :: binary()
defdelegate splitted(iban), to: Formatter
@spec compact(IbanEx.Iban.t()) :: binary()
defdelegate compact(iban), to: Formatter
end

37
lib/iban_ex/parser.ex Normal file
View File

@@ -0,0 +1,37 @@
defmodule IbanEx.Parser do
alias IbanEx.{Country, Iban, Validator}
import IbanEx.Commons, only: [normalize_and_slice: 2]
@type iban_string() :: String.t()
@type country_code_string() :: <<_::16>>
@type check_digits_string() :: <<_::16>>
@type iban_or_error() :: IbanEx.Iban.t() | {:error, atom()}
@spec parse({:ok, String.t()} | String.t()) :: iban_or_error()
def parse({:ok, iban_string}), do: parse(iban_string)
def parse(iban_string) do
with {:ok, valid_iban} <- Validator.validate(iban_string) do
iban_map = %{
country_code: country_code(valid_iban),
check_digits: check_digits(valid_iban),
}
regex = Country.country_module(iban_map.country_code).rule()
bban = bban(iban_string)
bban_map = for {key, val} <- Regex.named_captures(regex, bban), into: %{}, do: {String.to_atom(key), val}
{:ok, struct(Iban, Map.merge(iban_map, bban_map))}
else
{:error, error_type} -> {:error, error_type}
end
end
@spec country_code(iban_string()) :: country_code_string()
def country_code(iban_string), do: normalize_and_slice(iban_string, 0..1)
@spec check_digits(binary()) :: check_digits_string()
def check_digits(iban_string), do: normalize_and_slice(iban_string, 2..3)
@spec bban(binary()) :: binary()
def bban(iban_string), do: normalize_and_slice(iban_string, 4..-1//1)
end

9
lib/iban_ex/serialize.ex Normal file
View File

@@ -0,0 +1,9 @@
defmodule IbanEx.Serialize do
alias IbanEx.{Iban, Formatter}
@spec to_string(Iban.t()) :: String.t()
def to_string(iban), do: Formatter.format(iban)
@spec to_map(Iban.t()) :: Map.t()
def to_map(iban), do: Map.from_struct(iban)
end

View File

@@ -0,0 +1,46 @@
defmodule IbanEx.Validator.Replacements do
@moduledoc """
Replacements in IBANs for checksums calculations
"""
import IbanEx.Commons, only: [normalize: 1]
@type symbol() :: <<_::8>>
@type value() :: <<_::16>>
@type replacements() :: %{symbol() => value()}
@replacements %{
"A" => "10",
"B" => "11",
"C" => "12",
"D" => "13",
"E" => "14",
"F" => "15",
"G" => "16",
"H" => "17",
"I" => "18",
"J" => "19",
"K" => "20",
"L" => "21",
"M" => "22",
"N" => "23",
"O" => "24",
"P" => "25",
"Q" => "26",
"R" => "27",
"S" => "28",
"T" => "29",
"U" => "30",
"V" => "31",
"W" => "32",
"X" => "33",
"Y" => "34",
"Z" => "35"
}
@spec replacements() :: replacements()
def replacements(), do: @replacements
@spec replace(symbol()) :: value()
def replace(symbol), do: @replacements[normalize(symbol)] || normalize(symbol)
end

View File

@@ -0,0 +1,95 @@
defmodule IbanEx.Validator do
alias IbanEx.{Country, Parser}
alias IbanEx.Validator.Replacements
import IbanEx.Commons, only: [normalize: 1]
@spec validate(String.t()) :: {:ok, String.t()} | {:error}
def validate(iban) do
cond do
iban_violates_format?(iban) ->
{:error, :invalid_format}
iban_unsupported_country?(iban) ->
{:error, :unsupported_country_code}
iban_violates_length?(iban) ->
{:error, :invalid_length}
iban_violates_country_rule?(iban) ->
{:error, :invalid_format}
iban_violates_checksum?(iban) ->
{:error, :invalid_checksum}
true ->
{:ok, normalize(iban)}
end
end
@spec size(String.t()) :: non_neg_integer()
defp size(iban) do
iban
|> normalize()
|> String.length()
end
# - Check whether a given IBAN violates the required format.
@spec iban_violates_format?(String.t()) :: boolean
defp iban_violates_format?(iban),
do: Regex.match?(~r/[^A-Z0-9]/i, normalize(iban))
# - Check whether a given IBAN violates the supported countries.
@spec iban_unsupported_country?(String.t()) :: boolean
defp iban_unsupported_country?(iban) do
supported? =
iban
|> Parser.country_code()
|> Country.is_country_code_supported?()
!supported?
end
# - Check whether a given IBAN violates the required length.
@spec iban_violates_length?(String.t()) :: boolean
defp iban_violates_length?(iban) do
with country_code <- Parser.country_code(iban),
country_module <- Country.country_module(country_code) do
size(iban) != country_module.size()
else
{:error, _} -> true
end
end
# - Check whether a given IBAN violates the country rules.
@spec iban_violates_country_rule?(String.t()) :: boolean
defp iban_violates_country_rule?(iban) do
with country_code <- Parser.country_code(iban),
bban <- Parser.bban(iban),
country_module <- Country.country_module(country_code),
rule <- country_module.rule() do
!Regex.match?(rule, bban)
else
{:error, _} -> true
end
end
# - Check whether a given IBAN violates the required checksum.
@spec iban_violates_checksum?(String.t()) :: boolean
defp iban_violates_checksum?(iban) do
check_sum_base = Parser.bban(iban) <> Parser.country_code(iban) <> "00"
replacements = Replacements.replacements()
remainder =
for(<<c <- check_sum_base>>, into: "", do: replacements[<<c>>] || <<c>>)
|> String.to_integer()
|> rem(97)
checksum =
(98 - remainder)
|> Integer.to_string()
|> String.pad_leading(2, "0")
checksum !== Parser.check_digits(iban)
end
end