defmodule EverFaker.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> EverFaker.Code.Iban.iban "GI88LRCE6SQ3CQJGP3UHAJD" iex> EverFaker.Code.Iban.iban("NL") "NL26VYOC3032097337" iex> EverFaker.Code.Iban.iban(["NL", "BE"]) "NL74YRFX4598109960" iex> EverFaker.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> EverFaker.Code.Iban.iban "GI88LRCE6SQ3CQJGP3UHAJD" iex> EverFaker.Code.Iban.iban "BR0302030320973376033745981CB" iex> EverFaker.Code.Iban.iban "BE98607198979502" iex> EverFaker.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> EverFaker.Code.Iban.iban("FR") "FR650154264610QJGP3UHAJDJ02" iex> EverFaker.Code.Iban.iban("BE") "BE95030320973376" iex> EverFaker.Code.Iban.iban(["NL", "BE"]) "NL31RFXY5981099607" iex> EverFaker.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> EverFaker.Code.Iban.iban("NL", ["ABNA"]) "NL16ABNA0154264610" iex> EverFaker.Code.Iban.iban("MC", ["FOO", "BAR"]) "MC98FOOBAR83" iex> EverFaker.Code.Iban.iban("SM", ["A"]) "SM86A2970523570AY38NWIVZ5XT" iex> EverFaker.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(<>) 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, EverFaker.random_between(0, length(list) - 1)) end end