lib/faker/code/iban.ex

defmodule Faker.Code.Iban do
  @moduledoc """
  Functions for generating IBANs (International Bank Account Numbers).

  The generated IBANs should pass validators that check the checksum, country code, format and
  length of the IBAN.

  When more precision is required, you can pass predefined components that will be included in the
  generated IBAN. The components will not be validated, but are used when calculating the checksum.

  ## Examples

      iex> Faker.Code.Iban.iban
      "GI88LRCE6SQ3CQJGP3UHAJD"
      iex> Faker.Code.Iban.iban("NL")
      "NL26VYOC3032097337"
      iex> Faker.Code.Iban.iban(["NL", "BE"])
      "NL74YRFX4598109960"
      iex> Faker.Code.Iban.iban(["NL", "BE"])
      "BE31198979502980"
  """

  @alpha ~w(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
  @numeric ~w(0 1 2 3 4 5 6 7 8 9)
  @alpha_numeric @numeric ++ @alpha

  @iso_iban_specs [
    AL: [n: 8, c: 16],
    AD: [n: 8, c: 12],
    AT: [n: 16],
    AZ: [a: 4, c: 20],
    BH: [a: 4, c: 14],
    BE: [n: 12],
    BA: [n: 16],
    BR: [n: 23, a: 1, c: 1],
    BG: [a: 4, n: 6, c: 8],
    CR: [n: 17],
    HR: [n: 17],
    CY: [n: 8, c: 16],
    CZ: [n: 20],
    DK: [n: 14],
    FO: [n: 14],
    GL: [n: 14],
    DO: [c: 4, n: 20],
    EE: [n: 16],
    FI: [n: 14],
    FR: [n: 10, c: 11, n: 2],
    GE: [a: 2, n: 16],
    DE: [n: 18],
    GI: [a: 4, c: 15],
    GR: [n: 7, c: 16],
    GT: [c: 24],
    HU: [n: 24],
    IS: [n: 22],
    IE: [a: 4, n: 14],
    IL: [n: 19],
    IT: [a: 1, n: 10, c: 12],
    KW: [a: 4, c: 22],
    KZ: [n: 3, c: 13],
    LV: [a: 4, c: 13],
    LB: [n: 4, c: 20],
    LI: [n: 5, c: 12],
    LT: [n: 16],
    LU: [n: 3, c: 13],
    MK: [n: 3, c: 10, n: 2],
    MT: [a: 4, n: 5, c: 18],
    MR: [n: 23],
    MU: [a: 4, n: 19, a: 3],
    MD: [c: 20],
    MC: [n: 10, c: 11, n: 2],
    ME: [n: 18],
    NL: [a: 4, n: 10],
    NO: [n: 11],
    PK: [a: 4, c: 16],
    PS: [a: 4, c: 21],
    PL: [n: 24],
    PT: [n: 21],
    RO: [a: 4, c: 16],
    SM: [a: 1, n: 10, c: 12],
    SA: [n: 2, c: 18],
    RS: [n: 18],
    SK: [n: 20],
    SI: [n: 15],
    ES: [n: 20],
    SE: [n: 20],
    CH: [n: 5, c: 12],
    TN: [n: 20],
    TR: [n: 5, c: 17],
    AE: [n: 19],
    GB: [a: 4, n: 14],
    VG: [a: 4, n: 16]
  ]

  @spec iban() :: binary
  @doc """
  Returns a random IBAN from a random country

  ## Examples

      iex> Faker.Code.Iban.iban
      "GI88LRCE6SQ3CQJGP3UHAJD"
      iex> Faker.Code.Iban.iban
      "BR0302030320973376033745981CB"
      iex> Faker.Code.Iban.iban
      "BE98607198979502"
      iex> Faker.Code.Iban.iban
      "PT72807856869061130164499"
  """
  def iban, do: iban(Keyword.keys(@iso_iban_specs))

  @spec iban(binary | [binary]) :: binary
  @doc """
  Returns a random IBAN for a specific country code, or a random country code from a given list of
  country codes.

  ## Examples

      iex> Faker.Code.Iban.iban("FR")
      "FR650154264610QJGP3UHAJDJ02"
      iex> Faker.Code.Iban.iban("BE")
      "BE95030320973376"
      iex> Faker.Code.Iban.iban(["NL", "BE"])
      "NL31RFXY5981099607"
      iex> Faker.Code.Iban.iban(["BE", "DE"])
      "DE57989795029807856869"
  """
  def iban(country_code_or_codes), do: iban(country_code_or_codes, [])

  @doc """
  Returns a random IBAN starting with the given components. The given components are not validated
  but are included in the checksum.

  ## Examples

      iex> Faker.Code.Iban.iban("NL", ["ABNA"])
      "NL16ABNA0154264610"
      iex> Faker.Code.Iban.iban("MC", ["FOO", "BAR"])
      "MC98FOOBAR83"
      iex> Faker.Code.Iban.iban("SM", ["A"])
      "SM86A2970523570AY38NWIVZ5XT"
      iex> Faker.Code.Iban.iban("MC", ["FOO", "BAR"])
      "MC40FOOBAR60"
  """
  @spec iban(atom | binary | [binary], [binary]) :: binary
  def iban(country_code, prefix_components) when is_binary(country_code),
    do: iban(String.to_atom(country_code), prefix_components)

  def iban(country_codes, prefix_components) when is_list(country_codes),
    do: iban(sample(country_codes), prefix_components)

  def iban(country_code, prefix_components) when is_atom(country_code) do
    spec = Keyword.fetch!(@iso_iban_specs, country_code)
    {_, spec} = Enum.split(spec, length(prefix_components))
    bban = Enum.join(prefix_components) <> random_bban(spec)
    checksum = calculate_checksum(country_code, bban)
    "#{country_code}#{checksum}#{bban}"
  end

  defp calculate_checksum(country_code, bban) do
    checksum = 98 - rem(fragment_to_number("#{bban}#{country_code}") * 100, 97)
    if checksum < 10, do: "0#{checksum}", else: "#{checksum}"
  end

  defp fragment_to_number(fragment) do
    fragment
    |> fragment_to_numeric_string()
    |> String.to_integer()
  end

  defp fragment_to_numeric_string(<<character::utf8, tail::binary>>) do
    character = IO.iodata_to_binary([character])
    "#{Enum.find_index(@alpha_numeric, &(character == &1))}#{fragment_to_numeric_string(tail)}"
  end

  defp fragment_to_numeric_string(""), do: ""

  defp random_bban([entry]), do: random_bban(entry)
  defp random_bban([entry | tail]), do: random_bban(entry) <> random_bban(tail)
  defp random_bban([]), do: ""
  defp random_bban({:n, n}), do: random_bban(n, @numeric)
  defp random_bban({:a, n}), do: random_bban(n, @alpha)
  defp random_bban({:c, n}), do: random_bban(n, @alpha_numeric)

  defp random_bban(n, type) do
    n
    |> sample_list(type)
    |> Enum.join("")
  end

  defp sample_list(n, list) do
    Enum.map(0..(n - 1), fn _ -> sample(list) end)
  end

  defp sample(list) do
    Enum.fetch!(list, Faker.random_between(0, length(list) - 1))
  end
end