diff --git a/lib/ex_phone_number/constants/phone_number_types.ex b/lib/ex_phone_number/constants/phone_number_types.ex index 0383941..c305bf1 100644 --- a/lib/ex_phone_number/constants/phone_number_types.ex +++ b/lib/ex_phone_number/constants/phone_number_types.ex @@ -1,4 +1,18 @@ defmodule ExPhoneNumber.Constants.PhoneNumberTypes do + @type t() :: + :fixed_line + | :mobile + | :fixed_line_or_mobile + | :toll_free + | :premium_rate + | :shared_cost + | :voip + | :personal_number + | :pager + | :uan + | :voicemail + | :unknown + def fixed_line(), do: :fixed_line def mobile(), do: :mobile diff --git a/lib/ex_phone_number/constants/validation_results.ex b/lib/ex_phone_number/constants/validation_results.ex index c92859d..7a0fb77 100644 --- a/lib/ex_phone_number/constants/validation_results.ex +++ b/lib/ex_phone_number/constants/validation_results.ex @@ -1,4 +1,6 @@ defmodule ExPhoneNumber.Constants.ValidationResults do + @type t() :: :is_possible | :is_possible_local_only | :invalid_country_code | :too_short | :invalid_length | :too_long + def is_possible(), do: :is_possible def is_possible_local_only(), do: :is_possible_local_only diff --git a/lib/ex_phone_number/model/phone_number.ex b/lib/ex_phone_number/model/phone_number.ex index 483906b..1140a0f 100644 --- a/lib/ex_phone_number/model/phone_number.ex +++ b/lib/ex_phone_number/model/phone_number.ex @@ -75,6 +75,15 @@ defmodule ExPhoneNumber.Model.PhoneNumber do not is_nil(phone_number.raw_input) end + @country_code_default 1 + def get_country_code_or_default(phone_number = %PhoneNumber{}) do + if is_nil(phone_number.country_code) do + @country_code_default + else + phone_number.country_code + end + end + @country_code_source_default CountryCodeSource.from_number_with_plus_sign() def get_country_code_source_or_default(phone_number = %PhoneNumber{}) do if is_nil(phone_number.country_code_source) do diff --git a/lib/ex_phone_number/validation.ex b/lib/ex_phone_number/validation.ex index 155e609..9cf46af 100644 --- a/lib/ex_phone_number/validation.ex +++ b/lib/ex_phone_number/validation.ex @@ -180,6 +180,60 @@ defmodule ExPhoneNumber.Validation do end end + @doc """ + Convenience wrapper around `Validation.is_possible_number_for_type_with_reason?/2` + Instead of returning the reason for failure, this method returns true if the + number is either a possible fully-qualified number (containing the area code + and country code), or if the number could be a possible local number (with a + country code, but missing an area code). Local numbers are considered + possible if they could be possibly dialled in this format: if the area code + is needed for a call to connect, the number is not considered possible + without it. + + Implements `i18n.phonenumbers.PhoneNumberUtil.prototype.isPossibleNumberForType` + """ + @spec is_possible_number_for_type?(%PhoneNumber{}, PhoneNumberTypes.t()) :: ValidationResults.t() + def is_possible_number_for_type?(%PhoneNumber{} = number, type) when is_atom(type) do + result = is_possible_number_for_type_with_reason?(number, type) + + result == ValidationResults.is_possible() || + result == ValidationResults.is_possible_local_only() + end + + @doc """ + Check whether a phone number is a possible number. It provides a more lenient + check than `Validation.is_valid_number/1` in the following sense: + + It only checks the length of phone numbers. In particular, it doesn't + check starting digits of the number. + + For some numbers (particularly fixed-line), many regions have the concept + of area code, which together with subscriber number constitute the national + significant number. It is sometimes okay to dial only the subscriber number + when dialing in the same area. This function will return + :is_possible_local_only if the subscriber-number-only version is passed in. On + the other hand, because is_valid_number validates using information on both + starting digits (for fixed line numbers, that would most likely be area + codes) and length (obviously includes the length of area codes for fixed line + numbers), it will return false for the subscriber-number-only version. + + Implements `i18n.phonenumbers.PhoneNumberUtil.prototype.isPossibleNumberForTypeWithReason` + """ + @spec is_possible_number_for_type_with_reason?(%PhoneNumber{}, PhoneNumberTypes.t()) :: ValidationResults.t() + def is_possible_number_for_type_with_reason?(%PhoneNumber{} = number, type) when is_atom(type) do + national_number = PhoneNumber.get_national_significant_number(number) + country_code = PhoneNumber.get_country_code_or_default(number) + + if not Metadata.is_valid_country_code?(country_code) do + ValidationResults.invalid_country_code() + else + region_code = Metadata.get_region_code_for_country_code(country_code) + metadata = Metadata.get_for_region_code_or_calling_code(country_code, region_code) + + test_number_length_for_type(national_number, metadata, type) + end + end + def is_valid_possible_number_length?(metadata, number) do !Enum.member?( [ diff --git a/test/ex_phone_number/validation_test.exs b/test/ex_phone_number/validation_test.exs index b425bdc..4b6200e 100644 --- a/test/ex_phone_number/validation_test.exs +++ b/test/ex_phone_number/validation_test.exs @@ -255,4 +255,167 @@ defmodule ExPhoneNumber.ValidationTest do assert 4 == get_length_of_national_destination_code(PhoneNumberFixture.international_toll_free()) end end + + describe ".is_possible_number_for_type?/2" do + test "IsPossibleNumberForType_DifferentTypeLengths" do + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number7(), PhoneNumberTypes.fixed_line()) + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number7(), PhoneNumberTypes.unknown()) + + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.fixed_line()) + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.unknown()) + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.mobile()) + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.toll_free()) + + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.fixed_line()) + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.unknown()) + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.mobile()) + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.toll_free()) + + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.fixed_line()) + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.unknown()) + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.mobile()) + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.toll_free()) + + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.unknown()) + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.fixed_line()) + assert is_possible_number_for_type?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.mobile()) + refute is_possible_number_for_type?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.toll_free()) + end + + test "IsPossibleNumberForType_LocalOnly" do + assert is_possible_number_for_type?(PhoneNumberFixture.de_local_only(), PhoneNumberTypes.unknown()) + assert is_possible_number_for_type?(PhoneNumberFixture.de_local_only(), PhoneNumberTypes.fixed_line()) + refute is_possible_number_for_type?(PhoneNumberFixture.de_local_only(), PhoneNumberTypes.mobile()) + end + + test "IsPossibleNumberForType_DataMissingForSizeReasons" do + assert is_possible_number_for_type?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.unknown()) + assert is_possible_number_for_type?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.fixed_line()) + + assert is_possible_number_for_type?(PhoneNumberFixture.br_local_only2(), PhoneNumberTypes.unknown()) + assert is_possible_number_for_type?(PhoneNumberFixture.br_local_only2(), PhoneNumberTypes.fixed_line()) + end + + test "IsPossibleNumberForType_NumberTypeNotSupportedForRegion" do + refute is_possible_number_for_type?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.mobile()) + assert is_possible_number_for_type?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.fixed_line()) + assert is_possible_number_for_type?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.fixed_line_or_mobile()) + + refute is_possible_number_for_type?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.mobile()) + refute is_possible_number_for_type?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.fixed_line()) + refute is_possible_number_for_type?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.fixed_line_or_mobile()) + assert is_possible_number_for_type?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.premium_rate()) + end + end + + describe ".is_possible_number_for_type_with_reason?/2" do + test "IsPossibleNumberForTypeWithReason_DifferentTypeLengths" do + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number7(), PhoneNumberTypes.unknown()) + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number7(), PhoneNumberTypes.fixed_line()) + + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.unknown()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.mobile()) + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number8(), PhoneNumberTypes.toll_free()) + + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.unknown()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.mobile()) + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number9(), PhoneNumberTypes.toll_free()) + + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.unknown()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.mobile()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number10(), PhoneNumberTypes.toll_free()) + + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.unknown()) + assert ValidationResults.too_long() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.mobile()) + assert ValidationResults.too_long() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.ar_number11(), PhoneNumberTypes.toll_free()) + end + + test "IsPossibleNumberForTypeWithReason_LocalOnly" do + assert ValidationResults.is_possible_local_only() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.de_local_only(), PhoneNumberTypes.unknown()) + + assert ValidationResults.is_possible_local_only() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.de_local_only(), PhoneNumberTypes.fixed_line()) + + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.de_local_only(), PhoneNumberTypes.mobile()) + end + + test "IsPossibleNumberForTypeWithReason_DataMissingForSizeReasons" do + assert ValidationResults.is_possible_local_only() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.unknown()) + + assert ValidationResults.is_possible_local_only() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.fixed_line()) + + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_local_only2(), PhoneNumberTypes.unknown()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_local_only2(), PhoneNumberTypes.fixed_line()) + end + + test "IsPossibleNumberForTypeWithReason_NumberTypeNotSupportedForRegion" do + assert ValidationResults.invalid_length() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.mobile()) + + assert ValidationResults.is_possible_local_only() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_local_only(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.invalid_length() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_too_short(), PhoneNumberTypes.mobile()) + + assert ValidationResults.too_short() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_too_short(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.br_too_short(), PhoneNumberTypes.fixed_line()) + + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.international_networks(), PhoneNumberTypes.mobile()) + + assert ValidationResults.too_short() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.international_networks(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.invalid_length() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.international_networks(), PhoneNumberTypes.fixed_line()) + + assert ValidationResults.invalid_length() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.mobile()) + + assert ValidationResults.invalid_length() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.fixed_line()) + + assert ValidationResults.invalid_length() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.is_possible() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.universal_premium_rate(), PhoneNumberTypes.premium_rate()) + end + + test "IsPossibleNumberForTypeWithReason_FixedLineOrMobile" do + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_number(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_number(), PhoneNumberTypes.mobile()) + + assert ValidationResults.is_possible() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_number(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.too_short() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_invalid(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.too_long() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_invalid(), PhoneNumberTypes.mobile()) + + assert ValidationResults.invalid_length() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_invalid(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_number2(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.too_long() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_number2(), PhoneNumberTypes.mobile()) + + assert ValidationResults.is_possible() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_number2(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.too_long() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_too_long(), PhoneNumberTypes.fixed_line()) + assert ValidationResults.too_long() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_too_long(), PhoneNumberTypes.mobile()) + assert ValidationResults.too_long() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_too_long(), PhoneNumberTypes.fixed_line_or_mobile()) + + assert ValidationResults.is_possible() == is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_toll_free(), PhoneNumberTypes.toll_free()) + + assert ValidationResults.too_long() == + is_possible_number_for_type_with_reason?(PhoneNumberFixture.sh_toll_free(), PhoneNumberTypes.fixed_line_or_mobile()) + end + end end diff --git a/test/support/phone_number_fixture.ex b/test/support/phone_number_fixture.ex index 3a48c4f..45e6126 100644 --- a/test/support/phone_number_fixture.ex +++ b/test/support/phone_number_fixture.ex @@ -105,6 +105,41 @@ defmodule ExPhoneNumber.PhoneNumberFixture do } end + def ar_number7() do + %PhoneNumber{ + country_code: 54, + national_number: 12345 + } + end + + def ar_number8() do + %PhoneNumber{ + country_code: 54, + national_number: 123_456 + } + end + + def ar_number9() do + %PhoneNumber{ + country_code: 54, + national_number: 123_456_789 + } + end + + def ar_number10() do + %PhoneNumber{ + country_code: 54, + national_number: 1_234_567_890 + } + end + + def ar_number11() do + %PhoneNumber{ + country_code: 54, + national_number: 12_345_678_901 + } + end + def au_number() do %PhoneNumber{ country_code: 61, @@ -119,6 +154,27 @@ defmodule ExPhoneNumber.PhoneNumberFixture do } end + def br_local_only() do + %PhoneNumber{ + country_code: 55, + national_number: 12_345_678 + } + end + + def br_local_only2() do + %PhoneNumber{ + country_code: 55, + national_number: 1_234_567_890 + } + end + + def br_too_short() do + %PhoneNumber{ + country_code: 55, + national_number: 1_234_567 + } + end + def bs_mobile() do %PhoneNumber{ country_code: 1, @@ -224,6 +280,13 @@ defmodule ExPhoneNumber.PhoneNumberFixture do } end + def de_local_only() do + %PhoneNumber{ + country_code: 49, + national_number: 12 + } + end + def de_premium() do %PhoneNumber{ country_code: 49, @@ -578,6 +641,41 @@ defmodule ExPhoneNumber.PhoneNumberFixture do } end + def sh_number() do + %PhoneNumber{ + country_code: 290, + national_number: 1234 + } + end + + def sh_invalid() do + %PhoneNumber{ + country_code: 290, + national_number: 12345 + } + end + + def sh_number2() do + %PhoneNumber{ + country_code: 290, + national_number: 123_456 + } + end + + def sh_too_long() do + %PhoneNumber{ + country_code: 290, + national_number: 1_234_567 + } + end + + def sh_toll_free() do + %PhoneNumber{ + country_code: 290, + national_number: 12_345_678 + } + end + def us_long_number() do %PhoneNumber{ country_code: 1, @@ -796,4 +894,11 @@ defmodule ExPhoneNumber.PhoneNumberFixture do national_number: 6_502_530_000 } end + + def international_networks() do + %PhoneNumber{ + country_code: 882, + national_number: 1_234_567 + } + end end