From dc1b802c778daf5e52e7bb42120514c4c399f7d3 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Tue, 14 May 2024 23:09:24 -0400 Subject: [PATCH] Partial BBAN parsing --- lib/iban_ex/commons/commons.ex | 5 +++++ lib/iban_ex/country/template.ex | 25 ++++++++++++++++++++++- lib/iban_ex/parser.ex | 35 ++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/lib/iban_ex/commons/commons.ex b/lib/iban_ex/commons/commons.ex index a7c33c4..9641e25 100644 --- a/lib/iban_ex/commons/commons.ex +++ b/lib/iban_ex/commons/commons.ex @@ -1,6 +1,11 @@ defmodule IbanEx.Commons do @moduledoc false + @spec blank(nil | binary()) :: nil | binary() + def blank(nil), do: nil + def blank(""), do: nil + def blank(string) when is_binary(string), do: string + @spec normalize(binary()) :: binary() def normalize(string) do string diff --git a/lib/iban_ex/country/template.ex b/lib/iban_ex/country/template.ex index 8d413bc..89e97fd 100644 --- a/lib/iban_ex/country/template.ex +++ b/lib/iban_ex/country/template.ex @@ -9,6 +9,7 @@ defmodule IbanEx.Country.Template do @callback size() :: size() @callback rule() :: rule() + @callback incomplete_rule() :: rule() @callback to_string(Iban.t(), joiner()) :: String.t() @callback to_string(Iban.t()) :: String.t() @@ -39,11 +40,33 @@ defmodule IbanEx.Country.Template do @spec size() :: integer() def size(), do: @size + @doc """ + Return Regex for parsing complete BBAN (part of IBAN string) + """ @impl IbanEx.Country.Template @spec rule() :: Regex.t() def rule(), do: @rule - defoverridable to_string: 1, to_string: 2, size: 0, rule: 0 + @doc """ + Return Regex without trailing “$” for parsing incomplete BBAN (part of IBAN string) (for partial suggestions) + """ + @impl IbanEx.Country.Template + @spec incomplete_rule() :: Regex.t() + def incomplete_rule() do + source = + @rule + |> Regex.source() + |> String.slice(0..-2//1) + |> String.replace("{", "{0,") + + opts = + @rule + |> Regex.opts() + + Regex.compile!(source, opts) + end + + defoverridable to_string: 1, to_string: 2, size: 0, rule: 0, incomplete_rule: 0 end end end diff --git a/lib/iban_ex/parser.ex b/lib/iban_ex/parser.ex index badfa88..6d00fcc 100644 --- a/lib/iban_ex/parser.ex +++ b/lib/iban_ex/parser.ex @@ -2,7 +2,7 @@ defmodule IbanEx.Parser do @moduledoc false alias IbanEx.{Country, Iban, Validator} - import IbanEx.Commons, only: [normalize_and_slice: 2] + import IbanEx.Commons, only: [normalize_and_slice: 2, blank: 1] @type iban_string() :: String.t() @type country_code_string() :: <<_::16>> @@ -42,19 +42,36 @@ defmodule IbanEx.Parser do end @spec parse_bban(binary(), <<_::16>>) :: map() - def parse_bban(bban_string, country_code) do - regex = Country.country_module(country_code).rule() - for {key, val} <- Regex.named_captures(regex, bban_string), - into: %{}, - do: {String.to_atom(key), val} + def parse_bban(bban_string, country_code, options \\ [incomplete: false]) + + def parse_bban(bban_string, country_code, incomplete: true) do + Country.country_module(country_code).incomplete_rule() + |> parse_bban_by_regex(bban_string) + end + + def parse_bban(bban_string, country_code, _options) do + Country.country_module(country_code).rule() + |> parse_bban_by_regex(bban_string) + end + + defp parse_bban_by_regex(regex, bban_string) do + case Regex.named_captures(regex, bban_string) do + map when is_map(map) -> + for {key, val} <- map, + into: %{}, + do: {String.to_atom(key), blank(val)} + + nil -> + %{} + end end @spec country_code(iban_string()) :: country_code_string() - def country_code(iban_string), do: normalize_and_slice(iban_string, 0..1) + def country_code(iban_string), do: normalize_and_slice(iban_string, 0..1) |> blank() @spec check_digits(binary()) :: check_digits_string() - def check_digits(iban_string), do: normalize_and_slice(iban_string, 2..3) + def check_digits(iban_string), do: normalize_and_slice(iban_string, 2..3) |> blank() @spec bban(binary()) :: binary() - def bban(iban_string), do: normalize_and_slice(iban_string, 4..-1//1) + def bban(iban_string), do: normalize_and_slice(iban_string, 4..-1//1) |> blank() end