defmodule Luhn do @moduledoc """ Functions for validating Credit Card numbers using Luhn checksums. Credit card numbers may be of arbitrary length and in arbitrary base. """ alias __MODULE__.Algorithm @type check_digit :: non_neg_integer @type input :: binary | integer defguardp valid_base(base) when base >= 2 @doc """ Evaluates a given credit card number for its validity, with an optionally provided base. # Examples # Accepts a string iex(1)> Luhn.valid?("378282246310005") true # Or an integer iex(2)> Luhn.valid?(378282246310005) true # Octal iex(3)> Luhn.valid?("11111115", 8) true """ @spec valid?(number :: integer | String.t, base :: integer) :: boolean def valid?(number, base \\ 10) when valid_base(base) do checksum(number, base) |> Kernel.==(0) end @doc """ Calculate the checksum of a number. This is known as the "mod 10" algorithm and the result will be zero if the check digit suffix is valid for the number. iex> Luhn.checksum("79927398713") 0 """ def checksum(number, base \\ 10) @spec checksum(binary, integer) :: integer def checksum(number, base) when is_binary(number) and valid_base(base) do number |> String.to_integer(base) |> checksum(base) end @spec checksum(integer, integer) :: integer def checksum(number, base) when valid_base(base) do number |> Integer.digits(base) |> Enum.reverse |> double(base, 0) |> rem(base) end @doc """ Compute the check digit of a number. iex> Luhn.compute_check_digit("7992739871") 3 iex> Luhn.compute_check_digit(7992739871) 3 """ @spec compute_check_digit(input) :: check_digit def compute_check_digit(n), do: Algorithm.compute_check_digit(n) @doc """ Compute and append the check digit for a given number. iex> Luhn.append_check_digit("7992739871") "79927398713" """ @spec append_check_digit(binary) :: binary def append_check_digit(n) do check_digit = Algorithm.compute_check_digit(n) n <> to_string(check_digit) end @doc false @spec double([integer, ...], integer, integer) :: integer def double([], _, acc), do: acc def double([x], _, acc), do: x + acc def double([x,y|tail], base, acc), do: double(tail, base, acc + x + sum(y * 2, base)) @spec sum(integer, integer) :: integer defp sum(number, base) when number >= base, do: number - base + 1 defp sum(number, _), do: number defmodule Algorithm do @moduledoc false def compute_check_digit(n, base \\ 10) do sum = sum_digits(n, base) rem(base - (rem(sum, base)), base) end def sum_digits(n, base) when is_binary(n) do rev_digits = n |> String.graphemes() |> List.foldl([], fn x, acc -> [String.to_integer(x, base) | acc] end) digits = fn x -> Integer.digits(x, base) end luhn_sum(rev_digits, 2, digits) end def sum_digits(n, base) when is_integer(n) do digits = fn x -> Integer.digits(x, base) end rev_digits = n |> digits.() |> Enum.reverse() luhn_sum(rev_digits, 2, digits) end def luhn_sum([], _multiplier, _digits), do: 0 def luhn_sum([x | xs], multiplier, digits) do case multiplier do 1 -> x + luhn_sum(xs, 2, digits) 2 -> sum = (x * multiplier) |> digits.() |> Enum.sum() sum + luhn_sum(xs, 1, digits) end end end end