use std::sync::{
atomic::{AtomicU64, Ordering},
OnceLock,
};
use constants::COUNTRIES;
use definitions::Country;
pub use definitions::PhoneNumberType;
mod constants;
mod definitions;
mod tests;
struct CountryEntry {
country: &'static Country,
length_mask: u32,
}
fn prefix_table() -> &'static [Vec<CountryEntry>] {
static TABLE: OnceLock<Vec<Vec<CountryEntry>>> = OnceLock::new();
TABLE.get_or_init(|| {
let max_prefix = COUNTRIES.iter().map(|c| c.prefix).max().unwrap_or(0) as usize;
let mut table: Vec<Vec<CountryEntry>> = (0..=max_prefix).map(|_| Vec::new()).collect();
for country in COUNTRIES.iter() {
let mut mask = 0u32;
for &len in country.phone_lengths {
mask |= 1u32 << (len as u32);
}
table[country.prefix as usize].push(CountryEntry {
country,
length_mask: mask,
});
}
table
})
}
const NANP_TERRITORY_BITMAP: [u64; 16] = {
let mut bitmap = [0u64; 16];
let mut i = 0;
while i < COUNTRIES.len() {
let prefix = COUNTRIES[i].prefix;
if prefix >= 1000 && prefix < 2000 {
let idx = (prefix - 1000) as usize;
bitmap[idx >> 6] |= 1u64 << (idx & 63);
}
i += 1;
}
bitmap
};
fn random_seed() -> u64 {
static SEED_COUNTER: AtomicU64 = AtomicU64::new(0);
let time_seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos() as u64);
let counter = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
time_seed ^ counter.wrapping_mul(0x9E37_79B9_7F4A_7C15)
}
fn next_random_digit(seed: &mut u64) -> u8 {
*seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
((*seed / 65536) % 10) as u8
}
fn generate_random_national_number(length: usize, seed: &mut u64) -> String {
let mut national_number = String::with_capacity(length);
for index in 0..length {
let mut digit = next_random_digit(seed);
if index == 0 && digit == 0 {
digit = 1;
}
national_number.push(char::from(b'0' + digit));
}
national_number
}
#[inline]
fn build_e164(country: &Country, national: &str, prefix_len: usize) -> String {
let mut s = String::with_capacity(prefix_len + 1 + national.len());
s.push('+');
push_prefix_digits(&mut s, country.prefix);
s.push_str(national);
s
}
fn generate_valid_number(country: &Country, length: usize, seed: &mut u64) -> String {
let prefix_len = prefix_digit_count(country.prefix);
if country.prefix != 1 {
let national = generate_random_national_number(length, seed);
return build_e164(country, &national, prefix_len);
}
let mut candidate = String::new();
for _ in 0..64 {
let national = generate_random_national_number(length, seed);
candidate = build_e164(country, &national, prefix_len);
if normalize_phone_number(&candidate).as_deref() == Some(candidate.as_str()) {
return candidate;
}
}
candidate
}
pub fn is_valid_phone_number(phone_number: &str) -> bool {
if contains_invalid_character(phone_number) {
return false;
}
normalize_phone_number(phone_number).is_some()
}
pub fn extract_country(phone_number: &str) -> Option<&'static Country> {
normalize_and_extract(phone_number).map(|(_, country, _)| country)
}
pub fn normalize_phone_number(phone_number: &str) -> Option<String> {
normalize_and_extract(phone_number).map(|(normalized, _, _)| normalized)
}
pub fn normalize_phone_number_in_place(phone_number: &mut String) -> Option<String> {
let stripped_len = strip_extension(phone_number).len();
phone_number.truncate(stripped_len);
if phone_number.bytes().any(|byte| byte.is_ascii_alphabetic()) {
let converted = convert_vanity_letters(phone_number);
phone_number.clear();
phone_number.push_str(&converted);
}
remove_unwanted_character(phone_number);
let country = extract_country_data(phone_number)?;
let plen = prefix_digit_count(country.prefix);
phone_number.drain(0..plen);
let had_leading_zero = phone_number.as_bytes().first() == Some(&b'0');
leading_zero_remover(phone_number);
if had_leading_zero && !country.phone_lengths.contains(&(phone_number.len() as u8)) {
return None;
}
let mut normalized = String::with_capacity(phone_number.len() + plen + 1);
normalized.push('+');
push_prefix_digits(&mut normalized, country.prefix);
normalized.push_str(phone_number);
Some(normalized)
}
fn normalize_and_extract(input: &str) -> Option<(String, &'static Country, PhoneNumberType)> {
let stripped = strip_extension(input);
let processed_storage;
let processed_input = if stripped.bytes().any(|byte| byte.is_ascii_alphabetic()) {
processed_storage = convert_vanity_letters(stripped);
processed_storage.as_str()
} else {
stripped
};
let mut digits = [0u8; 20];
let mut digit_count = 0;
let input_bytes = processed_input.as_bytes();
let scan_start = if input_bytes.first() == Some(&b'+') {
1
} else {
0
};
for &b in &input_bytes[scan_start..] {
if b.is_ascii_digit() {
if digit_count >= 20 {
return None;
}
digits[digit_count] = b;
digit_count += 1;
}
}
let mut start = 0;
while start < digit_count && digits[start] == b'0' {
start += 1;
}
if start >= digit_count {
return None;
}
let cleaned = unsafe { std::str::from_utf8_unchecked(&digits[start..digit_count]) };
let country = extract_country_data(cleaned)?;
let plen = prefix_digit_count(country.prefix);
let national_bytes = &digits[start + plen..digit_count];
let mut nat_start = 0;
while nat_start < national_bytes.len() && national_bytes[nat_start] == b'0' {
nat_start += 1;
}
let national = unsafe { std::str::from_utf8_unchecked(&national_bytes[nat_start..]) };
if nat_start > 0 && !country.phone_lengths.contains(&(national.len() as u8)) {
return None;
}
let phone_type = classify_phone_number_type(national, country);
let mut normalized = String::with_capacity(national.len() + plen + 1);
normalized.push('+');
push_prefix_digits(&mut normalized, country.prefix);
normalized.push_str(national);
Some((normalized, country, phone_type))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhoneFormat {
E164,
International,
National,
RFC3966,
}
pub fn format_phone_number(phone_number: &str, format: PhoneFormat) -> Option<String> {
let (normalized, country, _) = normalize_and_extract(phone_number)?;
match format {
PhoneFormat::E164 => Some(normalized),
_ => {
let plen = prefix_digit_count(country.prefix);
let national_number = &normalized[1 + plen..];
match format {
PhoneFormat::International => {
let formatted = format_national_number(national_number, country);
let mut result = String::with_capacity(2 + plen + formatted.len());
result.push('+');
push_prefix_digits(&mut result, country.prefix);
result.push(' ');
result.push_str(&formatted);
Some(result)
}
PhoneFormat::National => Some(format_national_number(national_number, country)),
PhoneFormat::RFC3966 => {
let national = format_national_number_rfc3966(national_number, country);
let mut result = String::with_capacity(6 + plen + national.len());
result.push_str("tel:+");
push_prefix_digits(&mut result, country.prefix);
result.push('-');
result.push_str(&national);
Some(result)
}
_ => unreachable!(),
}
}
}
}
fn format_national_number(number: &str, country: &Country) -> String {
match country.code {
"US" | "CA" if number.len() == 10 => {
let mut s = String::with_capacity(14);
s.push('(');
s.push_str(&number[0..3]);
s.push_str(") ");
s.push_str(&number[3..6]);
s.push('-');
s.push_str(&number[6..]);
s
}
_ => {
let group_sizes = national_format_group_sizes(country.code, number.len())
.or_else(|| default_national_group_sizes(number.len()));
group_sizes
.and_then(|groups| format_grouped_number(number, groups, ' '))
.unwrap_or_else(|| number.to_string())
}
}
}
fn national_format_group_sizes(country_code: &str, number_len: usize) -> Option<&'static [usize]> {
match (country_code, number_len) {
("GB", 10) => Some(&[4, 3, 3]),
("DE", 10) => Some(&[2, 4, 4]),
("DE", 11) => Some(&[3, 4, 4]),
("FR", 9) => Some(&[1, 2, 2, 2, 2]),
("IN", 10) => Some(&[5, 5]),
("AU", 9) => Some(&[1, 4, 4]),
_ => None,
}
}
fn default_national_group_sizes(number_len: usize) -> Option<&'static [usize]> {
match number_len {
7 => Some(&[3, 4]),
8 => Some(&[4, 4]),
9 => Some(&[3, 3, 3]),
10 => Some(&[3, 3, 4]),
11 => Some(&[3, 4, 4]),
12 => Some(&[3, 3, 3, 3]),
13 => Some(&[3, 3, 3, 4]),
14 => Some(&[3, 3, 4, 4]),
15 => Some(&[3, 4, 4, 4]),
_ => None,
}
}
fn format_grouped_number(number: &str, groups: &[usize], separator: char) -> Option<String> {
if groups.iter().sum::<usize>() != number.len() {
return None;
}
let mut formatted = String::with_capacity(number.len() + groups.len().saturating_sub(1));
let mut start = 0;
for (index, &group_len) in groups.iter().enumerate() {
if index > 0 {
formatted.push(separator);
}
let end = start + group_len;
formatted.push_str(&number[start..end]);
start = end;
}
Some(formatted)
}
fn format_national_number_rfc3966(number: &str, country: &Country) -> String {
national_format_group_sizes(country.code, number.len())
.or_else(|| default_national_group_sizes(number.len()))
.and_then(|groups| format_grouped_number(number, groups, '-'))
.unwrap_or_else(|| number.to_string())
}
pub fn detect_phone_number_type(phone_number: &str) -> Option<PhoneNumberType> {
normalize_and_extract(phone_number).map(|(_, _, phone_type)| phone_type)
}
pub fn is_mobile_number(phone_number: &str) -> bool {
detect_phone_number_type(phone_number) == Some(PhoneNumberType::Mobile)
}
pub fn is_landline_number(phone_number: &str) -> bool {
detect_phone_number_type(phone_number) == Some(PhoneNumberType::FixedLine)
}
pub fn is_toll_free_number(phone_number: &str) -> bool {
detect_phone_number_type(phone_number) == Some(PhoneNumberType::TollFree)
}
fn classify_phone_number_type(national_number: &str, country: &Country) -> PhoneNumberType {
let bytes = national_number.as_bytes();
if bytes.is_empty() {
return PhoneNumberType::Unknown;
}
let d0 = bytes[0];
let n2: u16 = if bytes.len() >= 2 {
(bytes[0] - b'0') as u16 * 10 + (bytes[1] - b'0') as u16
} else {
u16::MAX
};
let n3: u16 = if bytes.len() >= 3 {
(bytes[0] - b'0') as u16 * 100 + (bytes[1] - b'0') as u16 * 10 + (bytes[2] - b'0') as u16
} else {
u16::MAX
};
match country.code {
"US" | "CA" => match n3 {
800 | 833 | 844 | 855 | 866 | 877 | 888 => PhoneNumberType::TollFree,
900 | 976 => PhoneNumberType::PremiumRate,
_ => {
if bytes.len() == 10 {
PhoneNumberType::FixedLine
} else {
PhoneNumberType::Unknown
}
}
},
"GB" => match d0 {
b'7' => PhoneNumberType::Mobile,
b'8' => match n2 {
80 | 84 | 87 => PhoneNumberType::TollFree,
81 | 82 | 89 => PhoneNumberType::PremiumRate,
_ => PhoneNumberType::SharedCost,
},
b'1' | b'2' => PhoneNumberType::FixedLine,
b'3' => PhoneNumberType::Uan,
b'5' => PhoneNumberType::Voip,
_ => PhoneNumberType::Unknown,
},
"DE" => match d0 {
b'1' => match n2 {
15..=17 => PhoneNumberType::Mobile,
18 => PhoneNumberType::SharedCost,
19 => PhoneNumberType::PremiumRate,
_ => PhoneNumberType::Unknown,
},
b'0' => PhoneNumberType::TollFree,
_ => PhoneNumberType::FixedLine,
},
"FR" => match d0 {
b'6' | b'7' => PhoneNumberType::Mobile,
b'8' => PhoneNumberType::TollFree,
b'1' | b'2' | b'3' | b'4' | b'5' | b'9' => PhoneNumberType::FixedLine,
_ => PhoneNumberType::Unknown,
},
"AU" => match d0 {
b'4' => PhoneNumberType::Mobile,
b'1' => match n3 {
180 | 188 => PhoneNumberType::TollFree,
190 => PhoneNumberType::PremiumRate,
_ => PhoneNumberType::Unknown,
},
b'2' | b'3' | b'7' | b'8' => PhoneNumberType::FixedLine,
_ => PhoneNumberType::Unknown,
},
"IN" => match d0 {
b'9' | b'8' | b'7' | b'6' => PhoneNumberType::Mobile,
b'1' | b'2' | b'3' | b'4' | b'5' => PhoneNumberType::FixedLine,
_ => PhoneNumberType::Unknown,
},
_ => match d0 {
b'9' | b'8' | b'7' | b'6' => PhoneNumberType::Mobile,
b'1' | b'2' | b'3' | b'4' | b'5' => PhoneNumberType::FixedLine,
b'0' => PhoneNumberType::TollFree,
_ => PhoneNumberType::Unknown,
},
}
}
pub fn generate_random_phone_number(country_code: &str) -> Option<String> {
let country = COUNTRIES.iter().find(|c| c.code == country_code)?;
let length = *country.phone_lengths.first()? as usize;
let mut seed = random_seed();
Some(generate_valid_number(country, length, &mut seed))
}
pub fn generate_random_phone_numbers(country_code: &str, count: usize) -> Vec<String> {
let country = match COUNTRIES.iter().find(|c| c.code == country_code) {
Some(country) => country,
None => return Vec::new(),
};
let length = match country.phone_lengths.first() {
Some(length) => *length as usize,
None => return Vec::new(),
};
let mut numbers = Vec::with_capacity(count);
let mut seed = random_seed();
for _ in 0..count {
numbers.push(generate_valid_number(country, length, &mut seed));
}
numbers
}
pub fn are_phone_numbers_equal(number1: &str, number2: &str) -> bool {
match (
normalize_phone_number(number1),
normalize_phone_number(number2),
) {
(Some(norm1), Some(norm2)) => norm1 == norm2,
_ => false,
}
}
pub fn group_equivalent_phone_numbers<T: AsRef<str>>(phone_numbers: &[T]) -> Vec<Vec<String>> {
let mut groups: Vec<Vec<String>> = Vec::new();
for number in phone_numbers {
let number_str = number.as_ref();
let mut found_group = false;
for group in &mut groups {
if let Some(representative) = group.first() {
if are_phone_numbers_equal(number_str, representative) {
group.push(number_str.to_string());
found_group = true;
break;
}
}
}
if !found_group {
groups.push(vec![number_str.to_string()]);
}
}
groups
}
pub fn validate_phone_numbers_batch<T: AsRef<str>>(phone_numbers: &[T]) -> Vec<bool> {
phone_numbers
.iter()
.map(|n| is_valid_phone_number(n.as_ref()))
.collect()
}
pub fn normalize_phone_numbers_batch<T: AsRef<str>>(phone_numbers: &[T]) -> Vec<Option<String>> {
phone_numbers
.iter()
.map(|n| normalize_phone_number(n.as_ref()))
.collect()
}
pub fn extract_countries_batch<T: AsRef<str>>(
phone_numbers: &[T],
) -> Vec<Option<&'static Country>> {
phone_numbers
.iter()
.map(|n| extract_country(n.as_ref()))
.collect()
}
pub fn detect_phone_number_types_batch<T: AsRef<str>>(
phone_numbers: &[T],
) -> Vec<Option<PhoneNumberType>> {
phone_numbers
.iter()
.map(|n| detect_phone_number_type(n.as_ref()))
.collect()
}
pub fn analyze_phone_numbers_batch<T: AsRef<str>>(phone_numbers: &[T]) -> Vec<PhoneNumberAnalysis> {
phone_numbers
.iter()
.map(|number| {
let number_str = number.as_ref();
match normalize_and_extract(number_str) {
Some((normalized, country, phone_type)) => PhoneNumberAnalysis {
original: number_str.to_string(),
is_valid: true,
normalized: Some(normalized),
country: Some(country),
phone_type: Some(phone_type),
},
None => PhoneNumberAnalysis {
original: number_str.to_string(),
is_valid: false,
normalized: None,
country: None,
phone_type: None,
},
}
})
.collect()
}
#[derive(Debug, Clone)]
pub struct PhoneNumberAnalysis {
pub original: String,
pub is_valid: bool,
pub normalized: Option<String>,
pub country: Option<&'static Country>,
pub phone_type: Option<PhoneNumberType>,
}
pub fn suggest_phone_number_corrections(
phone_number: &str,
country_hint: Option<&str>,
) -> Vec<String> {
if is_valid_phone_number(phone_number) {
return vec![phone_number.to_string()]; }
let mut suggestions = Vec::new();
let mut cleaned = phone_number.to_string();
remove_non_digit_character(&mut cleaned);
let hinted_country = country_hint.and_then(|hint| COUNTRIES.iter().find(|c| c.code == hint));
if let Some(country) = hinted_country {
let suggestion = format!("+{}{}", country.prefix, cleaned);
if is_valid_phone_number(&suggestion) {
suggestions.push(suggestion);
}
} else {
let common_countries = ["US", "GB", "DE", "FR", "IN", "AU", "CA"];
for &country_code in &common_countries {
if let Some(country) = COUNTRIES.iter().find(|c| c.code == country_code) {
let suggestion = format!("+{}{}", country.prefix, cleaned);
if is_valid_phone_number(&suggestion) {
suggestions.push(suggestion);
}
}
}
}
if cleaned.len() > 15 {
for i in 1..=(cleaned.len() - 7) {
let shortened = cleaned[i..].to_string();
if let Some(country) = hinted_country {
let suggestion = format!("+{}{}", country.prefix, shortened);
if is_valid_phone_number(&suggestion) {
suggestions.push(suggestion);
break;
}
}
}
}
if cleaned.len() < 10 {
for prefix in &["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] {
let extended = format!("{}{}", prefix, cleaned);
if let Some(country) = hinted_country {
let suggestion = format!("+{}{}", country.prefix, extended);
if is_valid_phone_number(&suggestion) {
suggestions.push(suggestion);
}
}
}
}
suggestions.sort();
suggestions.dedup();
suggestions.truncate(5);
suggestions
}
pub fn is_potentially_valid_phone_number(phone_number: &str) -> bool {
let mut cleaned = phone_number.to_string();
remove_non_digit_character(&mut cleaned);
cleaned.len() >= 7 && cleaned.len() <= 15 && !cleaned.chars().all(|c| c == '0')
}
pub fn guess_country_from_number(phone_number: &str) -> Option<&'static Country> {
let mut cleaned = phone_number.to_string();
remove_non_digit_character(&mut cleaned);
if cleaned.is_empty() {
return None;
}
for country in COUNTRIES.iter() {
let prefix_len = count_digits(country.prefix);
if cleaned.len() >= prefix_len {
if let Ok(parsed_prefix) = cleaned[0..prefix_len].parse::<u32>() {
if parsed_prefix == country.prefix {
let remaining_len = cleaned.len() - prefix_len;
if country.phone_lengths.contains(&(remaining_len as u8)) {
return Some(country);
}
}
}
}
}
match cleaned.len() {
10 => COUNTRIES.iter().find(|c| c.code == "US"), 11 if cleaned.starts_with('1') => COUNTRIES.iter().find(|c| c.code == "US"),
11 if cleaned.starts_with("44") => COUNTRIES.iter().find(|c| c.code == "GB"),
12 if cleaned.starts_with("49") => COUNTRIES.iter().find(|c| c.code == "DE"),
_ => None,
}
}
fn remove_unwanted_character(phone_number: &mut String) {
remove_non_digit_character(phone_number);
leading_zero_remover(phone_number);
}
fn contains_invalid_character(phone_number: &str) -> bool {
let mut parentheses_count = 0;
for (index, &byte) in phone_number.as_bytes().iter().enumerate() {
match byte {
b'0'..=b'9' | b'-' | b' ' | b'.' => {}
b'+' if index == 0 => {}
b'A'..=b'Z' | b'a'..=b'z' => {}
b'(' => parentheses_count += 1,
b')' if parentheses_count == 0 => return true,
b')' => parentheses_count -= 1,
_ => return true,
}
}
parentheses_count != 0
}
fn remove_non_digit_character(phone_number: &mut String) {
phone_number.retain(|c| c.is_ascii_digit());
}
fn strip_extension(input: &str) -> &str {
let bytes = input.as_bytes();
let len = bytes.len();
for i in 0..len {
if i + 4 <= len
&& (bytes[i] == b'e' || bytes[i] == b'E')
&& (bytes[i + 1] == b'x' || bytes[i + 1] == b'X')
&& (bytes[i + 2] == b't' || bytes[i + 2] == b'T')
&& (bytes[i + 3] == b'.' || bytes[i + 3] == b' ')
&& (i == 0 || !bytes[i - 1].is_ascii_alphabetic())
{
return input[..i].trim_end();
}
}
input
}
fn convert_vanity_letters(input: &str) -> String {
input
.chars()
.map(|c| match c.to_ascii_uppercase() {
'A' | 'B' | 'C' => '2',
'D' | 'E' | 'F' => '3',
'G' | 'H' | 'I' => '4',
'J' | 'K' | 'L' => '5',
'M' | 'N' | 'O' => '6',
'P' | 'Q' | 'R' | 'S' => '7',
'T' | 'U' | 'V' => '8',
'W' | 'X' | 'Y' | 'Z' => '9',
_ => c,
})
.collect()
}
fn leading_zero_remover(phone_number: &mut String) {
let first_non_zero = phone_number
.find(|c: char| c != '0')
.unwrap_or(phone_number.len());
if first_non_zero > 0 {
phone_number.drain(0..first_non_zero);
}
}
fn extract_country_data(phone_number: &str) -> Option<&'static Country> {
let bytes = phone_number.as_bytes();
let len = bytes.len();
if len == 0 {
return None;
}
let table = prefix_table();
let d0 = (bytes[0] - b'0') as u32;
if d0 > 9 {
return None;
}
if d0 == 1 {
if len >= 4 {
let d1 = (bytes[1] - b'0') as u32;
let d2 = (bytes[2] - b'0') as u32;
let d3 = (bytes[3] - b'0') as u32;
if d1 <= 9 && d2 <= 9 && d3 <= 9 {
let prefix4 = 1000 + d1 * 100 + d2 * 10 + d3;
let idx = (prefix4 - 1000) as usize;
if NANP_TERRITORY_BITMAP[idx >> 6] & (1u64 << (idx & 63)) != 0 {
let remaining = len - 4;
if remaining < 32 {
let bit = 1u32 << remaining;
for entry in &table[prefix4 as usize] {
if entry.length_mask & bit != 0 {
return Some(entry.country);
}
}
}
}
}
}
let remaining = len - 1;
if remaining < 32 {
let bit = 1u32 << remaining;
for entry in &table[1] {
if entry.length_mask & bit != 0 {
return Some(entry.country);
}
}
}
return None;
}
let remaining = len - 1;
if remaining < 32 {
let bit = 1u32 << remaining;
for entry in &table[d0 as usize] {
if entry.length_mask & bit != 0 {
return Some(entry.country);
}
}
}
if len >= 2 {
let d1 = (bytes[1] - b'0') as u32;
if d1 <= 9 {
let prefix2 = d0 * 10 + d1;
let remaining = len - 2;
if remaining < 32 {
let bit = 1u32 << remaining;
for entry in &table[prefix2 as usize] {
if entry.length_mask & bit != 0 {
return Some(entry.country);
}
}
}
if len >= 3 {
let d2 = (bytes[2] - b'0') as u32;
if d2 <= 9 {
let prefix3 = prefix2 * 10 + d2;
let remaining = len - 3;
if remaining < 32 {
let bit = 1u32 << remaining;
for entry in &table[prefix3 as usize] {
if entry.length_mask & bit != 0 {
return Some(entry.country);
}
}
}
}
}
}
}
None
}
#[inline(always)]
const fn prefix_digit_count(prefix: u32) -> usize {
if prefix >= 1000 {
4
} else if prefix >= 100 {
3
} else if prefix >= 10 {
2
} else {
1
}
}
#[inline(always)]
fn push_prefix_digits(s: &mut String, prefix: u32) {
if prefix >= 1000 {
s.push((b'0' + (prefix / 1000) as u8) as char);
}
if prefix >= 100 {
s.push((b'0' + ((prefix / 100) % 10) as u8) as char);
}
if prefix >= 10 {
s.push((b'0' + ((prefix / 10) % 10) as u8) as char);
}
s.push((b'0' + (prefix % 10) as u8) as char);
}
fn count_digits(n: u32) -> usize {
prefix_digit_count(n)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractedPhoneNumber {
pub raw: String,
pub normalized: Option<String>,
pub start: usize,
pub end: usize,
pub is_valid: bool,
}
pub fn extract_phone_numbers_from_text(text: &str) -> Vec<ExtractedPhoneNumber> {
let mut results = Vec::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if is_phone_number_start(&chars, i) {
if let Some((phone_str, start_byte, end_byte)) =
extract_phone_candidate(text, &chars, i)
{
let normalized = normalize_phone_number(&phone_str);
let is_valid = normalized.is_some();
let digit_count = phone_str.chars().filter(|c| c.is_ascii_digit()).count();
if digit_count >= 7 {
results.push(ExtractedPhoneNumber {
raw: phone_str,
normalized,
start: start_byte,
end: end_byte,
is_valid,
});
i = char_index_from_byte(text, end_byte);
continue;
}
}
}
i += 1;
}
results
}
pub fn extract_valid_phone_numbers_from_text(text: &str) -> Vec<ExtractedPhoneNumber> {
extract_phone_numbers_from_text(text)
.into_iter()
.filter(|n| n.is_valid)
.collect()
}
pub fn extract_phone_numbers_with_country_hint(
text: &str,
default_country: &str,
) -> Vec<ExtractedPhoneNumber> {
let country = COUNTRIES.iter().find(|c| c.code == default_country);
let mut results = Vec::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if is_phone_number_start(&chars, i) {
if let Some((phone_str, start_byte, end_byte)) =
extract_phone_candidate(text, &chars, i)
{
let digit_count = phone_str.chars().filter(|c| c.is_ascii_digit()).count();
if digit_count >= 7 {
let mut normalized = None;
if let Some(c) = country {
let mut cleaned = phone_str.clone();
remove_non_digit_character(&mut cleaned);
leading_zero_remover(&mut cleaned);
let with_country = format!("+{}{}", c.prefix, cleaned);
normalized = normalize_phone_number(&with_country);
}
if normalized.is_none() {
normalized = normalize_phone_number(&phone_str);
}
let is_valid = normalized.is_some();
results.push(ExtractedPhoneNumber {
raw: phone_str,
normalized,
start: start_byte,
end: end_byte,
is_valid,
});
i = char_index_from_byte(text, end_byte);
continue;
}
}
}
i += 1;
}
results
}
pub fn count_phone_numbers_in_text(text: &str) -> usize {
extract_phone_numbers_from_text(text).len()
}
pub fn replace_phone_numbers_in_text<F>(text: &str, replacement: F) -> String
where
F: Fn(&ExtractedPhoneNumber) -> String,
{
let numbers = extract_phone_numbers_from_text(text);
if numbers.is_empty() {
return text.to_string();
}
let mut result = String::with_capacity(text.len());
let mut last_end = 0;
for number in &numbers {
result.push_str(&text[last_end..number.start]);
result.push_str(&replacement(number));
last_end = number.end;
}
result.push_str(&text[last_end..]);
result
}
pub fn redact_phone_numbers(text: &str, visible_digits: usize) -> String {
replace_phone_numbers_in_text(text, |number| {
let digits: Vec<char> = number.raw.chars().filter(|c| c.is_ascii_digit()).collect();
let total = digits.len();
if visible_digits == 0 || visible_digits >= total {
return "[PHONE]".to_string();
}
let hidden_count = total - visible_digits;
let mut result = String::new();
for _ in 0..hidden_count {
result.push('*');
}
for &d in &digits[hidden_count..] {
result.push(d);
}
result
})
}
fn is_phone_number_start(chars: &[char], pos: usize) -> bool {
if pos >= chars.len() {
return false;
}
let c = chars[pos];
if c == '+' {
return pos + 1 < chars.len() && chars[pos + 1].is_ascii_digit();
}
if c == '(' {
return pos + 1 < chars.len() && chars[pos + 1].is_ascii_digit();
}
if c.is_ascii_digit() {
if pos > 0 {
let prev = chars[pos - 1];
if prev.is_alphanumeric() && prev != ' ' && prev != '\n' && prev != '\t' {
return false;
}
}
return true;
}
false
}
fn extract_phone_candidate(
text: &str,
chars: &[char],
start_pos: usize,
) -> Option<(String, usize, usize)> {
let mut end_pos = start_pos;
let mut digit_count = 0;
let mut last_digit_pos = start_pos;
let mut paren_depth = 0;
while end_pos < chars.len() {
let c = chars[end_pos];
match c {
'+' if end_pos == start_pos => {
end_pos += 1;
}
'0'..='9' => {
digit_count += 1;
last_digit_pos = end_pos;
end_pos += 1;
}
'(' => {
paren_depth += 1;
end_pos += 1;
}
')' if paren_depth > 0 => {
paren_depth -= 1;
end_pos += 1;
}
'-' | '.' | ' ' => {
if digit_count > 0
&& end_pos + 1 < chars.len()
&& (chars[end_pos + 1].is_ascii_digit() || chars[end_pos + 1] == '(')
{
end_pos += 1;
} else {
break;
}
}
_ => break,
}
if digit_count > 15 {
break;
}
}
if digit_count < 7 {
return None;
}
let start_byte = byte_index_from_char(text, start_pos);
let end_byte = byte_index_from_char(text, last_digit_pos + 1);
let phone_str = text[start_byte..end_byte].to_string();
Some((phone_str, start_byte, end_byte))
}
fn byte_index_from_char(text: &str, char_index: usize) -> usize {
text.char_indices()
.nth(char_index)
.map(|(i, _)| i)
.unwrap_or(text.len())
}
fn char_index_from_byte(text: &str, byte_index: usize) -> usize {
text[..byte_index].chars().count()
}
#[derive(Debug, Clone)]
pub struct PhoneNumber {
pub original: String,
pub normalized: String,
pub country: Option<&'static Country>,
pub phone_type: Option<PhoneNumberType>,
}
impl PhoneNumber {
pub fn parse(input: &str) -> Option<Self> {
let (normalized, country, phone_type) = normalize_and_extract(input)?;
Some(PhoneNumber {
original: input.to_string(),
normalized,
country: Some(country),
phone_type: Some(phone_type),
})
}
pub fn parse_with_country(input: &str, country_code: &str) -> Option<Self> {
let country = COUNTRIES.iter().find(|c| c.code == country_code);
let without_ext = strip_extension(input);
let processed = convert_vanity_letters(without_ext);
if let Some(phone) = Self::parse(&processed) {
let resolved_country = if let (Some(hint), Some(parsed)) = (country, phone.country) {
if hint.prefix == parsed.prefix && hint.code != parsed.code {
Some(hint)
} else {
phone.country
}
} else {
phone.country
};
if processed.trim_start().starts_with('+') {
return Some(PhoneNumber {
original: input.to_string(),
country: resolved_country,
..phone
});
}
if country.map_or(true, |hint| {
resolved_country.map(|c| c.code) == Some(hint.code)
}) {
return Some(PhoneNumber {
original: input.to_string(),
country: resolved_country,
..phone
});
}
}
let country = country?;
let mut cleaned = processed.to_string();
remove_non_digit_character(&mut cleaned);
for idd in &["0011", "011", "00"] {
if cleaned.starts_with(idd) && cleaned.len() > idd.len() + 5 {
let remaining = &cleaned[idd.len()..];
let with_plus = format!("+{}", remaining);
if let Some(mut phone) = Self::parse(&with_plus) {
phone.original = input.to_string();
return Some(phone);
}
}
}
if cleaned.starts_with('0') {
let without_trunk = cleaned.trim_start_matches('0');
let with_country = format!("+{}{}", country.prefix, without_trunk);
if let Some(mut phone) = Self::parse(&with_country) {
phone.original = input.to_string();
phone.country = Some(country);
return Some(phone);
}
}
let with_country = format!("+{}{}", country.prefix, cleaned);
Self::parse(&with_country).map(|mut phone| {
phone.original = input.to_string();
phone.country = Some(country);
phone
})
}
pub fn e164(&self) -> &str {
&self.normalized
}
pub fn national_number(&self) -> String {
if let Some(country) = self.country {
let prefix_len = count_digits(country.prefix) + 1; self.normalized[prefix_len..].to_string()
} else {
self.normalized.clone()
}
}
pub fn country_code(&self) -> Option<u32> {
self.country.map(|c| c.prefix)
}
pub fn format(&self, fmt: PhoneFormat) -> String {
match fmt {
PhoneFormat::E164 => self.normalized.clone(),
_ => {
if let Some(country) = self.country {
let plen = prefix_digit_count(country.prefix);
let national = &self.normalized[1 + plen..];
match fmt {
PhoneFormat::International => {
let formatted = format_national_number(national, country);
let mut result = String::with_capacity(2 + plen + formatted.len());
result.push('+');
push_prefix_digits(&mut result, country.prefix);
result.push(' ');
result.push_str(&formatted);
result
}
PhoneFormat::National => format_national_number(national, country),
PhoneFormat::RFC3966 => {
let grouped = format_national_number_rfc3966(national, country);
let mut result = String::with_capacity(6 + plen + grouped.len());
result.push_str("tel:+");
push_prefix_digits(&mut result, country.prefix);
result.push('-');
result.push_str(&grouped);
result
}
_ => unreachable!(),
}
} else {
self.normalized.clone()
}
}
}
}
pub fn is_mobile(&self) -> bool {
self.phone_type == Some(PhoneNumberType::Mobile)
}
pub fn is_landline(&self) -> bool {
self.phone_type == Some(PhoneNumberType::FixedLine)
}
pub fn is_toll_free(&self) -> bool {
self.phone_type == Some(PhoneNumberType::TollFree)
}
}
impl PartialEq for PhoneNumber {
fn eq(&self, other: &Self) -> bool {
self.normalized == other.normalized
}
}
impl Eq for PhoneNumber {}
impl std::hash::Hash for PhoneNumber {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.normalized.hash(state);
}
}
impl std::fmt::Display for PhoneNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.normalized)
}
}
impl std::str::FromStr for PhoneNumber {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
PhoneNumber::parse(s).ok_or("Invalid phone number")
}
}
#[derive(Debug, Clone, Default)]
pub struct PhoneNumberSet {
numbers: std::collections::HashMap<String, PhoneNumber>,
}
impl PhoneNumberSet {
pub fn new() -> Self {
PhoneNumberSet {
numbers: std::collections::HashMap::new(),
}
}
pub fn add(&mut self, phone_number: &str) -> bool {
if let Some(phone) = PhoneNumber::parse(phone_number) {
if !self.numbers.contains_key(&phone.normalized) {
self.numbers.insert(phone.normalized.clone(), phone);
return true;
}
}
false
}
pub fn contains(&self, phone_number: &str) -> bool {
if let Some(normalized) = normalize_phone_number(phone_number) {
self.numbers.contains_key(&normalized)
} else {
false
}
}
pub fn len(&self) -> usize {
self.numbers.len()
}
pub fn is_empty(&self) -> bool {
self.numbers.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &PhoneNumber> {
self.numbers.values()
}
pub fn normalized_numbers(&self) -> Vec<&str> {
self.numbers.keys().map(|s| s.as_str()).collect()
}
pub fn remove(&mut self, phone_number: &str) -> bool {
if let Some(normalized) = normalize_phone_number(phone_number) {
self.numbers.remove(&normalized).is_some()
} else {
false
}
}
pub fn find_duplicates(&self, phone_number: &str) -> Option<&PhoneNumber> {
let normalized = normalize_phone_number(phone_number)?;
self.numbers.get(&normalized)
}
}
impl FromIterator<String> for PhoneNumberSet {
fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
let mut set = PhoneNumberSet::new();
for number in iter {
set.add(&number);
}
set
}
}
impl<'a> FromIterator<&'a str> for PhoneNumberSet {
fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> Self {
let mut set = PhoneNumberSet::new();
for number in iter {
set.add(number);
}
set
}
}