2196 lines
101 KiB
Rust
2196 lines
101 KiB
Rust
use std::{
|
|
borrow::Cow, cmp::max, collections::{HashMap, HashSet, VecDeque}, sync::Arc
|
|
};
|
|
|
|
use super::phone_number_regexps_and_mappings::PhoneNumberRegExpsAndMappings;
|
|
use crate::{
|
|
i18n, interfaces::MatcherApi, macros::owned_from_cow_or, phonenumberutil::{
|
|
errors::{ExtractNumberError, GetExampleNumberError, ParseError, PhoneNumberUtilError}, helper_constants::{
|
|
DEFAULT_EXTN_PREFIX, MAX_LENGTH_COUNTRY_CODE, MAX_LENGTH_FOR_NSN, MIN_LENGTH_FOR_NSN, NANPA_COUNTRY_CODE, PLUS_SIGN, REGION_CODE_FOR_NON_GEO_ENTITY, RFC3966_EXTN_PREFIX, RFC3966_ISDN_SUBADDRESS, RFC3966_PHONE_CONTEXT, RFC3966_PREFIX
|
|
}, helper_functions::{
|
|
self, get_number_desc_by_type, get_supported_types_for_metadata, load_compiled_metadata, normalize_helper, prefix_number_with_country_calling_code, test_number_length_with_unknown_type
|
|
}, helper_types::{PhoneNumberAndCarrierCode, PhoneNumberWithCountryCodeSource}, PhoneNumberFormat, PhoneNumberType, ValidNumberLenType, ValidationResultErr
|
|
}, proto_gen::{
|
|
phonemetadata::{NumberFormat, PhoneMetadata, PhoneNumberDesc},
|
|
phonenumber::{phone_number::CountryCodeSource, PhoneNumber},
|
|
}, regex_based_matcher::RegexBasedMatcher, regex_util::{RegexConsume, RegexFullMatch}, regexp_cache::ErrorInvalidRegex, string_util::strip_cow_prefix
|
|
};
|
|
|
|
use dec_from_char::DecimalExtended;
|
|
use log::{error, trace, warn};
|
|
use regex::Regex;
|
|
|
|
// Helper type for Result
|
|
pub type Result<T> = std::result::Result<T, PhoneNumberUtilError>;
|
|
|
|
pub type RegexResult<T> = std::result::Result<T, ErrorInvalidRegex>;
|
|
|
|
pub type ParseResult<T> = std::result::Result<T, ParseError>;
|
|
|
|
pub type ExampleNumberResult = std::result::Result<PhoneNumber, GetExampleNumberError>;
|
|
|
|
pub struct PhoneNumberUtil {
|
|
/// An API for validation checking.
|
|
matcher_api: Box<dyn MatcherApi>,
|
|
|
|
/// Helper class holding useful regular expressions and character mappings.
|
|
reg_exps: PhoneNumberRegExpsAndMappings,
|
|
|
|
/// A mapping from a country calling code to a RegionCode object which denotes
|
|
/// NANPA share the country calling code 1 and Russia and Kazakhstan share the
|
|
/// country calling code 7. Under this map, 1 is mapped to region code "US" and
|
|
/// 7 is mapped to region code "RU". This is implemented as a sorted vector to
|
|
/// achieve better performance.
|
|
country_calling_code_to_region_code_map: Vec<(i32, Vec<String>)>,
|
|
|
|
nanpa_regions: HashSet<String>,
|
|
|
|
/// A mapping from a region code to a PhoneMetadata for that region.
|
|
region_to_metadata_map: HashMap<String, PhoneMetadata>,
|
|
|
|
/// A mapping from a country calling code for a non-geographical entity to the
|
|
/// PhoneMetadata for that country calling code. Examples of the country
|
|
/// calling codes include 800 (International Toll Free Service) and 808
|
|
/// (International Shared Cost Service).
|
|
country_code_to_non_geographical_metadata_map: HashMap<i32, PhoneMetadata>,
|
|
}
|
|
|
|
impl PhoneNumberUtil {
|
|
pub(super) fn new() -> Self {
|
|
let mut instance = Self {
|
|
matcher_api: Box::new(RegexBasedMatcher::new()),
|
|
reg_exps: PhoneNumberRegExpsAndMappings::new(),
|
|
country_calling_code_to_region_code_map: Default::default(),
|
|
nanpa_regions: Default::default(),
|
|
region_to_metadata_map: Default::default(),
|
|
country_code_to_non_geographical_metadata_map: Default::default(),
|
|
};
|
|
let metadata_collection = match load_compiled_metadata() {
|
|
Err(err) => {
|
|
let err_message = format!("Could not parse compiled-in metadata: {:?}", err);
|
|
log::error!("{}", err_message);
|
|
panic!("{}", err_message);
|
|
}
|
|
Ok(metadata) => metadata,
|
|
};
|
|
// that share a country calling code when inserting data.
|
|
let mut country_calling_code_to_region_map = HashMap::<i32, VecDeque<String>>::new();
|
|
for metadata in metadata_collection.metadata {
|
|
let region_code = &metadata.id().to_string();
|
|
let main_country_code = metadata.main_country_for_code();
|
|
if i18n::RegionCode::get_unknown() == region_code {
|
|
continue;
|
|
}
|
|
|
|
let country_calling_code = metadata.country_code();
|
|
if REGION_CODE_FOR_NON_GEO_ENTITY == region_code {
|
|
instance
|
|
.country_code_to_non_geographical_metadata_map
|
|
.insert(country_calling_code, metadata);
|
|
} else {
|
|
instance
|
|
.region_to_metadata_map
|
|
.insert(region_code.to_owned(), metadata);
|
|
}
|
|
|
|
let calling_code_in_map_o =
|
|
country_calling_code_to_region_map.get_mut(&country_calling_code);
|
|
if let Some(calling_code_in) = calling_code_in_map_o {
|
|
if main_country_code {
|
|
calling_code_in.push_front(region_code.to_owned());
|
|
} else {
|
|
calling_code_in.push_back(region_code.to_owned());
|
|
}
|
|
} else {
|
|
// For most country calling codes, there will be only one region code.
|
|
let mut list_with_region_code = VecDeque::new();
|
|
list_with_region_code.push_back(region_code.to_owned());
|
|
country_calling_code_to_region_map
|
|
.insert(country_calling_code, list_with_region_code);
|
|
}
|
|
if country_calling_code == NANPA_COUNTRY_CODE {
|
|
instance.nanpa_regions.insert(region_code.to_owned());
|
|
}
|
|
}
|
|
|
|
instance.country_calling_code_to_region_code_map.extend(
|
|
country_calling_code_to_region_map.into_iter().map(| (k, v) | {
|
|
(k, Vec::from(v))
|
|
})
|
|
);
|
|
// Sort all the pairs in ascending order according to country calling code.
|
|
instance
|
|
.country_calling_code_to_region_code_map
|
|
.sort_by_key(|(a, _)| *a);
|
|
instance
|
|
}
|
|
|
|
pub fn get_supported_regions(&self) -> Vec<&str> {
|
|
let mut regions = Vec::new();
|
|
for (k, _) in self.region_to_metadata_map.iter() {
|
|
regions.push(k.as_str());
|
|
}
|
|
regions
|
|
}
|
|
|
|
pub fn get_supported_global_network_calling_codes(&self) -> HashSet<i32> {
|
|
let mut codes = HashSet::new();
|
|
for (k, _) in self.country_code_to_non_geographical_metadata_map.iter() {
|
|
codes.insert(*k);
|
|
}
|
|
codes
|
|
}
|
|
|
|
pub fn get_supported_calling_codes(&self) -> HashSet<i32> {
|
|
let mut codes = HashSet::new();
|
|
|
|
for (k, _) in self.country_calling_code_to_region_code_map.iter() {
|
|
codes.insert(*k);
|
|
}
|
|
codes
|
|
}
|
|
|
|
pub fn get_supported_types_for_region(
|
|
&self,
|
|
region_code: &str,
|
|
) -> Option<HashSet<PhoneNumberType>> {
|
|
self.region_to_metadata_map
|
|
.get(region_code)
|
|
.and_then(|metadata| Some(get_supported_types_for_metadata(metadata)))
|
|
.or_else(|| {
|
|
warn!("Invalid or unknown region code provided: {}", region_code);
|
|
None
|
|
})
|
|
}
|
|
|
|
pub fn get_supported_types_for_non_geo_entity(
|
|
&self,
|
|
country_calling_code: i32,
|
|
) -> Option<HashSet<PhoneNumberType>> {
|
|
self.country_code_to_non_geographical_metadata_map
|
|
.get(&country_calling_code)
|
|
.and_then(|metadata| Some(get_supported_types_for_metadata(metadata)))
|
|
.or_else(|| {
|
|
warn!(
|
|
"Unknown country calling code for a non-geographical entity provided: {}",
|
|
country_calling_code
|
|
);
|
|
None
|
|
})
|
|
}
|
|
|
|
fn get_extn_patterns_for_matching(&self) -> &str {
|
|
return &self.reg_exps.extn_patterns_for_matching;
|
|
}
|
|
|
|
fn starts_with_plus_chars_pattern(&self, phone_number: &str) -> bool {
|
|
self.reg_exps
|
|
.plus_chars_pattern
|
|
.matches_start(phone_number)
|
|
}
|
|
|
|
fn contains_only_valid_digits(&self, s: &str) -> bool {
|
|
self.reg_exps.digits_pattern.full_match(s)
|
|
}
|
|
|
|
fn trim_unwanted_end_chars<'a>(&self, phone_number: &'a str) -> &'a str {
|
|
let mut bytes_to_trim = 0;
|
|
|
|
for char in phone_number.chars().rev() {
|
|
if !self
|
|
.reg_exps
|
|
.unwanted_end_char_pattern
|
|
.full_match(&char.to_string())
|
|
{
|
|
break;
|
|
}
|
|
bytes_to_trim += char.len_utf8();
|
|
}
|
|
|
|
if bytes_to_trim > 0 {
|
|
let new_len = phone_number.len() - bytes_to_trim;
|
|
&phone_number[..new_len]
|
|
} else {
|
|
phone_number
|
|
}
|
|
}
|
|
|
|
fn is_format_eligible_for_as_you_type_formatter(&self, format: &str) -> bool {
|
|
// We require that the first
|
|
// group is present in the output pattern to ensure no data is lost while
|
|
// formatting; when we format as you type, this should always be the case.
|
|
return self
|
|
.reg_exps
|
|
.is_format_eligible_as_you_type_formatting_regex
|
|
.full_match(format);
|
|
}
|
|
|
|
fn formatting_rule_has_first_group_only(&self, national_prefix_formatting_rule: &str) -> bool {
|
|
// A pattern that is used to determine if the national prefix formatting rule
|
|
// has the first group only, i.e., does not start with the national prefix.
|
|
// Note that the pattern explicitly allows for unbalanced parentheses.
|
|
let first_group_only_prefix_pattern = self
|
|
.reg_exps
|
|
.regexp_cache
|
|
.get_regex("\\(?\\$1\\)?")
|
|
.expect("Invalid constant pattern!");
|
|
return national_prefix_formatting_rule.is_empty()
|
|
|| first_group_only_prefix_pattern.full_match(national_prefix_formatting_rule);
|
|
}
|
|
|
|
fn get_ndd_prefix_for_region(
|
|
&self,
|
|
region_code: &str,
|
|
strip_non_digits: bool,
|
|
) -> String {
|
|
self.region_to_metadata_map
|
|
.get(region_code)
|
|
.map(|metadata| {
|
|
let mut prefix = metadata.national_prefix().to_owned();
|
|
if strip_non_digits {
|
|
prefix = prefix.replace("~", "");
|
|
}
|
|
prefix
|
|
})
|
|
.unwrap_or_else(|| {
|
|
warn!("Invalid or unknown region code ({}) provided.", region_code);
|
|
"".to_string()
|
|
})
|
|
}
|
|
|
|
/// 'hot' function wrapper for region_to_metadata_map.get
|
|
fn get_metadata_for_region(&self, region_code: &str) -> Option<&PhoneMetadata> {
|
|
return self.region_to_metadata_map.get(region_code)
|
|
}
|
|
|
|
fn format<'b>(
|
|
&self,
|
|
phone_number: &'b PhoneNumber,
|
|
number_format: PhoneNumberFormat,
|
|
) -> RegexResult<Cow<'b, str>> {
|
|
if phone_number.national_number() == 0 {
|
|
let raw_input = phone_number.raw_input();
|
|
if !raw_input.is_empty() {
|
|
// Unparseable numbers that kept their raw input just use that.
|
|
// This is the only case where a number can be formatted as E164 without a
|
|
// leading '+' symbol (but the original number wasn't parseable anyway).
|
|
// TODO: Consider removing the 'if' above so that unparseable
|
|
// strings without raw input format to the empty string instead of "+00".
|
|
return Ok(Cow::Borrowed(raw_input));
|
|
}
|
|
}
|
|
let country_calling_code = phone_number.country_code();
|
|
let mut formatted_number = Self::get_national_significant_number(phone_number);
|
|
|
|
if matches!(number_format, PhoneNumberFormat::E164) {
|
|
// Early exit for E164 case (even if the country calling code is invalid)
|
|
// since no formatting of the national number needs to be applied.
|
|
// Extensions are not formatted.
|
|
prefix_number_with_country_calling_code(
|
|
country_calling_code,
|
|
PhoneNumberFormat::E164,
|
|
&mut formatted_number,
|
|
);
|
|
return Ok(Cow::Owned(formatted_number));
|
|
}
|
|
// Note here that all NANPA formatting rules are contained by US, so we use
|
|
// rules are contained by Russia. French Indian Ocean country rules are
|
|
// contained by Réunion.
|
|
let region_code = self.get_region_code_for_country_code(country_calling_code);
|
|
let metadata =
|
|
self.get_metadata_for_region_or_calling_code(country_calling_code, ®ion_code);
|
|
|
|
if let Some(metadata) = metadata {
|
|
if let Cow::Owned(s) = self.format_nsn(&formatted_number, metadata, number_format)? {
|
|
formatted_number = s;
|
|
}
|
|
if let Some(formatted_extension) =
|
|
Self::get_formatted_extension(phone_number, metadata, number_format)
|
|
{
|
|
formatted_number.push_str(&formatted_extension);
|
|
}
|
|
prefix_number_with_country_calling_code(
|
|
country_calling_code,
|
|
number_format,
|
|
&mut formatted_number,
|
|
);
|
|
}
|
|
Ok(Cow::Owned(formatted_number))
|
|
}
|
|
|
|
fn get_national_significant_number(phone_number: &PhoneNumber) -> String {
|
|
let zeros_start = if phone_number.italian_leading_zero() {
|
|
"0".repeat(max(phone_number.number_of_leading_zeros() as usize, 0))
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
let mut buf = itoa::Buffer::new();
|
|
let national_number = buf.format(phone_number.national_number());
|
|
|
|
// If leading zero(s) have been set, we prefix this now. Note this is not a
|
|
// national prefix. Ensure the number of leading zeros is at least 0 so we
|
|
// don't crash in the case of malicious input.
|
|
return fast_cat::concat_str!(&zeros_start, national_number);
|
|
}
|
|
|
|
/// Returns the region code that matches the specific country calling code. In
|
|
/// the case of no region code being found, the unknown region code will be
|
|
/// returned.
|
|
fn get_region_code_for_country_code(&self, country_calling_code: i32) -> &str {
|
|
let region_codes = self.get_region_codes_for_country_calling_code(country_calling_code);
|
|
return region_codes
|
|
.first()
|
|
.map(|v| *v)
|
|
.unwrap_or(i18n::RegionCode::get_unknown());
|
|
}
|
|
|
|
// Returns the region codes that matches the specific country calling code. In
|
|
// the case of no region code being found, region_codes will be left empty.
|
|
fn get_region_codes_for_country_calling_code(
|
|
&self,
|
|
country_calling_code: i32,
|
|
) -> Vec<&str> {
|
|
let mut acc = Vec::with_capacity(10);
|
|
// Create a IntRegionsPair with the country_code passed in, and use it to
|
|
// locate the pair with the same country_code in the sorted vector.
|
|
self.country_calling_code_to_region_code_map
|
|
.binary_search_by_key(&country_calling_code, |(code, _)| *code)
|
|
.map(|index| {
|
|
self.country_calling_code_to_region_code_map[index]
|
|
.1
|
|
.iter()
|
|
.for_each(|v| {
|
|
acc.push(v.as_str());
|
|
});
|
|
}) /* suppress Result ignoring */
|
|
.ok();
|
|
acc
|
|
}
|
|
|
|
fn get_metadata_for_region_or_calling_code(
|
|
&self,
|
|
country_calling_code: i32,
|
|
region_code: &str,
|
|
) -> Option<&PhoneMetadata> {
|
|
return if REGION_CODE_FOR_NON_GEO_ENTITY == region_code {
|
|
self.country_code_to_non_geographical_metadata_map
|
|
.get(&country_calling_code)
|
|
} else {
|
|
self.region_to_metadata_map.get(region_code)
|
|
};
|
|
}
|
|
|
|
fn format_nsn<'b>(
|
|
&self,
|
|
phone_number: &'b str,
|
|
metadata: &PhoneMetadata,
|
|
number_format: PhoneNumberFormat,
|
|
) -> RegexResult<Cow<'b, str>> {
|
|
self.format_nsn_with_carrier(phone_number, metadata, number_format, "")
|
|
}
|
|
|
|
fn format_nsn_with_carrier<'b>(
|
|
&self,
|
|
number: &'b str,
|
|
metadata: &PhoneMetadata,
|
|
number_format: PhoneNumberFormat,
|
|
carrier_code: &str,
|
|
) -> RegexResult<Cow<'b, str>> {
|
|
// When the intl_number_formats exists, we use that to format national number
|
|
// for the INTERNATIONAL format instead of using the number_formats.
|
|
let available_formats = if metadata.intl_number_format.len() == 0
|
|
|| number_format == PhoneNumberFormat::National
|
|
{
|
|
&metadata.number_format
|
|
} else {
|
|
&metadata.intl_number_format
|
|
};
|
|
let formatting_pattern =
|
|
self.choose_formatting_pattern_for_number(available_formats, number)?;
|
|
if let Some(formatting_pattern) = formatting_pattern {
|
|
self.format_nsn_using_pattern_with_carrier(
|
|
number,
|
|
formatting_pattern,
|
|
number_format,
|
|
carrier_code,
|
|
)
|
|
} else {
|
|
Ok(Cow::Borrowed(number))
|
|
}
|
|
}
|
|
|
|
fn choose_formatting_pattern_for_number<'b>(
|
|
&self,
|
|
available_formats: &'b [NumberFormat],
|
|
national_number: &str,
|
|
) -> RegexResult<Option<&'b NumberFormat>> {
|
|
for format in available_formats {
|
|
if !format
|
|
.leading_digits_pattern
|
|
// We always use the last leading_digits_pattern, as it is the most
|
|
// detailed.
|
|
.last()
|
|
.map(|last| {
|
|
self.reg_exps
|
|
.regexp_cache
|
|
.get_regex(&last)
|
|
.and_then(|regex| Ok(regex.matches_start(national_number)))
|
|
})
|
|
// default not continue
|
|
.unwrap_or(Ok(true))?
|
|
{
|
|
continue;
|
|
}
|
|
let pattern_to_match = self.reg_exps.regexp_cache.get_regex(format.pattern())?;
|
|
if pattern_to_match.full_match(national_number) {
|
|
return Ok(Some(format));
|
|
}
|
|
}
|
|
return Ok(None);
|
|
}
|
|
|
|
// Note that carrier_code is optional - if an empty string, no carrier code
|
|
// replacement will take place.
|
|
fn format_nsn_using_pattern_with_carrier<'b>(
|
|
&self,
|
|
national_number: &'b str,
|
|
formatting_pattern: &NumberFormat,
|
|
number_format: PhoneNumberFormat,
|
|
carrier_code: &str,
|
|
) -> RegexResult<Cow<'b, str>> {
|
|
let mut number_format_rule = Cow::Borrowed(formatting_pattern.format());
|
|
if matches!(number_format, PhoneNumberFormat::National)
|
|
&& carrier_code.len() > 0
|
|
&& formatting_pattern
|
|
.domestic_carrier_code_formatting_rule().len() > 0 {
|
|
// Replace the $CC in the formatting rule with the desired carrier code.
|
|
let mut carrier_code_formatting_rule =
|
|
Cow::Borrowed(formatting_pattern.domestic_carrier_code_formatting_rule());
|
|
|
|
if let Cow::Owned(s) = self
|
|
.reg_exps
|
|
.carrier_code_pattern
|
|
.replace(&carrier_code_formatting_rule, carrier_code)
|
|
{
|
|
carrier_code_formatting_rule = Cow::Owned(s);
|
|
}
|
|
if let Cow::Owned(s) = self
|
|
.reg_exps
|
|
.first_group_capturing_pattern
|
|
.replace(&number_format_rule, carrier_code_formatting_rule)
|
|
{
|
|
number_format_rule = Cow::Owned(s);
|
|
}
|
|
} else {
|
|
// Use the national prefix formatting rule instead.
|
|
let national_prefix_formatting_rule =
|
|
formatting_pattern.national_prefix_formatting_rule();
|
|
|
|
if matches!(number_format, PhoneNumberFormat::National)
|
|
&& national_prefix_formatting_rule.len() > 0
|
|
{
|
|
// Apply the national_prefix_formatting_rule as the formatting_pattern
|
|
// contains only information on how the national significant number
|
|
// should be formatted at this point.
|
|
if let Cow::Owned(s) = self
|
|
.reg_exps
|
|
.first_group_capturing_pattern
|
|
.replace(&number_format_rule, national_prefix_formatting_rule)
|
|
{
|
|
number_format_rule = Cow::Owned(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
let pattern_to_match = self
|
|
.reg_exps
|
|
.regexp_cache
|
|
.get_regex(formatting_pattern.pattern())?;
|
|
|
|
let mut formatted_number =
|
|
pattern_to_match.replace_all(national_number, number_format_rule);
|
|
|
|
if matches!(number_format, PhoneNumberFormat::RFC3966) {
|
|
// First consume any leading punctuation, if any was present.
|
|
if let Some(matches) = self
|
|
.reg_exps
|
|
.separator_pattern
|
|
.find_start(&formatted_number) {
|
|
let rest = &formatted_number[matches.end()..];
|
|
formatted_number = Cow::Owned(rest.to_string());
|
|
}
|
|
// Then replace all separators with a "-".
|
|
if let Cow::Owned(s) = self
|
|
.reg_exps
|
|
.separator_pattern
|
|
.replace_all(&formatted_number, "-")
|
|
{
|
|
formatted_number = Cow::Owned(s)
|
|
}
|
|
}
|
|
Ok(formatted_number)
|
|
}
|
|
|
|
/// Simple wrapper of FormatNsnUsingPatternWithCarrier for the common case of
|
|
/// no carrier code.
|
|
fn format_nsn_using_pattern<'b>(
|
|
&self,
|
|
national_number: &'b str,
|
|
formatting_pattern: &NumberFormat,
|
|
number_format: PhoneNumberFormat,
|
|
) -> RegexResult<Cow<'b, str>> {
|
|
self.format_nsn_using_pattern_with_carrier(
|
|
national_number,
|
|
formatting_pattern,
|
|
number_format,
|
|
"",
|
|
)
|
|
}
|
|
|
|
// Returns the formatted extension of a phone number, if the phone number had an
|
|
// extension specified else None.
|
|
fn get_formatted_extension(
|
|
phone_number: &PhoneNumber,
|
|
metadata: &PhoneMetadata,
|
|
number_format: PhoneNumberFormat,
|
|
) -> Option<String> {
|
|
if !phone_number.has_extension() || phone_number.extension().is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let prefix = if matches!(number_format, PhoneNumberFormat::RFC3966) {
|
|
RFC3966_EXTN_PREFIX
|
|
} else if metadata.has_preferred_extn_prefix() {
|
|
metadata.preferred_extn_prefix()
|
|
} else {
|
|
DEFAULT_EXTN_PREFIX
|
|
};
|
|
Some(fast_cat::concat_str!(prefix, phone_number.extension()))
|
|
}
|
|
|
|
fn format_by_pattern(
|
|
&self,
|
|
phone_number: &PhoneNumber,
|
|
number_format: PhoneNumberFormat,
|
|
user_defined_formats: &[NumberFormat],
|
|
) -> RegexResult<String> {
|
|
let country_calling_code = phone_number.country_code();
|
|
// Note GetRegionCodeForCountryCode() is used because formatting information
|
|
// contained in the metadata for US.
|
|
let national_significant_number = Self::get_national_significant_number(phone_number);
|
|
let region_code = self.get_region_code_for_country_code(country_calling_code);
|
|
let Some(metadata) =
|
|
self.get_metadata_for_region_or_calling_code(country_calling_code, ®ion_code)
|
|
else {
|
|
return Ok(national_significant_number);
|
|
};
|
|
|
|
let formatting_pattern = self.choose_formatting_pattern_for_number(
|
|
user_defined_formats,
|
|
&national_significant_number,
|
|
)?;
|
|
|
|
let mut formatted_number = if let Some(formatting_pattern) = formatting_pattern {
|
|
// Before we do a replacement of the national prefix pattern $NP with the
|
|
// national prefix, we need to copy the rule so that subsequent replacements
|
|
// for different numbers have the appropriate national prefix.
|
|
let mut num_format_copy = formatting_pattern.clone();
|
|
|
|
let national_prefix_formatting_rule =
|
|
formatting_pattern.national_prefix_formatting_rule();
|
|
if !national_prefix_formatting_rule.is_empty() {
|
|
let national_prefix = metadata.national_prefix();
|
|
if !national_prefix.is_empty() {
|
|
// Replace $NP with national prefix and $FG with the first group ($1).
|
|
let rule = national_prefix_formatting_rule
|
|
.replace("$NP", national_prefix)
|
|
.replace("$FG", "$1");
|
|
num_format_copy.set_national_prefix_formatting_rule(rule);
|
|
} else {
|
|
// We don't want to have a rule for how to format the national prefix if
|
|
// there isn't one.
|
|
num_format_copy.clear_national_prefix_formatting_rule();
|
|
}
|
|
}
|
|
self.format_nsn_using_pattern(
|
|
&national_significant_number,
|
|
&num_format_copy,
|
|
number_format,
|
|
)?
|
|
.to_string()
|
|
} else {
|
|
national_significant_number
|
|
};
|
|
if let Some(extension) =
|
|
Self::get_formatted_extension(phone_number, metadata, PhoneNumberFormat::National)
|
|
{
|
|
formatted_number.push_str(&extension);
|
|
}
|
|
prefix_number_with_country_calling_code(
|
|
country_calling_code,
|
|
number_format,
|
|
&mut formatted_number,
|
|
);
|
|
Ok(formatted_number)
|
|
}
|
|
|
|
fn format_national_number_with_carrier_code(
|
|
&self,
|
|
phone_number: &PhoneNumber,
|
|
carrier_code: &str,
|
|
) -> Result<String> {
|
|
let country_calling_code = phone_number.country_code();
|
|
let national_significant_number = Self::get_national_significant_number(phone_number);
|
|
let region_code = self.get_region_code_for_country_code(country_calling_code);
|
|
|
|
// Note GetRegionCodeForCountryCode() is used because formatting information
|
|
// contained in the metadata for US.
|
|
let Some(metadata) = self.get_metadata_for_region_or_calling_code(country_calling_code, ®ion_code) else {
|
|
return Ok(national_significant_number)
|
|
};
|
|
|
|
let mut formatted_number = owned_from_cow_or!(
|
|
self.format_nsn_with_carrier(
|
|
&national_significant_number, metadata,
|
|
PhoneNumberFormat::National, carrier_code,
|
|
)?,
|
|
national_significant_number
|
|
);
|
|
if let Some(formatted_extension) = Self::get_formatted_extension(
|
|
phone_number, metadata, PhoneNumberFormat::National
|
|
) {
|
|
formatted_number.push_str(&formatted_extension);
|
|
}
|
|
|
|
prefix_number_with_country_calling_code(
|
|
country_calling_code,
|
|
PhoneNumberFormat::National,
|
|
&mut formatted_number,
|
|
);
|
|
|
|
Ok(formatted_number)
|
|
}
|
|
|
|
fn format_national_number_with_preferred_carrier_code(
|
|
&self,
|
|
phone_number: &PhoneNumber,
|
|
fallback_carrier_code: &str,
|
|
) -> Result<String> {
|
|
let carrier_code = if !phone_number.preferred_domestic_carrier_code().is_empty() {
|
|
phone_number.preferred_domestic_carrier_code()
|
|
} else {
|
|
fallback_carrier_code
|
|
};
|
|
self.format_national_number_with_carrier_code(phone_number, carrier_code)
|
|
}
|
|
|
|
|
|
fn has_valid_country_calling_code(&self, country_calling_code: i32) -> bool {
|
|
// Create an IntRegionsPair with the country_code passed in, and use it to
|
|
// locate the pair with the same country_code in the sorted vector.
|
|
|
|
return self
|
|
.country_calling_code_to_region_code_map
|
|
.binary_search_by_key(&country_calling_code, |(k, _)| *k)
|
|
.is_ok();
|
|
}
|
|
|
|
fn format_number_for_mobile_dialing<'b>(
|
|
&self,
|
|
phone_number: &'b PhoneNumber,
|
|
calling_from: &str,
|
|
with_formatting: bool,
|
|
) -> Result<Cow<'b, str>> {
|
|
let country_calling_code = phone_number.country_code();
|
|
if !self.has_valid_country_calling_code(country_calling_code) {
|
|
return if phone_number.has_raw_input() {
|
|
Ok(Cow::Borrowed(phone_number.raw_input()))
|
|
} else {
|
|
Ok(Cow::Borrowed(""))
|
|
}
|
|
}
|
|
|
|
let mut formatted_number = String::new();
|
|
// Clear the extension, as that part cannot normally be dialed together with
|
|
// the main number.
|
|
let mut number_no_extension = phone_number.clone();
|
|
number_no_extension.clear_extension();
|
|
let region_code = self.get_region_code_for_country_code(country_calling_code);
|
|
let number_type = self.get_number_type(&number_no_extension)?;
|
|
let is_valid_number = !matches!(number_type, PhoneNumberType::Unknown);
|
|
if calling_from == region_code {
|
|
let is_fixed_line_or_mobile = matches!(number_type,
|
|
PhoneNumberType::FixedLine | PhoneNumberType::FixedLineOrMobile | PhoneNumberType::Mobile
|
|
);
|
|
// Carrier codes may be needed in some countries. We handle this here.
|
|
if (region_code == "BR") && (is_fixed_line_or_mobile) {
|
|
// Historically, we set this to an empty string when parsing with raw
|
|
// input if none was found in the input string. However, this doesn't
|
|
// result in a number we can dial. For this reason, we treat the empty
|
|
// string the same as if it isn't set at all.
|
|
if !number_no_extension.preferred_domestic_carrier_code().is_empty() {
|
|
formatted_number = self.format_national_number_with_preferred_carrier_code(
|
|
&number_no_extension, ""
|
|
)?;
|
|
} else {
|
|
// Brazilian fixed line and mobile numbers need to be dialed with a
|
|
// carrier code when called within Brazil. Without that, most of the
|
|
// carriers won't connect the call. Because of that, we return an empty
|
|
// string here.
|
|
// IDK BUT KEPPET
|
|
formatted_number.clear();
|
|
}
|
|
} else if country_calling_code == NANPA_COUNTRY_CODE {
|
|
// For NANPA countries, we output international format for numbers that
|
|
// can be dialed internationally, since that always works, except for
|
|
// numbers which might potentially be short numbers, which are always
|
|
// dialled in national format.
|
|
let region_metadata = self.region_to_metadata_map
|
|
.get(calling_from)
|
|
.unwrap() /* we've checked if number is valid at top of function */;
|
|
let national_number = Self::get_national_significant_number(&number_no_extension);
|
|
let format = if self.can_be_internationally_dialled(&number_no_extension)?
|
|
&& test_number_length_with_unknown_type(
|
|
&national_number,
|
|
region_metadata
|
|
).is_err_and(| e | matches!(e, ValidationResultErr::TooShort)) {
|
|
PhoneNumberFormat::International
|
|
}
|
|
else {
|
|
PhoneNumberFormat::National
|
|
};
|
|
if let Cow::Owned(s) = self.format(
|
|
&number_no_extension, format
|
|
)? {
|
|
formatted_number = s;
|
|
}
|
|
}
|
|
else {
|
|
// For non-geographical countries, and Mexican, Chilean and Uzbek fixed
|
|
// line and mobile numbers, we output international format for numbers
|
|
// that can be dialed internationally as that always works.
|
|
let format = if (region_code == REGION_CODE_FOR_NON_GEO_ENTITY ||
|
|
// MX fixed line and mobile numbers should always be formatted in
|
|
// international format, even when dialed within MX. For national
|
|
// format to work, a carrier code needs to be used, and the correct
|
|
// carrier code depends on if the caller and callee are from the same
|
|
// local area. It is trickier to get that to work correctly than
|
|
// using international format, which is tested to work fine on all
|
|
// carriers.
|
|
// CL fixed line numbers need the national prefix when dialing in the
|
|
// national format, but don't have it when used for display. The
|
|
// reverse is true for mobile numbers. As a result, we output them in
|
|
// the international format to make it work.
|
|
// UZ mobile and fixed-line numbers have to be formatted in
|
|
// international format or prefixed with special codes like 03, 04
|
|
// (for fixed-line) and 05 (for mobile) for dialling successfully
|
|
// from mobile devices. As we do not have complete information on
|
|
// special codes and to be consistent with formatting across all
|
|
// phone types we return the number in international format here.
|
|
((region_code == "MX" ||
|
|
region_code == "CL" ||
|
|
region_code == "UZ") &&
|
|
is_fixed_line_or_mobile)) &&
|
|
self.can_be_internationally_dialled(&number_no_extension)? {
|
|
PhoneNumberFormat::International
|
|
}
|
|
else {
|
|
PhoneNumberFormat::National
|
|
};
|
|
if let Cow::Owned(s) = self.format(
|
|
&number_no_extension, format
|
|
)? {
|
|
formatted_number = s;
|
|
}
|
|
}
|
|
}
|
|
else if is_valid_number && self.can_be_internationally_dialled(&number_no_extension)? {
|
|
// We assume that short numbers are not diallable from outside their
|
|
// region, so if a number is not a valid regular length phone number, we
|
|
// treat it as if it cannot be internationally dialled.
|
|
let format = if with_formatting {
|
|
PhoneNumberFormat::International
|
|
} else {
|
|
PhoneNumberFormat::E164
|
|
};
|
|
return Ok(Cow::Owned(
|
|
owned_from_cow_or!(self.format(
|
|
&number_no_extension, format
|
|
)?, formatted_number))
|
|
)
|
|
}
|
|
if !with_formatting {
|
|
Ok(Cow::Owned(self.normalize_diallable_chars_only(&formatted_number)))
|
|
} else {
|
|
Ok(Cow::Owned(formatted_number))
|
|
}
|
|
}
|
|
|
|
fn get_number_type(&self, phone_number: &PhoneNumber) -> Result<PhoneNumberType> {
|
|
let region_code = self.get_region_code_for_number(phone_number)?;
|
|
let Some(metadata) = self
|
|
.get_metadata_for_region_or_calling_code(phone_number.country_code(), region_code)
|
|
else {
|
|
return Ok(PhoneNumberType::Unknown)
|
|
};
|
|
let national_significant_number = Self::get_national_significant_number(phone_number);
|
|
Ok(self.get_number_type_helper(&national_significant_number, metadata))
|
|
}
|
|
|
|
fn get_region_code_for_number(&self, phone_number: &PhoneNumber) -> RegexResult<&str>{
|
|
let country_calling_code = phone_number.country_code();
|
|
let region_codes = self.get_region_codes_for_country_calling_code(country_calling_code);
|
|
if region_codes.len() == 0 {
|
|
log::trace!("Missing/invalid country calling code ({})", country_calling_code);
|
|
return Ok(i18n::RegionCode::get_unknown())
|
|
}
|
|
if region_codes.len() == 1 {
|
|
return Ok(region_codes[0])
|
|
} else {
|
|
self.get_region_code_for_number_from_region_list(phone_number, ®ion_codes)
|
|
}
|
|
}
|
|
|
|
fn get_region_code_for_number_from_region_list<'b>(
|
|
&self,
|
|
phone_number: &PhoneNumber,
|
|
region_codes: &[&'b str],
|
|
) -> RegexResult<&'b str> {
|
|
let national_number = Self::get_national_significant_number(phone_number);
|
|
for code in region_codes {
|
|
// Metadata cannot be NULL because the region codes come from the country
|
|
// calling code map.
|
|
let metadata = &self.region_to_metadata_map[*code];
|
|
if metadata.has_leading_digits() {
|
|
if self.reg_exps.regexp_cache
|
|
.get_regex(metadata.leading_digits())?
|
|
.matches_start(&national_number) {
|
|
|
|
return Ok(code)
|
|
}
|
|
} else if self.get_number_type_helper(&national_number, metadata) != PhoneNumberType::Unknown {
|
|
return Ok(code);
|
|
}
|
|
}
|
|
return Ok(i18n::RegionCode::get_unknown())
|
|
}
|
|
|
|
fn get_number_type_helper(
|
|
&self,
|
|
national_number: &str,
|
|
metadata: &PhoneMetadata
|
|
) -> PhoneNumberType {
|
|
if !self.is_number_matching_desc(national_number, &metadata.general_desc) {
|
|
trace!("Number '{national_number}' type unknown - doesn't match general national number pattern");
|
|
return PhoneNumberType::Unknown
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.premium_rate) {
|
|
trace!("Number '{national_number}' is a premium number.");
|
|
return PhoneNumberType::PremiumRate;
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.toll_free) {
|
|
trace!("Number '{national_number}' is a toll-free number.");
|
|
return PhoneNumberType::TollFree;
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.shared_cost) {
|
|
trace!("Number '{national_number}' is a shared cost number.");
|
|
return PhoneNumberType::SharedCost;
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.voip) {
|
|
trace!("Number '{national_number}' is a VOIP (Voice over IP) number.");
|
|
return PhoneNumberType::VoIP;
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.personal_number) {
|
|
trace!("Number '{national_number}' is a personal number.");
|
|
return PhoneNumberType::PersonalNumber;
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.pager) {
|
|
trace!("Number '{national_number}' is a pager number.");
|
|
return PhoneNumberType::Pager;
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.uan) {
|
|
trace!("Number '{national_number}' is a UAN.");
|
|
return PhoneNumberType::UAN;
|
|
}
|
|
if self.is_number_matching_desc(national_number, &metadata.voicemail) {
|
|
trace!("Number '{national_number}' is a voicemail number.");
|
|
return PhoneNumberType::VoiceMail;
|
|
}
|
|
|
|
let is_fixed_line = self.is_number_matching_desc(national_number, &metadata.fixed_line);
|
|
if is_fixed_line {
|
|
if metadata.same_mobile_and_fixed_line_pattern() {
|
|
trace!("Number '{national_number}': fixed-line and mobile patterns equal,\
|
|
number is fixed-line or mobile");
|
|
return PhoneNumberType::FixedLineOrMobile;
|
|
}
|
|
else if self.is_number_matching_desc(national_number, &metadata.mobile) {
|
|
trace!("Number '{national_number}': Fixed-line and mobile patterns differ, but number is \
|
|
still fixed-line or mobile"
|
|
);
|
|
return PhoneNumberType::FixedLineOrMobile;
|
|
}
|
|
trace!("Number '{national_number}' is a fixed line number.");
|
|
return PhoneNumberType::FixedLine;
|
|
}
|
|
// Otherwise, test to see if the number is mobile. Only do this if certain
|
|
// that the patterns for mobile and fixed line aren't the same.
|
|
if !metadata.same_mobile_and_fixed_line_pattern()
|
|
&& self.is_number_matching_desc(national_number, &metadata.mobile) {
|
|
trace!("Number '{national_number}' is a mobile number.");
|
|
return PhoneNumberType::Mobile;
|
|
}
|
|
trace!("Number'{national_number}' type unknown - doesn\'t match any specific number type pattern.");
|
|
return PhoneNumberType::Unknown;
|
|
}
|
|
|
|
fn is_number_matching_desc(
|
|
&self,
|
|
national_number: &str,
|
|
number_desc: &PhoneNumberDesc
|
|
) -> bool {
|
|
// Check if any possible number lengths are present; if so, we use them to
|
|
// avoid checking the validation pattern if they don't match. If they are
|
|
// absent, this means they match the general description, which we have
|
|
// already checked before checking a specific number type.
|
|
let actual_length = national_number.len() as i32;
|
|
if number_desc.possible_length.len() > 0 && !number_desc.possible_length.contains(&actual_length) {
|
|
return false;
|
|
}
|
|
// very common name, so specify mod
|
|
helper_functions::is_match(&self.matcher_api, national_number, number_desc)
|
|
}
|
|
|
|
fn can_be_internationally_dialled(
|
|
&self,
|
|
phone_number: &PhoneNumber
|
|
) -> Result<bool> {
|
|
let region_code = self.get_region_code_for_number(phone_number)?;
|
|
let Some(metadata) = self.region_to_metadata_map.get(region_code) else {
|
|
// Note numbers belonging to non-geographical entities (e.g. +800 numbers)
|
|
// are always internationally diallable, and will be caught here.
|
|
return Ok(true)
|
|
};
|
|
let national_significant_number = Self::get_national_significant_number(phone_number);
|
|
return Ok(!self.is_number_matching_desc(
|
|
&national_significant_number, &metadata.no_international_dialling
|
|
));
|
|
}
|
|
|
|
fn normalize_diallable_chars_only(&self, phone_number: &str) -> String {
|
|
normalize_helper(
|
|
&self.reg_exps.diallable_char_mappings,
|
|
true, phone_number
|
|
)
|
|
}
|
|
|
|
fn normalize_digits_only<'a>(&self, phone_number: &'a str) -> String {
|
|
phone_number.chars()
|
|
.filter_map(| c | c.to_decimal_utf8())
|
|
.filter_map(| i | char::from_u32(b'0' as u32 + i) )
|
|
.collect()
|
|
}
|
|
|
|
fn format_out_of_country_calling_number<'a>(
|
|
&self,
|
|
phone_number: &'a PhoneNumber,
|
|
calling_from: &str,
|
|
) -> RegexResult<Cow<'a, str>> {
|
|
let Some(metadata_calling_from) = self.region_to_metadata_map.get(calling_from) else {
|
|
trace!("Trying to format number from invalid region {calling_from}\
|
|
. International formatting applied.");
|
|
return self.format(phone_number, PhoneNumberFormat::International)
|
|
};
|
|
let country_code = phone_number.country_code();
|
|
let national_significant_number = Self::get_national_significant_number(
|
|
phone_number,
|
|
);
|
|
if !self.has_valid_country_calling_code(country_code) {
|
|
return Ok(Cow::Owned(national_significant_number))
|
|
}
|
|
if country_code == NANPA_COUNTRY_CODE {
|
|
if self.nanpa_regions.contains(calling_from) {
|
|
let mut buf = itoa::Buffer::new();
|
|
// prefix it with the country calling code.
|
|
return Ok(Cow::Owned(fast_cat::concat_str!(
|
|
buf.format(country_code) , " ",
|
|
&self.format(phone_number, PhoneNumberFormat::National)?,
|
|
)))
|
|
}
|
|
}
|
|
else if country_code == metadata_calling_from.country_code() {
|
|
// If neither region is a NANPA region, then we check to see if the
|
|
// country calling code of the number and the country calling code of the
|
|
// region we are calling from are the same.
|
|
// need not be dialled. This also applies when dialling within a region, so
|
|
// this if clause covers both these cases.
|
|
// Technically this is the case for dialling from la Réunion to other
|
|
// overseas departments of France (French Guiana, Martinique, Guadeloupe),
|
|
// but not vice versa - so we don't cover this edge case for now and for
|
|
// those cases return the version including country calling code.
|
|
// Details here:
|
|
// http://www.petitfute.com/voyage/225-info-pratiques-reunion
|
|
return self.format(phone_number, PhoneNumberFormat::National)
|
|
}
|
|
// Metadata cannot be NULL because we checked 'IsValidRegionCode()' above.
|
|
let international_prefix = metadata_calling_from.international_prefix();
|
|
|
|
// In general, if there is a preferred international prefix, use that.
|
|
// international format of the number is returned since we would not know
|
|
// which one to use.
|
|
let international_prefix_for_formatting = if metadata_calling_from
|
|
.has_preferred_international_prefix() {
|
|
metadata_calling_from.preferred_international_prefix()
|
|
} else if self.reg_exps.single_international_prefix
|
|
.full_match(international_prefix) {
|
|
international_prefix
|
|
} else {
|
|
""
|
|
};
|
|
|
|
let region_code = self.get_region_code_for_country_code(country_code);
|
|
// Metadata cannot be NULL because the country_code is valid.
|
|
let metadata_for_region = self
|
|
.get_metadata_for_region_or_calling_code(country_code, region_code)
|
|
.expect("Metadata cannot be NULL because the country_code is valid");
|
|
|
|
let formatted_nsn = self
|
|
.format_nsn(
|
|
&national_significant_number,
|
|
metadata_for_region, PhoneNumberFormat::International
|
|
)?;
|
|
|
|
let mut formatted_number = owned_from_cow_or!(formatted_nsn, national_significant_number);
|
|
|
|
if let Some(extension) = Self::
|
|
get_formatted_extension(phone_number, metadata_for_region, PhoneNumberFormat::International) {
|
|
formatted_number.push_str(&extension);
|
|
}
|
|
|
|
return Ok(Cow::Owned(
|
|
if !international_prefix_for_formatting.is_empty() {
|
|
let mut buf = itoa::Buffer::new();
|
|
fast_cat::concat_str!(
|
|
international_prefix_for_formatting, " ", buf.format(country_code) , " ",
|
|
&formatted_number,
|
|
)
|
|
}
|
|
else {
|
|
prefix_number_with_country_calling_code(
|
|
country_code, PhoneNumberFormat::International, &mut formatted_number
|
|
);
|
|
formatted_number
|
|
}
|
|
))
|
|
}
|
|
|
|
fn has_formatting_pattern_for_number(
|
|
&self, phone_number: &PhoneNumber
|
|
) -> RegexResult<bool> {
|
|
let country_calling_code = phone_number.country_code();
|
|
let region_code = self.get_region_code_for_country_code(country_calling_code);
|
|
let Some(metadata) = self.get_metadata_for_region_or_calling_code(
|
|
country_calling_code, region_code
|
|
) else {
|
|
return Ok(false)
|
|
};
|
|
let national_number = Self::get_national_significant_number(phone_number);
|
|
let format_rule = self.choose_formatting_pattern_for_number(
|
|
&metadata.number_format,
|
|
&national_number
|
|
);
|
|
return format_rule.map(| rule | rule.is_some());
|
|
}
|
|
|
|
fn format_in_original_format<'a>(
|
|
&self,
|
|
phone_number: &'a PhoneNumber,
|
|
region_calling_from: &str,
|
|
) -> RegexResult<Cow<'a, str>> {
|
|
if phone_number.has_raw_input() && !self.has_formatting_pattern_for_number(phone_number)? {
|
|
// We check if we have the formatting pattern because without that, we might
|
|
// format the number as a group without national prefix.
|
|
return Ok(Cow::Borrowed(phone_number.raw_input()))
|
|
}
|
|
if !phone_number.has_country_code_source() {
|
|
return self.format(phone_number, PhoneNumberFormat::National)
|
|
}
|
|
let formatted_number = match phone_number.country_code_source() {
|
|
CountryCodeSource::FROM_NUMBER_WITH_PLUS_SIGN =>
|
|
self.format(phone_number, PhoneNumberFormat::International)?,
|
|
CountryCodeSource::FROM_NUMBER_WITH_IDD =>
|
|
self.format_out_of_country_calling_number(phone_number, region_calling_from)?,
|
|
CountryCodeSource::FROM_NUMBER_WITHOUT_PLUS_SIGN => Cow::Owned(
|
|
self.format(phone_number, PhoneNumberFormat::International)?[1..].to_string()
|
|
),
|
|
CountryCodeSource::FROM_DEFAULT_COUNTRY | CountryCodeSource::UNSPECIFIED => 'default_block: {
|
|
let format_national = || {
|
|
self.format(phone_number, PhoneNumberFormat::National)
|
|
};
|
|
|
|
let region_code = self.get_region_code_for_country_code(
|
|
phone_number.country_code()
|
|
);
|
|
// We strip non-digits from the NDD here, and from the raw input later, so
|
|
// that we can compare them easily.
|
|
let national_prefix = self.get_ndd_prefix_for_region(
|
|
region_code, true /* strip non-digits */,
|
|
);
|
|
if national_prefix.is_empty() {
|
|
break 'default_block format_national()?
|
|
}
|
|
let Some(metadata) = self.region_to_metadata_map.get(region_code) else {
|
|
// If the region doesn't have a national prefix at all, we can safely
|
|
// return the national format without worrying about a national prefix
|
|
// being added.
|
|
break 'default_block format_national()?
|
|
};
|
|
// Otherwise, we check if the original number was entered with a national
|
|
// prefix.
|
|
if self.raw_input_contains_national_prefix(
|
|
phone_number.raw_input(), &national_prefix, region_code
|
|
)? {
|
|
// If so, we can safely return the national format.
|
|
break 'default_block format_national()?
|
|
}
|
|
// Metadata cannot be NULL here because GetNddPrefixForRegion() (above)
|
|
// leaves the prefix empty if there is no metadata for the region.
|
|
let national_number = Self::get_national_significant_number(phone_number);
|
|
// This shouldn't be NULL, because we have checked that above with
|
|
// HasFormattingPatternForNumber.
|
|
let format_rule = self.choose_formatting_pattern_for_number(
|
|
&metadata.number_format, &national_number
|
|
)?;
|
|
// The format rule could still be NULL here if the national number was 0
|
|
// and there was no raw input (this should not be possible for numbers
|
|
// generated by the phonenumber library as they would also not have a
|
|
// country calling code and we would have exited earlier).
|
|
let Some(format_rule) = format_rule else {
|
|
break 'default_block format_national()?
|
|
};
|
|
// When the format we apply to this number doesn't contain national
|
|
// prefix, we can just return the national format.
|
|
// TODO: Refactor the code below with the code in
|
|
// IsNationalPrefixPresentIfRequired.
|
|
let candidate_national_prefix_rule = format_rule.national_prefix_formatting_rule();
|
|
// We assume that the first-group symbol will never be _before_ the
|
|
// national prefix.
|
|
let candidate_national_prefix_rule_empty = if !candidate_national_prefix_rule.is_empty() {
|
|
let Some(index_of_first_group) = candidate_national_prefix_rule.find("$1") else {
|
|
error!("First group missing in national prefix rule: {}", candidate_national_prefix_rule);
|
|
break 'default_block format_national()?
|
|
};
|
|
let candidate_national_prefix_rule = &candidate_national_prefix_rule[..index_of_first_group];
|
|
self.normalize_digits_only(&candidate_national_prefix_rule).is_empty()
|
|
} else {
|
|
true
|
|
};
|
|
if candidate_national_prefix_rule_empty {
|
|
// National prefix not used when formatting this number.
|
|
break 'default_block format_national()?
|
|
};
|
|
// Otherwise, we need to remove the national prefix from our output.
|
|
let mut number_format = format_rule.clone();
|
|
number_format.clear_national_prefix_formatting_rule();
|
|
Cow::Owned(
|
|
self.format_by_pattern(
|
|
phone_number, PhoneNumberFormat::National, &[number_format]
|
|
)?
|
|
)
|
|
}
|
|
};
|
|
// If no digit is inserted/removed/modified as a result of our formatting, we
|
|
// return the formatted phone number; otherwise we return the raw input the
|
|
// user entered.
|
|
if !formatted_number.is_empty() && !phone_number.raw_input().is_empty() {
|
|
let normalized_formatted_number = self.normalize_diallable_chars_only(&formatted_number);
|
|
let normalized_raw_input = self.normalize_diallable_chars_only(phone_number.raw_input());
|
|
if normalized_formatted_number != normalized_raw_input {
|
|
return Ok(Cow::Borrowed(phone_number.raw_input()))
|
|
}
|
|
}
|
|
Ok(formatted_number)
|
|
}
|
|
|
|
/// Check if raw_input, which is assumed to be in the national format, has a
|
|
/// national prefix. The national prefix is assumed to be in digits-only form.
|
|
fn raw_input_contains_national_prefix(
|
|
&self,
|
|
raw_input: &str,
|
|
national_prefix: &str,
|
|
region_code: &str
|
|
) -> RegexResult<bool> {
|
|
let normalized_national_number = self.normalize_digits_only(raw_input);
|
|
if normalized_national_number.starts_with(national_prefix) {
|
|
// Some Japanese numbers (e.g. 00777123) might be mistaken to contain
|
|
// the national prefix when written without it (e.g. 0777123) if we just
|
|
// do prefix matching. To tackle that, we check the validity of the
|
|
// number if the assumed national prefix is removed (777123 won't be
|
|
// valid in Japan).
|
|
if let Ok(number_without_national_prefix) = self.parse(
|
|
&normalized_national_number[national_prefix.len()..],
|
|
region_code
|
|
) {
|
|
return self.is_valid_number(&number_without_national_prefix)
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
|
|
fn parse(&self, number_to_parse: &str, default_region: &str) -> ParseResult<PhoneNumber> {
|
|
self.parse_helper(number_to_parse, default_region, false, true)
|
|
}
|
|
|
|
fn parse_and_keep_raw_input(&self, number_to_parse: &str, default_region: &str) -> ParseResult<PhoneNumber> {
|
|
self.parse_helper(number_to_parse, default_region, true, true)
|
|
}
|
|
|
|
fn is_valid_number(&self, phone_number: &PhoneNumber) -> RegexResult<bool> {
|
|
let region_code = self.get_region_code_for_number(phone_number)?;
|
|
return Ok(self.is_valid_number_for_region(phone_number, region_code));
|
|
}
|
|
|
|
fn is_valid_number_for_region(&self, phone_number: &PhoneNumber, region_code: &str) -> bool {
|
|
let country_code = phone_number.country_code();
|
|
let metadata =
|
|
self.get_metadata_for_region_or_calling_code(country_code, region_code);
|
|
if let Some(metadata) = metadata.filter(| metadata |
|
|
!(REGION_CODE_FOR_NON_GEO_ENTITY != region_code && country_code != metadata.country_code())
|
|
) {
|
|
let national_number = Self::get_national_significant_number(phone_number);
|
|
!matches!(self.get_number_type_helper(&national_number, metadata), PhoneNumberType::Unknown)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn format_out_of_country_keeping_alpha_chars<'a>(
|
|
&self,
|
|
phone_number: &'a PhoneNumber,
|
|
calling_from: &str,
|
|
) -> RegexResult<Cow<'a, str>> {
|
|
// If there is no raw input, then we can't keep alpha characters because there
|
|
// aren't any. In this case, we return FormatOutOfCountryCallingNumber.
|
|
if phone_number.raw_input().is_empty() {
|
|
return self.format_out_of_country_calling_number(phone_number, calling_from)
|
|
}
|
|
|
|
let country_code = phone_number.country_code();
|
|
if !self.has_valid_country_calling_code(country_code) {
|
|
return Ok(phone_number.raw_input().into())
|
|
}
|
|
// Strip any prefix such as country calling code, IDD, that was present. We do
|
|
// this by comparing the number in raw_input with the parsed number.
|
|
// Normalize punctuation. We retain number grouping symbols such as " " only.
|
|
let mut normalized_raw_input = helper_functions::normalize_helper(
|
|
&self.reg_exps.all_plus_number_grouping_symbols,
|
|
true,
|
|
phone_number.raw_input()
|
|
);
|
|
// Now we trim everything before the first three digits in the parsed number.
|
|
// We choose three because all valid alpha numbers have 3 digits at the start
|
|
// - if it does not, then we don't trim anything at all. Similarly, if the
|
|
// national number was less than three digits, we don't trim anything at all.
|
|
let national_number = Self::get_national_significant_number(phone_number);
|
|
if national_number.len() > 3 {
|
|
let first_national_number_digit = normalized_raw_input
|
|
.find(&national_number[0..3]);
|
|
if let Some(first_national_number_digit) = first_national_number_digit {
|
|
normalized_raw_input.drain(0..first_national_number_digit);
|
|
}
|
|
}
|
|
let metadata = self.region_to_metadata_map.get(calling_from);
|
|
if country_code == NANPA_COUNTRY_CODE {
|
|
if self.nanpa_regions.contains(calling_from) {
|
|
let mut buf = itoa::Buffer::new();
|
|
|
|
return Ok(fast_cat::concat_str!(
|
|
buf.format(country_code), " ", &normalized_raw_input
|
|
).into());
|
|
}
|
|
}
|
|
else if let Some(metadata) = metadata.filter(
|
|
|metadata| country_code == metadata.country_code()
|
|
) {
|
|
let Some(formatting_pattern) = self.choose_formatting_pattern_for_number(
|
|
&metadata.number_format, &national_number
|
|
)? else {
|
|
// If no pattern above is matched, we format the original input.
|
|
return Ok(normalized_raw_input.into())
|
|
};
|
|
let mut new_format = formatting_pattern.clone();
|
|
// The first group is the first group of digits that the user wrote
|
|
// together.
|
|
new_format.set_pattern("(\\d+)(.*)".to_owned());
|
|
// Here we just concatenate them back together after the national prefix
|
|
// has been fixed.
|
|
new_format.set_format("$1$2".to_owned());
|
|
// Now we format using this pattern instead of the default pattern, but
|
|
// with the national prefix prefixed if necessary.
|
|
// This will not work in the cases where the pattern (and not the
|
|
// leading digits) decide whether a national prefix needs to be used, since
|
|
// we have overridden the pattern to match anything, but that is not the
|
|
// case in the metadata to date.
|
|
return self.format_nsn_using_pattern(
|
|
&normalized_raw_input,
|
|
&new_format,
|
|
PhoneNumberFormat::National
|
|
).map(| cow | Cow::Owned(cow.into_owned()) );
|
|
}
|
|
|
|
// If an unsupported region-calling-from is entered, or a country with
|
|
// multiple international prefixes, the international format of the number is
|
|
// returned, unless there is a preferred international prefix.
|
|
let international_prefix_for_formatting = metadata.map(| metadata | {
|
|
let international_prefix = metadata.international_prefix();
|
|
if self.reg_exps.single_international_prefix
|
|
.full_match(international_prefix) {
|
|
international_prefix
|
|
} else {
|
|
metadata.preferred_international_prefix()
|
|
}
|
|
});
|
|
let formatted_number = if let Some(international_prefix_for_formatting) = international_prefix_for_formatting {
|
|
let mut buf = itoa::Buffer::new();
|
|
fast_cat::concat_str!(
|
|
international_prefix_for_formatting, " ",
|
|
buf.format(country_code), " ", &normalized_raw_input
|
|
)
|
|
} else {
|
|
// Invalid region entered as country-calling-from (so no metadata was found
|
|
// for it) or the region chosen has multiple international dialling
|
|
// prefixes.
|
|
if !self.region_to_metadata_map.contains_key(calling_from) {
|
|
trace!(
|
|
"Trying to format number from invalid region {}. International formatting applied.",
|
|
calling_from
|
|
);
|
|
}
|
|
let mut formatted_number = normalized_raw_input;
|
|
prefix_number_with_country_calling_code(country_code, PhoneNumberFormat::International, &mut formatted_number);
|
|
formatted_number
|
|
};
|
|
let region_code = self.get_region_code_for_country_code(country_code);
|
|
// Metadata cannot be null because the country code is valid.
|
|
let metadata_for_region = self
|
|
.get_metadata_for_region_or_calling_code(country_code, region_code)
|
|
.expect("Metadata cannot be null because the country code is valid.");
|
|
|
|
// Strip any extension
|
|
let (phone_number_without_extension, _) = self
|
|
.maybe_strip_extension(&formatted_number);
|
|
// Append the formatted extension
|
|
let extension = Self::get_formatted_extension(phone_number, metadata_for_region, PhoneNumberFormat::International);
|
|
Ok(
|
|
if let Some(extension) = extension {
|
|
fast_cat::concat_str!(phone_number_without_extension, &extension)
|
|
} else {
|
|
phone_number_without_extension.to_string()
|
|
}
|
|
.into()
|
|
)
|
|
}
|
|
|
|
|
|
/// Returns whether the value of phoneContext follows the syntax defined in
|
|
/// RFC3966.
|
|
fn is_phone_context_valid(
|
|
&self,
|
|
phone_context: &str
|
|
) -> bool {
|
|
if phone_context.is_empty() {
|
|
return false
|
|
}
|
|
|
|
// Does phone-context value match pattern of global-number-digits or
|
|
// domainname
|
|
return self.reg_exps.rfc3966_global_number_digits_pattern.full_match(phone_context)
|
|
||
|
|
self.reg_exps.rfc3966_domainname_pattern.full_match(phone_context);
|
|
}
|
|
|
|
/// Converts number_to_parse to a form that we can parse and write it to
|
|
/// national_number if it is written in RFC3966; otherwise extract a possible
|
|
/// number out of it and write to national_number.
|
|
fn build_national_number_for_parsing(
|
|
&self, number_to_parse: &str
|
|
) -> ParseResult<String> {
|
|
let index_of_phone_context = number_to_parse.find(RFC3966_PHONE_CONTEXT);
|
|
|
|
let mut national_number = String::with_capacity(
|
|
number_to_parse.len() +
|
|
RFC3966_PREFIX.len()
|
|
);
|
|
|
|
// IMPORTANT RUST NOTE: in original c++ code function IsPhoneContextValid
|
|
// always returns `true` if index of phone context is NULL (=> phone context is NULL)
|
|
// if anything changes that logic MUST change.
|
|
if let Some(index_of_phone_context) = index_of_phone_context {
|
|
let phone_context =
|
|
Self::extract_phone_context(number_to_parse, index_of_phone_context);
|
|
if !self.is_phone_context_valid(phone_context) {
|
|
trace!("The phone-context value for phone number {number_to_parse} is invalid.");
|
|
return Err(ParseError::NotANumber)
|
|
}
|
|
// If the phone context contains a phone number prefix, we need to capture
|
|
// it, whereas domains will be ignored.
|
|
if phone_context.starts_with(PLUS_SIGN) {
|
|
// Additional parameters might follow the phone context. If so, we will
|
|
// remove them here because the parameters after phone context are not
|
|
// important for parsing the phone number.
|
|
national_number.push_str(phone_context)
|
|
};
|
|
|
|
// Now append everything between the "tel:" prefix and the phone-context.
|
|
// This should include the national number, an optional extension or
|
|
// isdn-subaddress component. Note we also handle the case when "tel:" is
|
|
// missing, as we have seen in some of the phone number inputs. In that
|
|
// case, we append everything from the beginning.
|
|
let index_of_rfc_prefix = number_to_parse.find(RFC3966_PREFIX);
|
|
let index_of_national_number = index_of_rfc_prefix.map_or(0, | index_of_rfc_prefix |
|
|
index_of_rfc_prefix + RFC3966_PREFIX.len()
|
|
);
|
|
national_number.push_str(
|
|
&number_to_parse[index_of_national_number..index_of_phone_context]
|
|
);
|
|
} else {
|
|
// Extract a possible number from the string passed in (this strips leading
|
|
// characters that could not be the start of a phone number.)
|
|
national_number.push_str(self.extract_possible_number(number_to_parse)?);
|
|
}
|
|
|
|
// Delete the isdn-subaddress and everything after it if it is present. Note
|
|
// extension won't appear at the same time with isdn-subaddress according to
|
|
// paragraph 5.3 of the RFC3966 spec.
|
|
let index_of_isdn = national_number.find(RFC3966_ISDN_SUBADDRESS);
|
|
if let Some(index_of_isdn) = index_of_isdn {
|
|
national_number.truncate(index_of_isdn);
|
|
}
|
|
// If both phone context and isdn-subaddress are absent but other parameters
|
|
// are present, the parameters are left in nationalNumber. This is because
|
|
// we are concerned about deleting content from a potential number string
|
|
// when there is no strong evidence that the number is actually written in
|
|
// RFC3966.
|
|
return Ok(national_number);
|
|
}
|
|
|
|
/// Extracts the value of the phone-context parameter of number_to_extract_from
|
|
/// where the index of ";phone-context=" is parameter index_of_phone_context,
|
|
/// following the syntax defined in RFC3966.
|
|
///
|
|
/// Returns the extracted `Some(possibly empty)`, or a `None` if no
|
|
/// phone-context parameter is found.
|
|
fn extract_phone_context<'a>(
|
|
number_to_extract_from: &'a str,
|
|
index_of_phone_context: usize
|
|
) -> &'a str {
|
|
let phone_context_start = index_of_phone_context
|
|
+ RFC3966_PHONE_CONTEXT.len();
|
|
// If phone-context parameter is empty
|
|
if phone_context_start >= number_to_extract_from.len() {
|
|
return ""
|
|
}
|
|
|
|
let phone_context_end = number_to_extract_from[phone_context_start..]
|
|
.find(';');
|
|
// If phone-context is not the last parameter
|
|
|
|
if let Some(phone_context_end) = phone_context_end {
|
|
&number_to_extract_from[phone_context_start..phone_context_end+phone_context_start]
|
|
} else {
|
|
&number_to_extract_from[phone_context_start..]
|
|
}
|
|
}
|
|
|
|
/// Attempts to extract a possible number from the string passed in. This
|
|
/// currently strips all leading characters that could not be used to start a
|
|
/// phone number. Characters that can be used to start a phone number are
|
|
/// defined in the valid_start_char_pattern. If none of these characters are
|
|
/// found in the number passed in, an empty string is returned. This function
|
|
/// also attempts to strip off any alternative extensions or endings if two or
|
|
/// more are present, such as in the case of: (530) 583-6985 x302/x2303. The
|
|
/// second extension here makes this actually two phone numbers, (530) 583-6985
|
|
/// x302 and (530) 583-6985 x2303. We remove the second extension so that the
|
|
/// first number is parsed correctly.
|
|
fn extract_possible_number<'a>(&self, phone_number: &'a str) -> std::result::Result<&'a str, ExtractNumberError> {
|
|
// Rust note: skip UTF-8 validation since in rust strings are already UTF-8 valid
|
|
let mut i: usize = 0;
|
|
for c in phone_number.chars() {
|
|
i += c.len_utf8();
|
|
if self.reg_exps.valid_start_char_pattern.full_match(&phone_number[i-c.len_utf8()..i]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if i == phone_number.len() {
|
|
// No valid start character was found. extracted_number should be set to
|
|
// empty string.
|
|
return Err(ExtractNumberError::NoValidStartCharacter)
|
|
}
|
|
|
|
let mut extracted_number = &phone_number[i..];
|
|
extracted_number = self.trim_unwanted_end_chars(extracted_number);
|
|
if extracted_number.len() == 0 {
|
|
return Err(ExtractNumberError::NotANumber)
|
|
}
|
|
|
|
// Now remove any extra numbers at the end.
|
|
return Ok(
|
|
self.reg_exps.capture_up_to_second_number_start_pattern
|
|
.find(&extracted_number)
|
|
.map(move | m | m.as_str() )
|
|
.unwrap_or("")
|
|
)
|
|
}
|
|
|
|
// Note if any new field is added to this method that should always be filled
|
|
// in, even when keepRawInput is false, it should also be handled in the
|
|
// CopyCoreFieldsOnly() method.
|
|
fn parse_helper(
|
|
&self,
|
|
number_to_parse: &str,
|
|
default_region: &str,
|
|
keep_raw_input: bool,
|
|
check_region: bool,
|
|
) -> ParseResult<PhoneNumber> {
|
|
let national_number = self.build_national_number_for_parsing(number_to_parse)?;
|
|
if !self.is_viable_phone_number(&national_number) {
|
|
trace!("The string supplied did not seem to be a phone number {national_number}.");
|
|
return Err(ParseError::NotANumber)
|
|
}
|
|
|
|
if check_region && !self.check_region_for_parsing(&national_number, default_region) {
|
|
trace!("Missing or invalid default country.");
|
|
return Err(ParseError::InvalidCountryCodeError)
|
|
}
|
|
let mut temp_number = PhoneNumber::new();
|
|
if keep_raw_input {
|
|
temp_number.set_raw_input(number_to_parse.to_owned());
|
|
}
|
|
// Attempt to parse extension first, since it doesn't require country-specific
|
|
// data and we want to have the non-normalised number here.
|
|
|
|
let (national_number, extension) = self
|
|
.maybe_strip_extension(&national_number);
|
|
|
|
if let Some(extension) = extension {
|
|
temp_number.set_extension(extension.to_owned());
|
|
}
|
|
let mut country_metadata = self.get_metadata_for_region(default_region);
|
|
// Check to see if the number is given in international format so we know
|
|
// whether this number is from the default country or not.
|
|
let mut normalized_national_number = self
|
|
.maybe_extract_country_code(
|
|
country_metadata, keep_raw_input, &national_number, &mut temp_number
|
|
).or_else(| err | {
|
|
if !matches!(err, ParseError::InvalidCountryCodeError) {
|
|
return Err(err)
|
|
}
|
|
let plus_match = self.reg_exps.plus_chars_pattern.find_start(national_number);
|
|
if let Some(plus_match) = plus_match {
|
|
let normalized_national_number = &national_number[plus_match.end()..];
|
|
// Strip the plus-char, and try again.
|
|
let normalized_national_number = self
|
|
.maybe_extract_country_code(country_metadata,
|
|
keep_raw_input,
|
|
normalized_national_number,
|
|
&mut temp_number
|
|
)?;
|
|
if temp_number.country_code() == 0 {
|
|
return Err(ParseError::InvalidCountryCodeError.into());
|
|
}
|
|
return Ok(normalized_national_number);
|
|
}
|
|
Err(err)
|
|
})?;
|
|
|
|
let mut country_code = temp_number.country_code();
|
|
if country_code != 0 {
|
|
let phone_number_region = self.get_region_code_for_country_code(country_code);
|
|
if phone_number_region != default_region {
|
|
country_metadata = self
|
|
.get_metadata_for_region_or_calling_code(country_code, phone_number_region);
|
|
}
|
|
}
|
|
else if let Some(country_metadata) = country_metadata {
|
|
// If no extracted country calling code, use the region supplied instead.
|
|
// Note that the national number was already normalized by
|
|
// MaybeExtractCountryCode.
|
|
country_code = country_metadata.country_code();
|
|
}
|
|
if normalized_national_number.len() < MIN_LENGTH_FOR_NSN{
|
|
trace!("The string supplied is too short to be a phone number '{}'.", normalized_national_number);
|
|
return Err(ParseError::TooShortNsn.into())
|
|
}
|
|
if let Some(country_metadata) = country_metadata {
|
|
let potential_national_number = normalized_national_number.clone();
|
|
|
|
let phone_number_and_carrier_code = self.maybe_strip_national_prefix_and_carrier_code(
|
|
country_metadata, &potential_national_number
|
|
)?;
|
|
|
|
let carrier_code = phone_number_and_carrier_code.as_ref().and_then(| p | p.carrier_code.map(| c | c.to_string()));
|
|
let potential_national_number= if let Some(phone_number_and_carrier_code) = phone_number_and_carrier_code {
|
|
Cow::Owned(phone_number_and_carrier_code.phone_number.to_string())
|
|
} else {
|
|
potential_national_number
|
|
};
|
|
|
|
// We require that the NSN remaining after stripping the national prefix
|
|
// and carrier code be long enough to be a possible length for the region.
|
|
// Otherwise, we don't do the stripping, since the original number could be
|
|
// a valid short number.
|
|
let validation_result = test_number_length_with_unknown_type(
|
|
&potential_national_number, country_metadata
|
|
);
|
|
if !validation_result.is_ok_and(| res | matches!(res, ValidNumberLenType::IsPossibleLocalOnly))
|
|
&& !validation_result.is_err_and(|err | matches!(
|
|
err, ValidationResultErr::TooShort | ValidationResultErr::InvalidLength
|
|
)) {
|
|
normalized_national_number = potential_national_number;
|
|
if let Some(carrier_code) = carrier_code.filter(|_| keep_raw_input) {
|
|
temp_number.set_preferred_domestic_carrier_code(carrier_code.to_owned());
|
|
}
|
|
}
|
|
}
|
|
let normalized_national_number_length = normalized_national_number.len();
|
|
if normalized_national_number_length < MIN_LENGTH_FOR_NSN {
|
|
trace!("The string supplied is too short to be a phone number: '{}'.", normalized_national_number);
|
|
return Err(ParseError::TooShortNsn.into())
|
|
}
|
|
if normalized_national_number_length > MAX_LENGTH_FOR_NSN {
|
|
trace!("The string supplied is too long to be a phone number: '{}'.", normalized_national_number);
|
|
return Err(ParseError::TooLongNsn.into())
|
|
}
|
|
temp_number.set_country_code(country_code);
|
|
|
|
if let Some(zeroes_count) = Self::get_italian_leading_zeros_for_phone_number(&normalized_national_number) {
|
|
temp_number.set_italian_leading_zero(true);
|
|
temp_number.set_number_of_leading_zeros(zeroes_count as i32);
|
|
}
|
|
let number_as_int = u64::from_str_radix(&normalized_national_number, 10);
|
|
match number_as_int {
|
|
Ok(number_as_int) => temp_number.set_national_number(number_as_int),
|
|
Err(err) => return Err(ParseError::ParseNumberAsIntError(err).into())
|
|
}
|
|
return Ok(temp_number);
|
|
}
|
|
|
|
/// Checks to see if the string of characters could possibly be a phone number at
|
|
/// all. At the moment, checks to see that the string begins with at least 3
|
|
/// digits, ignoring any punctuation commonly found in phone numbers. This
|
|
/// method does not require the number to be normalized in advance - but does
|
|
/// assume that leading non-number symbols have been removed, such as by the
|
|
/// method `ExtractPossibleNumber`.
|
|
fn is_viable_phone_number(&self, phone_number: &str) -> bool {
|
|
if phone_number.len() < MIN_LENGTH_FOR_NSN {
|
|
false
|
|
} else {
|
|
self.reg_exps.valid_phone_number_pattern.full_match(phone_number)
|
|
}
|
|
}
|
|
|
|
/// Checks to see that the region code used is valid, or if it is not valid, that
|
|
/// the number to parse starts with a + symbol so that we can attempt to infer
|
|
/// the country from the number. Returns false if it cannot use the region
|
|
/// provided and the region cannot be inferred.
|
|
fn check_region_for_parsing(
|
|
&self,
|
|
number_to_parse: &str,
|
|
default_region: &str
|
|
) -> bool {
|
|
self.get_metadata_for_region(default_region).is_some()
|
|
|| number_to_parse.is_empty()
|
|
|| self.reg_exps.plus_chars_pattern.matches_start(number_to_parse)
|
|
}
|
|
|
|
/// Strips any extension (as in, the part of the number dialled after the call is
|
|
/// connected, usually indicated with extn, ext, x or similar) from the end of
|
|
/// the number, and returns stripped number and extension. The number passed in should be non-normalized.
|
|
fn maybe_strip_extension<'a>(&self, phone_number: &'a str) -> (&'a str, Option<&'a str>) {
|
|
let Some(captures) = self.reg_exps.extn_pattern.captures(phone_number) else {
|
|
return (phone_number, None);
|
|
};
|
|
|
|
let full_capture = captures.get(0).expect("first capture MUST always be not None");
|
|
// Replace the extensions in the original string here.
|
|
let phone_number_no_extn = &phone_number[..full_capture.start()];
|
|
// If we find a potential extension, and the number preceding this is a
|
|
// viable number, we assume it is an extension.
|
|
if !self.is_viable_phone_number(&phone_number_no_extn) {
|
|
return (phone_number, None);
|
|
}
|
|
if let Some(ext) = captures.iter().skip(1).flatten().find(|m| !m.is_empty()) {
|
|
return (phone_number_no_extn, Some(ext.as_str()));
|
|
}
|
|
|
|
(phone_number, None)
|
|
}
|
|
|
|
// Tries to extract a country calling code from a number. Country calling codes
|
|
// are extracted in the following ways:
|
|
// - by stripping the international dialing prefix of the region the person
|
|
// is dialing from, if this is present in the number, and looking at the next
|
|
// digits
|
|
// - by stripping the '+' sign if present and then looking at the next digits
|
|
// - by comparing the start of the number and the country calling code of the
|
|
// default region. If the number is not considered possible for the numbering
|
|
// plan of the default region initially, but starts with the country calling
|
|
// code of this region, validation will be reattempted after stripping this
|
|
// country calling code. If this number is considered a possible number, then
|
|
// the first digits will be considered the country calling code and removed as
|
|
// such.
|
|
//
|
|
// Returns NO_PARSING_ERROR if a country calling code was successfully
|
|
// extracted or none was present, or the appropriate error otherwise, such as
|
|
// if a + was present but it was not followed by a valid country calling code.
|
|
// If NO_PARSING_ERROR is returned, the national_number without the country
|
|
// calling code is populated, and the country_code of the phone_number passed
|
|
// in is set to the country calling code if found, otherwise to 0.
|
|
fn maybe_extract_country_code<'a>(
|
|
&self,
|
|
default_region_metadata: Option<&PhoneMetadata>,
|
|
keep_raw_input: bool,
|
|
national_number: &'a str,
|
|
phone_number: &mut PhoneNumber
|
|
) -> std::result::Result<Cow<'a, str>, ParseError> {
|
|
// Set the default prefix to be something that will never match if there is no
|
|
// default region.
|
|
let possible_country_idd_prefix = if let Some(default_region_metadata) = default_region_metadata {
|
|
default_region_metadata.international_prefix()
|
|
} else {
|
|
"NonMatch"
|
|
};
|
|
|
|
let phone_number_with_country_code_source = self
|
|
.maybe_strip_international_prefix_and_normalize(national_number, possible_country_idd_prefix)?;
|
|
|
|
let national_number = phone_number_with_country_code_source.phone_number;
|
|
if keep_raw_input {
|
|
phone_number.set_country_code_source(phone_number_with_country_code_source.country_code_source);
|
|
}
|
|
if !matches!(phone_number_with_country_code_source.country_code_source, CountryCodeSource::FROM_DEFAULT_COUNTRY) {
|
|
if national_number.len() <= MIN_LENGTH_FOR_NSN {
|
|
trace!("Phone number {} had an IDD, but after this was not \
|
|
long enough to be a viable phone number.", national_number);
|
|
return Err(ParseError::TooShortAfterIdd)
|
|
}
|
|
let Some((national_number, potential_country_code)) = self.extract_country_code(national_number) else {
|
|
// If this fails, they must be using a strange country calling code that we
|
|
// don't recognize, or that doesn't exist.
|
|
return Err(ParseError::InvalidCountryCodeError);
|
|
};
|
|
phone_number.set_country_code(potential_country_code);
|
|
return Ok(national_number);
|
|
} else if let Some(default_region_metadata) = default_region_metadata {
|
|
// Check to see if the number starts with the country calling code for the
|
|
// default region. If so, we remove the country calling code, and do some
|
|
// checks on the validity of the number before and after.
|
|
let default_country_code = default_region_metadata.country_code();
|
|
let mut buf = itoa::Buffer::new();
|
|
let default_country_code_string = buf.format(default_country_code);
|
|
trace!("Possible country calling code for number '{}': {}", national_number, default_country_code_string);
|
|
if let Some(potential_national_number) = strip_cow_prefix(
|
|
national_number.clone(), default_country_code_string
|
|
) {
|
|
let general_num_desc = &default_region_metadata.general_desc;
|
|
let phone_number_and_carrier_code = self
|
|
.maybe_strip_national_prefix_and_carrier_code(
|
|
default_region_metadata,&potential_national_number,
|
|
)?;
|
|
|
|
trace!("Number without country calling code prefix: {:?}", phone_number_and_carrier_code);
|
|
// If the number was not valid before but is valid now, or if it was too
|
|
// long before, we consider the number with the country code stripped to
|
|
// be a better result and keep that instead.
|
|
if (!helper_functions::is_match(
|
|
&self.matcher_api, &national_number, general_num_desc
|
|
) && helper_functions::is_match(
|
|
&self.matcher_api, &potential_national_number, general_num_desc
|
|
)) ||
|
|
test_number_length_with_unknown_type(
|
|
&national_number, default_region_metadata
|
|
).is_err_and(| e | matches!(e, ValidationResultErr::TooLong)) {
|
|
|
|
if keep_raw_input {
|
|
phone_number.set_country_code_source(
|
|
CountryCodeSource::FROM_NUMBER_WITHOUT_PLUS_SIGN
|
|
);
|
|
}
|
|
phone_number.set_country_code(default_country_code);
|
|
return Ok(potential_national_number);
|
|
}
|
|
}
|
|
}
|
|
// No country calling code present. Set the country_code to 0.
|
|
phone_number.set_country_code(0);
|
|
return Ok(national_number);
|
|
}
|
|
|
|
/// Gets a valid fixed-line number for the specified region_code. Returns false
|
|
/// if no number exists.
|
|
fn get_example_number(&self, region_code: &str) -> ExampleNumberResult {
|
|
self.get_example_number_for_type_and_region_code(region_code, PhoneNumberType::FixedLine)
|
|
}
|
|
|
|
fn get_invalid_example_number(&self, region_code: &str) -> ExampleNumberResult {
|
|
let Some(region_metadata) = self.region_to_metadata_map.get(region_code) else {
|
|
warn!("Invalid or unknown region code ({}) provided.", region_code);
|
|
return Err(GetExampleNumberError::InvalidMetadataError)
|
|
};
|
|
|
|
// We start off with a valid fixed-line number since every country supports
|
|
// this. Alternatively we could start with a different number type, since
|
|
// fixed-line numbers typically have a wide breadth of valid number lengths
|
|
// and we may have to make it very short before we get an invalid number.
|
|
let desc = get_number_desc_by_type(
|
|
region_metadata, PhoneNumberType::FixedLine
|
|
);
|
|
|
|
if !desc.has_example_number() {
|
|
// This shouldn't happen - we have a test for this.
|
|
return Err(GetExampleNumberError::NoExampleNumberError)
|
|
}
|
|
|
|
let example_number = desc.example_number();
|
|
// Try and make the number invalid. We do this by changing the length. We try
|
|
// reducing the length of the number, since currently no region has a number
|
|
// that is the same length as kMinLengthForNsn. This is probably quicker than
|
|
// making the number longer, which is another alternative. We could also use
|
|
// the possible number pattern to extract the possible lengths of the number
|
|
// to make this faster, but this method is only for unit-testing so simplicity
|
|
// is preferred to performance.
|
|
// We don't want to return a number that can't be parsed, so we check the
|
|
// number is long enough. We try all possible lengths because phone number
|
|
// plans often have overlapping prefixes so the number 123456 might be valid
|
|
// as a fixed-line number, and 12345 as a mobile number. It would be faster to
|
|
// loop in a different order, but we prefer numbers that look closer to real
|
|
// numbers (and it gives us a variety of different lengths for the resulting
|
|
// phone numbers - otherwise they would all be kMinLengthForNsn digits long.)
|
|
for phone_number_length in (MIN_LENGTH_FOR_NSN..=example_number.len().saturating_sub(1)).rev() {
|
|
let number_to_try = &example_number[0..phone_number_length];
|
|
let Ok(possibly_valid_number) = self.parse(&number_to_try, region_code) else {
|
|
continue
|
|
};
|
|
// We don't check the return value since we have already checked the
|
|
// length, we know example numbers have only valid digits, and we know the
|
|
// region code is fine.
|
|
if !self.is_valid_number(&possibly_valid_number)? {
|
|
return Ok(possibly_valid_number)
|
|
}
|
|
}
|
|
// We have a test to check that this doesn't happen for any of our supported
|
|
Err(GetExampleNumberError::CouldNotGetNumberError)
|
|
}
|
|
|
|
// Gets a valid number for the specified region_code and type. Returns false if
|
|
// no number exists.
|
|
fn get_example_number_for_type_and_region_code(
|
|
&self,
|
|
region_code: &str,
|
|
phone_number_type: PhoneNumberType,
|
|
) -> ExampleNumberResult {
|
|
let Some(region_metadata) = self.region_to_metadata_map.get(region_code) else {
|
|
warn!("Invalid or unknown region code ({}) provided.", region_code);
|
|
return Err(GetExampleNumberError::InvalidMetadataError)
|
|
};
|
|
let desc = get_number_desc_by_type(region_metadata, phone_number_type);
|
|
if desc.has_example_number() {
|
|
return Ok(
|
|
self.parse(desc.example_number(), region_code)
|
|
.inspect_err(| err | error!("Error parsing example number ({:?})", err))?
|
|
);
|
|
}
|
|
Err(GetExampleNumberError::CouldNotGetNumberError)
|
|
}
|
|
|
|
fn get_example_number_for_type(
|
|
&self,
|
|
phone_number_type: PhoneNumberType,
|
|
) -> ExampleNumberResult {
|
|
if let Some(number) = self.get_supported_regions().iter()
|
|
.find_map(| region_code |
|
|
self.get_example_number_for_type_and_region_code(region_code, phone_number_type).ok()
|
|
) {
|
|
return Ok(number);
|
|
}
|
|
|
|
// If there wasn't an example number for a region, try the non-geographical
|
|
// entities.
|
|
if let Some(res) = self.get_supported_global_network_calling_codes().into_iter()
|
|
.find_map(| country_calling_code | {
|
|
|
|
let Some(metadata) = self
|
|
.country_code_to_non_geographical_metadata_map.get(&country_calling_code) else {
|
|
return Some(Err(GetExampleNumberError::InvalidMetadataError));
|
|
};
|
|
let desc = get_number_desc_by_type(metadata, phone_number_type);
|
|
if desc.has_example_number() {
|
|
let mut buf = itoa::Buffer::new();
|
|
return Some(
|
|
self.parse(&fast_cat::concat_str!(
|
|
PLUS_SIGN, buf.format(country_calling_code), desc.example_number()
|
|
), i18n::RegionCode::get_unknown())
|
|
.map_err(| err | GetExampleNumberError::ParseError(err))
|
|
)
|
|
}
|
|
None
|
|
}) {
|
|
return res;
|
|
}
|
|
// There are no example numbers of this type for any country in the library.
|
|
Err(GetExampleNumberError::CouldNotGetNumberError)
|
|
}
|
|
|
|
fn get_example_number_for_non_geo_entity(
|
|
&self,
|
|
country_calling_code: i32
|
|
) -> ExampleNumberResult {
|
|
let Some(metadata) = self.country_code_to_non_geographical_metadata_map
|
|
.get(&country_calling_code) else {
|
|
|
|
warn!("Invalid or unknown country calling code provided: {}", country_calling_code);
|
|
return Err(GetExampleNumberError::InvalidMetadataError)
|
|
};
|
|
// For geographical entities, fixed-line data is always present. However,
|
|
// for non-geographical entities, this is not the case, so we have to go
|
|
// through different types to find the example number. We don't check
|
|
// fixed-line or personal number since they aren't used by non-geographical
|
|
// entities (if this changes, a unit-test will catch this.)
|
|
const NUMBER_TYPES_COUNT: usize = 7;
|
|
|
|
let types: [_; NUMBER_TYPES_COUNT] = [
|
|
&metadata.mobile,
|
|
&metadata.toll_free,
|
|
&metadata.shared_cost,
|
|
&metadata.voip,
|
|
&metadata.voicemail,
|
|
&metadata.uan,
|
|
&metadata.premium_rate
|
|
];
|
|
for number_type in types {
|
|
if !number_type.has_example_number() {
|
|
continue
|
|
}
|
|
let mut buf = itoa::Buffer::new();
|
|
return Ok(self.parse(&fast_cat::concat_str!(
|
|
PLUS_SIGN, buf.format(country_calling_code), number_type.example_number()
|
|
), i18n::RegionCode::get_unknown()
|
|
)?)
|
|
}
|
|
return Err(GetExampleNumberError::CouldNotGetNumberError)
|
|
}
|
|
|
|
/// Strips any international prefix (such as +, 00, 011) present in the number
|
|
/// provided, normalizes the resulting number, and indicates if an international
|
|
/// prefix was present.
|
|
///
|
|
/// possible_idd_prefix represents the international direct dialing prefix from
|
|
/// the region we think this number may be dialed in.
|
|
/// Returns true if an international dialing prefix could be removed from the
|
|
/// number, otherwise false if the number did not seem to be in international
|
|
/// format.
|
|
fn maybe_strip_international_prefix_and_normalize<'a>(
|
|
&self,
|
|
phone_number: &'a str,
|
|
possible_idd_prefix: &str,
|
|
) -> RegexResult<PhoneNumberWithCountryCodeSource<'a>> {
|
|
if phone_number.is_empty() {
|
|
Ok(PhoneNumberWithCountryCodeSource::new(
|
|
Cow::Borrowed(phone_number), CountryCodeSource::FROM_DEFAULT_COUNTRY
|
|
))
|
|
} else if let Some(number_string_piece) = self.reg_exps.plus_chars_pattern.find_start(phone_number) {
|
|
// Can now normalize the rest of the number since we've consumed the "+"
|
|
// sign at the start.
|
|
Ok(PhoneNumberWithCountryCodeSource::new(
|
|
Cow::Owned(self.normalize(number_string_piece.as_str())),
|
|
CountryCodeSource::FROM_NUMBER_WITH_PLUS_SIGN
|
|
))
|
|
} else {
|
|
// Attempt to parse the first digits as an international prefix.
|
|
let idd_pattern = self.reg_exps
|
|
.regexp_cache
|
|
.get_regex(possible_idd_prefix)?;
|
|
let normalized_number = self.normalize(phone_number);
|
|
let value =
|
|
if let Some(stripped_prefix_number) = self
|
|
.parse_prefix_as_idd(&normalized_number, idd_pattern) {
|
|
PhoneNumberWithCountryCodeSource::new(
|
|
Cow::Owned(stripped_prefix_number.to_owned()),
|
|
CountryCodeSource::FROM_NUMBER_WITH_IDD
|
|
)
|
|
} else {
|
|
PhoneNumberWithCountryCodeSource::new(
|
|
Cow::Owned(normalized_number),
|
|
CountryCodeSource::FROM_DEFAULT_COUNTRY
|
|
)
|
|
};
|
|
|
|
Ok(value)
|
|
}
|
|
}
|
|
|
|
/// Normalizes a string of characters representing a phone number. This performs
|
|
/// the following conversions:
|
|
/// - Punctuation is stripped.
|
|
/// For ALPHA/VANITY numbers:
|
|
/// - Letters are converted to their numeric representation on a telephone
|
|
/// keypad. The keypad used here is the one defined in ITU Recommendation
|
|
/// E.161. This is only done if there are 3 or more letters in the number, to
|
|
/// lessen the risk that such letters are typos.
|
|
/// For other numbers:
|
|
/// - Wide-ascii digits are converted to normal ASCII (European) digits.
|
|
/// - Arabic-Indic numerals are converted to European numerals.
|
|
/// - Spurious alpha characters are stripped.
|
|
fn normalize<'a>(&self, phone_number: &'a str) -> String {
|
|
if self.reg_exps.valid_alpha_phone_pattern.is_match(phone_number) {
|
|
normalize_helper(
|
|
&self.reg_exps.alpha_phone_mappings,
|
|
true,
|
|
phone_number
|
|
)
|
|
} else {
|
|
self.normalize_digits_only(phone_number)
|
|
}
|
|
}
|
|
|
|
/// Strips the IDD from the start of the number if present. Helper function used
|
|
/// by MaybeStripInternationalPrefixAndNormalize.
|
|
fn parse_prefix_as_idd<'a>(&self, phone_number: & 'a str, idd_pattern: Arc<Regex>) -> Option<&'a str> {
|
|
// First attempt to strip the idd_pattern at the start, if present. We make a
|
|
// copy so that we can revert to the original string if necessary.
|
|
let Some(idd_pattern_match) = idd_pattern.find_start(&phone_number) else {
|
|
return None
|
|
};
|
|
let captured_range_end = idd_pattern_match.end();
|
|
|
|
// Only strip this if the first digit after the match is not a 0, since
|
|
// country calling codes cannot begin with 0.
|
|
if phone_number[captured_range_end..]
|
|
.chars()
|
|
.find(| c | c.is_decimal_utf8())
|
|
.and_then(| c | c.to_decimal_utf8()) == Some(0) {
|
|
return None;
|
|
}
|
|
Some(&phone_number[captured_range_end..])
|
|
}
|
|
|
|
/// Extracts country calling code from national_number, and returns tuple
|
|
/// that contains national_number without calling code and calling code itself.
|
|
///
|
|
/// It assumes that the leading plus sign or IDD has already been removed.
|
|
///
|
|
/// Returns None if national_number doesn't start with a valid country calling code
|
|
/// Assumes the national_number is at least 3 characters long.
|
|
fn extract_country_code<'a>(&self, national_number: Cow<'a, str>) -> Option<(Cow<'a, str>, i32)> {
|
|
if national_number.as_ref().is_empty() || national_number.as_ref().starts_with('0') {
|
|
return None
|
|
}
|
|
for i in 0..=MAX_LENGTH_COUNTRY_CODE {
|
|
let Ok(potential_country_code) = i32
|
|
::from_str_radix(&national_number.as_ref()[0..i], 10) else {
|
|
continue;
|
|
};
|
|
let region_code = self.get_region_code_for_country_code(potential_country_code);
|
|
if region_code != i18n::RegionCode::get_unknown() {
|
|
return match national_number {
|
|
Cow::Borrowed(s) => Some((Cow::Borrowed(&s[i..]), potential_country_code)),
|
|
Cow::Owned(mut s) => {
|
|
s.drain(0..i);
|
|
Some((Cow::Owned(s), potential_country_code))
|
|
}
|
|
};
|
|
}
|
|
}
|
|
return None
|
|
}
|
|
|
|
// Strips any national prefix (such as 0, 1) present in the number provided.
|
|
// The number passed in should be the normalized telephone number that we wish
|
|
// to strip any national dialing prefix from. The metadata should be for the
|
|
// region that we think this number is from. Returns true if a national prefix
|
|
// and/or carrier code was stripped.
|
|
fn maybe_strip_national_prefix_and_carrier_code<'a>(
|
|
&self,
|
|
metadata: &PhoneMetadata,
|
|
phone_number: &'a str,
|
|
) -> RegexResult<Option<PhoneNumberAndCarrierCode<'a>>> {
|
|
let possible_national_prefix = metadata.national_prefix_for_parsing();
|
|
if phone_number.is_empty() || possible_national_prefix.is_empty() {
|
|
// Early return for numbers of zero length or with no national prefix
|
|
// possible.
|
|
return Ok(None);
|
|
}
|
|
let general_desc = &metadata.general_desc;
|
|
// Check if the original number is viable.
|
|
let is_viable_original_number = helper_functions::is_match(&self.matcher_api, &phone_number, general_desc);
|
|
// Attempt to parse the first digits as a national prefix. We make a
|
|
// copy so that we can revert to the original string if necessary.
|
|
let transform_rule = metadata.national_prefix_transform_rule();
|
|
|
|
let possible_national_prefix_pattern =
|
|
self.reg_exps.regexp_cache.get_regex(possible_national_prefix)?;
|
|
|
|
let captures = possible_national_prefix_pattern.captures_start(&phone_number);
|
|
let first_capture = captures.as_ref().and_then(| c | c.get(1));
|
|
let second_capture = captures.as_ref().and_then(| c | c.get(2));
|
|
|
|
if !transform_rule.is_empty() &&
|
|
second_capture.is_some_and(| c | !c.is_empty() && first_capture.is_some())
|
|
|| first_capture.is_some_and(| c | !c.is_empty() && second_capture.is_none()) {
|
|
|
|
let first_capture = first_capture.unwrap();
|
|
let carrier_code_temp = if second_capture.is_some() {
|
|
Some(first_capture.as_str())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// If this succeeded, then we must have had a transform rule and there must
|
|
// have been some part of the prefix that we captured.
|
|
// We make the transformation and check that the resultant number is still
|
|
// viable. If so, replace the number and return.
|
|
let replaced_number = possible_national_prefix_pattern.replace(
|
|
&phone_number, transform_rule
|
|
);
|
|
if is_viable_original_number &&
|
|
!helper_functions::is_match(&self.matcher_api, &replaced_number, general_desc){
|
|
return Ok(None);
|
|
}
|
|
return Ok(Some(PhoneNumberAndCarrierCode::new(carrier_code_temp, replaced_number)));
|
|
} else if let Some(matched) = captures.and_then(| c | c.get(0)) {
|
|
trace!("Parsed the first digits as a national prefix for number '{}'.", phone_number);
|
|
// If captured_part_of_prefix is empty, this implies nothing was captured by
|
|
// the capturing groups in possible_national_prefix; therefore, no
|
|
// transformation is necessary, and we just remove the national prefix.
|
|
let stripped_number = &phone_number[matched.end()..];
|
|
if is_viable_original_number &&
|
|
!helper_functions::is_match(&self.matcher_api, stripped_number, general_desc) {
|
|
return Ok(None)
|
|
}
|
|
return Ok(Some(PhoneNumberAndCarrierCode::new_phone(stripped_number)));
|
|
}
|
|
trace!("The first digits did not match the national prefix for number '{}'.", phone_number);
|
|
Ok(None)
|
|
}
|
|
|
|
// A helper function to set the values related to leading zeros in a
|
|
// PhoneNumber.
|
|
fn get_italian_leading_zeros_for_phone_number(
|
|
national_number: &str
|
|
) -> Option<usize> {
|
|
if national_number.len() < 2 {
|
|
return None
|
|
}
|
|
let zero_count = national_number.chars().take_while(| c| *c == '0').count();
|
|
// Note that if the national number is all "0"s, the last "0" is not
|
|
// counted as a leading zero.
|
|
if zero_count == national_number.len() {
|
|
return Some(zero_count-1);
|
|
}
|
|
|
|
Some(zero_count)
|
|
}
|
|
}
|