Packages

An Elixir library to validate and format chilean ID/TAX number ('RUN/RUT')

Current section

Files

Jump to
ex_rut lib ex_rut.ex
Raw

lib/ex_rut.ex

defmodule ExRut do
@moduledoc """
An Elixir library to validate and format chilean ID/TAX number ('RUN/RUT')
"""
@regex_rut ~r/^((?'num'\d{1,3}(?:([\.\,]?)\d{1,3}){2})(-?)(?'dv'[\dkK]))$/
@defaults [
delimiter: ".",
show_dv: true
]
def valid?(rut) when is_binary(rut) do
case get_rut_values(rut) do
{:ok, number, dv} ->
calculated_dv = calculate_dv_value(number)
calculated_dv == dv
{:error, :invalid_value} ->
false
end
end
def valid?(rut) when is_integer(rut) do
rut
|> Integer.to_string
|> valid?
end
def valid?(rut) when is_nil(rut), do: false
def valid?(_), do: false
def calculate_dv(number) when is_integer(number) do
{:ok, calculate_dv_value(number)}
end
def calculate_dv(number) when is_binary(number) do
number = clean_rut(number)
case Integer.parse(number) do
{number_as_int, ""} ->
{:ok, calculate_dv_value(number_as_int)}
{_num_as_int, _} ->
{:error, :invalid_value}
:error ->
{:error, :invalid_value}
end
end
def calculate_dv!(number) do
case calculate_dv(number) do
{:ok, dv} ->
dv
{:error, :invalid_value} ->
raise "Invalid Value"
end
end
def format(rut, options \\ [])
def format(rut, options) when is_binary(rut) do
if valid?(rut) do
options = Keyword.merge(@defaults, options)
delimiter = Keyword.get(options, :delimiter)
show_dv = Keyword.get(options, :show_dv)
{:ok, num, dv} = get_rut_values(rut)
dv = if show_dv, do: dv, else: nil
dash = if show_dv, do: "-", else: ""
formatted_rut =
[delimit_integer(num, delimiter), dv]
|> Enum.reject(&is_nil/1)
|> Enum.join(dash)
{:ok, formatted_rut}
else
{:error, :invalid_value}
end
end
def format(rut, options) when is_integer(rut) do
format(Integer.to_string(rut), options)
end
def format!(number, options \\ []) do
case format(number, options) do
{:ok, formatted_rut} ->
formatted_rut
{:error, :invalid_value} ->
raise "Invalid Value"
end
end
defp calculate_dv_value(number) when is_integer(number) do
calculated_dv =
number
|> split_integer
|> Enum.reverse
|> Enum.with_index(2)
|> Enum.map(fn {n, index} ->
if index > 7 do
{n, (index-6)}
else
{n, index}
end
end)
|> Enum.reduce(0, fn {n, index}, acc ->
acc + (n * index)
end)
|> Kernel.rem(11)
|> Kernel.-(11)
|> Kernel.abs
cond do
calculated_dv == 11 -> "0"
calculated_dv == 10 -> "k"
true -> Integer.to_string(calculated_dv)
end
end
defp get_rut_values(rut) do
rut = clean_rut(rut)
cond do
# valid format
Regex.match?(@regex_rut, rut) ->
get_clean_rut_values(@regex_rut, rut)
# invalid format
true ->
{:error, :invalid_value}
end
end
defp get_clean_rut_values(regex, rut) do
rut = clean_rut(rut)
%{"num" => number, "dv" => dv} = Regex.named_captures(regex, rut)
dv = String.downcase(dv)
case Integer.parse(number) do
{number_as_int, ""} ->
{:ok, number_as_int, dv}
:error ->
{:error, :invalid_rut}
end
end
defp clean_rut(rut) do
rut
|> String.replace(".", "")
|> String.replace(",", "")
end
defp split_integer(number) when is_integer(number) do
number
|> Kernel.to_string()
|> String.split("", trim: true)
|> Enum.map(fn int_string ->
Integer.parse(int_string)
|> case do
{int, _} ->
int
_ ->
0
end
end)
end
defp delimit_integer(number, delimiter) when is_binary(number) do
case Integer.parse(number) do
{number_as_int, ""} -> delimit_integer(number_as_int, delimiter)
:error -> number
end
end
defp delimit_integer(number, delimiter) when is_integer(number) do
integer =
abs(number)
|> Integer.to_charlist()
|> :lists.reverse()
|> delimit_integer(delimiter, [])
Enum.join([integer])
end
defp delimit_integer([a, b, c, d | tail], delimiter, acc) do
delimit_integer([d | tail], delimiter, [delimiter, c, b, a | acc])
end
defp delimit_integer(list, _, acc) do
:lists.reverse(list) ++ acc
end
end