电话号码处理是现代软件开发中常见的需求,特别是在通讯、社交、电商等应用中。在 Exercism 的 "phone-number" 练习中,我们需要实现一个函数来清理和验证北美电话号码(NANP - North American Numbering Plan)。这不仅能帮助我们掌握字符串处理和正则表达式技巧,还能深入学习Rust中的错误处理和数据验证。
什么是北美电话号码?
北美电话号码遵循北美编号计划(NANP),格式为:(NXX) NXX-XXXX,其中:
- 第一部分(3位)是地区代码(Area Code)
- 第二部分(3位)是交换代码(Exchange Code)
- 第三部分(4位)是号码(Number)
有效的NANP电话号码需要满足以下条件:
- 总共10位数字(不包括国家代码)
- 如果有11位数字,第一位必须是1(美国国家代码)
- 地区代码不能以0或1开头
- 交换代码不能以0或1开头
让我们先看看练习提供的函数签名:
rust
pub fn number(user_number: &str) -> Option<String> {
unimplemented!(
"Given the number entered by user '{}', convert it into SMS-friendly format. If the entered number is not a valid NANP number, return None.",
user_number
);
}
我们需要实现number函数,将用户输入的电话号码转换为标准格式,如果输入无效则返回None。
设计分析
1. 核心要求
- 数据清理:从用户输入中提取数字,去除所有非数字字符
- 格式验证:验证电话号码是否符合NANP标准
- 长度检查:检查电话号码长度是否正确
- 数字验证:验证地区代码和交换代码的首位不能是0或1
2. 技术要点
- 字符串处理:高效处理和过滤字符串中的字符
- 正则表达式:使用正则表达式进行模式匹配和提取
- 错误处理:使用Option类型处理无效输入
- 数据验证:实现复杂的业务规则验证
完整实现
1. 基础实现
rust
pub fn number(user_number: &str) -> Option<String> {
// 提取所有数字字符
let digits: String = user_number.chars().filter(|c| c.is_ascii_digit()).collect();
// 根据数字长度进行处理
match digits.len() {
10 => {
// 检查地区代码和交换代码
if is_valid_area_code(&digits[0..3]) && is_valid_exchange_code(&digits[3..6]) {
Some(digits)
} else {
None
}
}
11 => {
// 检查第一位是否为1
if digits.starts_with('1') {
let number_part = &digits[1..];
if is_valid_area_code(&number_part[0..3]) && is_valid_exchange_code(&number_part[3..6]) {
Some(number_part.to_string())
} else {
None
}
} else {
None
}
}
_ => None,
}
}
fn is_valid_area_code(area_code: &str) -> bool {
// 地区代码不能以0或1开头
!area_code.starts_with('0') && !area_code.starts_with('1')
}
fn is_valid_exchange_code(exchange_code: &str) -> bool {
// 交换代码不能以0或1开头
!exchange_code.starts_with('0') && !exchange_code.starts_with('1')
}
2. 优化实现
rust
pub fn number(user_number: &str) -> Option<String> {
// 提取所有数字字符
let digits: String = user_number.chars().filter(|c| c.is_ascii_digit()).collect();
// 验证并处理电话号码
validate_and_format(digits)
}
fn validate_and_format(digits: String) -> Option<String> {
match digits.len() {
10 => {
// 直接验证10位号码
validate_ten_digit_number(&digits)
}
11 => {
// 验证11位号码,第一位必须是1
if digits.starts_with('1') {
validate_ten_digit_number(&digits[1..])
} else {
None
}
}
_ => None,
}
}
fn validate_ten_digit_number(digits: &str) -> Option<String> {
// 检查地区代码(前3位)和交换代码(第4-6位)
let area_code = &digits[0..3];
let exchange_code = &digits[3..6];
if is_valid_area_code(area_code) && is_valid_exchange_code(exchange_code) {
Some(digits.to_string())
} else {
None
}
}
fn is_valid_area_code(area_code: &str) -> bool {
// 地区代码不能以0或1开头
!area_code.starts_with('0') && !area_code.starts_with('1')
}
fn is_valid_exchange_code(exchange_code: &str) -> bool {
// 交换代码不能以0或1开头
!exchange_code.starts_with('0') && !exchange_code.starts_with('1')
}
3. 使用正则表达式的实现
rust
pub fn number(user_number: &str) -> Option<String> {
use regex::Regex;
// 移除所有非数字字符
let re = Regex::new(r"\D").unwrap();
let digits = re.replace_all(user_number, "").to_string();
// 验证并格式化电话号码
validate_and_format(digits)
}
fn validate_and_format(digits: String) -> Option<String> {
match digits.len() {
10 => {
validate_ten_digit_number(&digits)
}
11 => {
if digits.starts_with('1') {
validate_ten_digit_number(&digits[1..])
} else {
None
}
}
_ => None,
}
}
fn validate_ten_digit_number(digits: &str) -> Option<String> {
let area_code = &digits[0..3];
let exchange_code = &digits[3..6];
if is_valid_area_code(area_code) && is_valid_exchange_code(exchange_code) {
Some(digits.to_string())
} else {
None
}
}
fn is_valid_area_code(area_code: &str) -> bool {
!area_code.starts_with('0') && !area_code.starts_with('1')
}
fn is_valid_exchange_code(exchange_code: &str) -> bool {
!exchange_code.starts_with('0') && !exchange_code.starts_with('1')
}
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
rust
#[test]
fn test_cleans_the_number() {
process_clean_case("(223) 456-7890", Some("2234567890"));
}
应该清理括号、空格和连字符等字符。
rust
#[test]
fn test_cleans_numbers_with_dots() {
process_clean_case("223.456.7890", Some("2234567890"));
}
应该清理点号等分隔符。
rust
#[test]
fn test_cleans_numbers_with_multiple_spaces() {
process_clean_case("223 456 7890 ", Some("2234567890"));
}
应该清理多余的空格。
rust
#[test]
fn test_invalid_when_9_digits() {
process_clean_case("123456789", None);
}
9位数字是无效的。
rust
#[test]
fn test_invalid_when_11_digits_does_not_start_with_a_1() {
process_clean_case("22234567890", None);
}
11位数字但不以1开头是无效的。
rust
#[test]
fn test_valid_when_11_digits_and_starting_with_1() {
process_clean_case("12234567890", Some("2234567890"));
}
11位数字且以1开头是有效的,应移除前导1。
rust
#[test]
fn test_valid_when_11_digits_and_starting_with_1_even_with_punctuation() {
process_clean_case("+1 (223) 456-7890", Some("2234567890"));
}
带有标点符号的11位数字且以+1开头是有效的。
rust
#[test]
fn test_invalid_when_more_than_11_digits() {
process_clean_case("321234567890", None);
}
超过11位数字是无效的。
rust
#[test]
fn test_invalid_with_letters() {
process_clean_case("123-abc-7890", None);
}
包含字母是无效的。
rust
#[test]
fn test_invalid_with_punctuations() {
process_clean_case("123-@:!-7890", None);
}
包含特殊标点符号是无效的。
rust
#[test]
fn test_invalid_if_area_code_starts_with_1_on_valid_11digit_number() {
process_clean_case("1 (123) 456-7890", None);
}
地区代码以1开头是无效的。
rust
#[test]
fn test_invalid_if_area_code_starts_with_0_on_valid_11digit_number() {
process_clean_case("1 (023) 456-7890", None);
}
地区代码以0开头是无效的。
rust
#[test]
fn test_invalid_if_area_code_starts_with_1() {
process_clean_case("(123) 456-7890", None);
}
地区代码以1开头是无效的。
rust
#[test]
fn test_invalid_if_exchange_code_starts_with_1() {
process_clean_case("(223) 156-7890", None);
}
交换代码以1开头是无效的。
rust
#[test]
fn test_invalid_if_exchange_code_starts_with_0() {
process_clean_case("(223) 056-7890", None);
}
交换代码以0开头是无效的。
rust
#[test]
fn test_invalid_if_exchange_code_starts_with_1_on_valid_11digit_number() {
process_clean_case("1 (223) 156-7890", None);
}
交换代码以1开头是无效的。
rust
#[test]
fn test_invalid_if_exchange_code_starts_with_0_on_valid_11digit_number() {
process_clean_case("1 (223) 056-7890", None);
}
交换代码以0开头是无效的。
rust
#[test]
fn test_invalid_if_area_code_starts_with_0() {
process_clean_case("(023) 456-7890", None);
}
地区代码以0开头是无效的。
性能优化版本
考虑性能的优化实现:
rust
pub fn number(user_number: &str) -> Option<String> {
// 预分配字符串容量以避免重新分配
let mut digits = String::with_capacity(11);
// 手动迭代字符以提高性能
for c in user_number.chars() {
if c.is_ascii_digit() {
digits.push(c);
}
}
// 验证并格式化电话号码
validate_and_format_optimized(digits)
}
fn validate_and_format_optimized(digits: String) -> Option<String> {
match digits.len() {
10 => {
validate_ten_digit_number_optimized(&digits)
}
11 => {
// 检查第一位是否为1(使用索引而不是starts_with以提高性能)
if unsafe { digits.as_bytes().get_unchecked(0) == &b'1' } {
validate_ten_digit_number_optimized(&digits[1..])
} else {
None
}
}
_ => None,
}
}
fn validate_ten_digit_number_optimized(digits: &str) -> Option<String> {
// 使用字节比较以提高性能
let bytes = digits.as_bytes();
// 检查地区代码(前3位)首位不能是0或1
if bytes[0] == b'0' || bytes[0] == b'1' {
return None;
}
// 检查交换代码(第4-6位)首位不能是0或1
if bytes[3] == b'0' || bytes[3] == b'1' {
return None;
}
Some(digits.to_string())
}
// 使用预编译正则表达式的版本
use regex::Regex;
use std::sync::OnceLock;
fn get_digit_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"\D").unwrap())
}
pub fn number_with_regex(user_number: &str) -> Option<String> {
let re = get_digit_regex();
let digits = re.replace_all(user_number, "").to_string();
validate_and_format_optimized(digits)
}
错误处理和边界情况
考虑更多边界情况的实现:
rust
pub fn number(user_number: &str) -> Option<String> {
// 处理空字符串
if user_number.is_empty() {
return None;
}
// 提取所有数字字符
let digits: String = user_number.chars().filter(|c| c.is_ascii_digit()).collect();
// 处理没有数字的情况
if digits.is_empty() {
return None;
}
// 验证并格式化电话号码
validate_and_format_with_error_handling(digits)
}
fn validate_and_format_with_error_handling(digits: String) -> Option<String> {
match digits.len() {
10 => {
validate_ten_digit_number_with_error_handling(&digits)
}
11 => {
if digits.starts_with('1') {
validate_ten_digit_number_with_error_handling(&digits[1..])
} else {
None
}
}
_ => None,
}
}
fn validate_ten_digit_number_with_error_handling(digits: &str) -> Option<String> {
let area_code = &digits[0..3];
let exchange_code = &digits[3..6];
// 更详细的验证
if !is_valid_area_code_detailed(area_code) {
return None;
}
if !is_valid_exchange_code_detailed(exchange_code) {
return None;
}
Some(digits.to_string())
}
fn is_valid_area_code_detailed(area_code: &str) -> bool {
// 地区代码不能以0或1开头
if area_code.starts_with('0') || area_code.starts_with('1') {
return false;
}
// 额外的业务规则可以在这里添加
true
}
fn is_valid_exchange_code_detailed(exchange_code: &str) -> bool {
// 交换代码不能以0或1开头
if exchange_code.starts_with('0') || exchange_code.starts_with('1') {
return false;
}
// 额外的业务规则可以在这里添加
true
}
// 返回详细错误信息的版本
#[derive(Debug, PartialEq)]
pub enum PhoneNumberError {
InvalidLength,
InvalidCountryCode,
InvalidAreaCode,
InvalidExchangeCode,
NoDigits,
}
impl std::fmt::Display for PhoneNumberError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
PhoneNumberError::InvalidLength => write!(f, "电话号码长度无效"),
PhoneNumberError::InvalidCountryCode => write!(f, "国家代码无效"),
PhoneNumberError::InvalidAreaCode => write!(f, "地区代码无效"),
PhoneNumberError::InvalidExchangeCode => write!(f, "交换代码无效"),
PhoneNumberError::NoDigits => write!(f, "未找到数字"),
}
}
}
impl std::error::Error for PhoneNumberError {}
pub fn number_detailed(user_number: &str) -> Result<String, PhoneNumberError> {
// 处理空字符串
if user_number.is_empty() {
return Err(PhoneNumberError::NoDigits);
}
// 提取所有数字字符
let digits: String = user_number.chars().filter(|c| c.is_ascii_digit()).collect();
// 处理没有数字的情况
if digits.is_empty() {
return Err(PhoneNumberError::NoDigits);
}
// 验证并格式化电话号码
validate_and_format_detailed(digits)
}
fn validate_and_format_detailed(digits: String) -> Result<String, PhoneNumberError> {
match digits.len() {
10 => {
validate_ten_digit_number_detailed(&digits)
}
11 => {
if digits.starts_with('1') {
validate_ten_digit_number_detailed(&digits[1..])
} else {
Err(PhoneNumberError::InvalidCountryCode)
}
}
_ => Err(PhoneNumberError::InvalidLength),
}
}
fn validate_ten_digit_number_detailed(digits: &str) -> Result<String, PhoneNumberError> {
let area_code = &digits[0..3];
let exchange_code = &digits[3..6];
if area_code.starts_with('0') || area_code.starts_with('1') {
return Err(PhoneNumberError::InvalidAreaCode);
}
if exchange_code.starts_with('0') || exchange_code.starts_with('1') {
return Err(PhoneNumberError::InvalidExchangeCode);
}
Ok(digits.to_string())
}
扩展功能
基于基础实现,我们可以添加更多功能:
rust
pub struct PhoneNumber {
digits: String,
}
impl PhoneNumber {
pub fn new(user_number: &str) -> Option<Self> {
number(user_number).map(|digits| PhoneNumber { digits })
}
pub fn new_unchecked(digits: String) -> Self {
PhoneNumber { digits }
}
pub fn area_code(&self) -> &str {
&self.digits[0..3]
}
pub fn exchange_code(&self) -> &str {
&self.digits[3..6]
}
pub fn number(&self) -> &str {
&self.digits[6..]
}
pub fn full_number(&self) -> &str {
&self.digits
}
pub fn to_formatted_string(&self) -> String {
format!("({}) {}-{}",
self.area_code(),
self.exchange_code(),
self.number())
}
pub fn is_valid(&self) -> bool {
self.digits.len() == 10 &&
is_valid_area_code(self.area_code()) &&
is_valid_exchange_code(self.exchange_code())
}
}
pub fn number(user_number: &str) -> Option<String> {
let digits: String = user_number.chars().filter(|c| c.is_ascii_digit()).collect();
validate_and_format(digits)
}
fn validate_and_format(digits: String) -> Option<String> {
match digits.len() {
10 => {
validate_ten_digit_number(&digits)
}
11 => {
if digits.starts_with('1') {
validate_ten_digit_number(&digits[1..])
} else {
None
}
}
_ => None,
}
}
fn validate_ten_digit_number(digits: &str) -> Option<String> {
let area_code = &digits[0..3];
let exchange_code = &digits[3..6];
if is_valid_area_code(area_code) && is_valid_exchange_code(exchange_code) {
Some(digits.to_string())
} else {
None
}
}
fn is_valid_area_code(area_code: &str) -> bool {
!area_code.starts_with('0') && !area_code.starts_with('1')
}
fn is_valid_exchange_code(exchange_code: &str) -> bool {
!exchange_code.starts_with('0') && !exchange_code.starts_with('1')
}
// 电话号码验证器
pub struct PhoneNumberValidator;
impl PhoneNumberValidator {
pub fn new() -> Self {
PhoneNumberValidator
}
pub fn validate(&self, user_number: &str) -> Option<PhoneNumber> {
PhoneNumber::new(user_number)
}
pub fn is_valid(&self, user_number: &str) -> bool {
self.validate(user_number).is_some()
}
// 批量验证电话号码
pub fn validate_batch(&self, numbers: &[&str]) -> Vec<(String, bool)> {
numbers
.iter()
.map(|&number| {
let is_valid = self.is_valid(number);
(number.to_string(), is_valid)
})
.collect()
}
// 查找有效的电话号码
pub fn find_valid_numbers(&self, numbers: &[&str]) -> Vec<String> {
numbers
.iter()
.filter_map(|&number| self.validate(number))
.map(|phone| phone.full_number().to_string())
.collect()
}
// 格式化电话号码(如果有效)
pub fn format_if_valid(&self, user_number: &str) -> Option<String> {
self.validate(user_number)
.map(|phone| phone.to_formatted_string())
}
}
// 电话号码分析器
pub struct PhoneNumberAnalysis {
pub original_input: String,
pub cleaned_number: Option<String>,
pub is_valid: bool,
pub area_code: Option<String>,
pub exchange_code: Option<String>,
pub number_part: Option<String>,
pub formatted_number: Option<String>,
}
impl PhoneNumberValidator {
pub fn analyze(&self, user_number: &str) -> PhoneNumberAnalysis {
let cleaned_number = number(user_number);
let (area_code, exchange_code, number_part, formatted_number) =
if let Some(ref phone) = cleaned_number {
let phone_obj = PhoneNumber::new_unchecked(phone.clone());
(
Some(phone_obj.area_code().to_string()),
Some(phone_obj.exchange_code().to_string()),
Some(phone_obj.number().to_string()),
Some(phone_obj.to_formatted_string()),
)
} else {
(None, None, None, None)
};
PhoneNumberAnalysis {
original_input: user_number.to_string(),
cleaned_number,
is_valid: cleaned_number.is_some(),
area_code,
exchange_code,
number_part,
formatted_number,
}
}
}
// 便利函数
pub fn format_phone_number(user_number: &str) -> Option<String> {
let validator = PhoneNumberValidator::new();
validator.format_if_valid(user_number)
}
pub fn is_valid_phone_number(user_number: &str) -> bool {
let validator = PhoneNumberValidator::new();
validator.is_valid(user_number)
}
实际应用场景
电话号码处理在实际开发中有以下应用:
- 通讯应用:电话、短信、视频通话应用
- 电商平台:用户注册、订单联系信息
- 社交网络:用户资料、好友联系
- 金融服务:银行、支付应用的用户验证
- 医疗健康:预约系统、患者联系
- 物流配送:快递、外卖的联系信息
- 企业管理系统:客户关系管理、员工信息
- 政府服务:公共服务、政务应用
算法复杂度分析
-
时间复杂度:O(n)
- 其中n是输入字符串的长度,需要遍历每个字符
-
空间复杂度:O(n)
- 需要存储提取的数字字符
与其他实现方式的比较
rust
// 使用nom解析器的实现
use nom::{
character::complete::{digit1, char},
combinator::{opt, map_res},
sequence::{delimited, tuple},
multi::many0,
bytes::complete::tag,
IResult,
};
pub fn number_nom(user_number: &str) -> Option<String> {
// 使用nom解析器库实现电话号码解析
// 这里只是一个示例,实际实现会更复杂
unimplemented!()
}
// 使用功能完整的电话号码库实现
// [dependencies]
// phonenumber = "0.3"
pub fn number_phonenumber_lib(user_number: &str) -> Option<String> {
use phonenumber::Mode;
match phonenumber::parse(None, user_number) {
Ok(phone_number) => {
if phonenumber::is_valid(&phone_number) {
Some(phonenumber::format(&phone_number, Mode::E164)[1..].to_string()) // 移除+号
} else {
None
}
}
Err(_) => None,
}
}
// 使用状态机的实现
#[derive(Debug, Clone, Copy)]
enum ParseState {
Start,
ReadingCountryCode,
ReadingAreaCode,
ReadingExchangeCode,
ReadingNumber,
Done,
Error,
}
pub fn number_state_machine(user_number: &str) -> Option<String> {
let mut state = ParseState::Start;
let mut digits = String::new();
for c in user_number.chars() {
match state {
ParseState::Start => {
if c.is_ascii_digit() {
digits.push(c);
if digits.len() == 1 && c == '1' {
state = ParseState::ReadingCountryCode;
} else {
state = ParseState::ReadingAreaCode;
}
}
// 忽略非数字字符
}
ParseState::ReadingCountryCode => {
if c.is_ascii_digit() {
digits.push(c);
state = ParseState::ReadingAreaCode;
}
}
ParseState::ReadingAreaCode => {
if c.is_ascii_digit() {
digits.push(c);
if digits.len() == 3 {
state = ParseState::ReadingExchangeCode;
}
}
}
ParseState::ReadingExchangeCode => {
if c.is_ascii_digit() {
digits.push(c);
if digits.len() == 6 {
state = ParseState::ReadingNumber;
}
}
}
ParseState::ReadingNumber => {
if c.is_ascii_digit() {
digits.push(c);
if digits.len() == 10 {
state = ParseState::Done;
}
}
}
ParseState::Done | ParseState::Error => {
if c.is_ascii_digit() {
// 超过10位数字
state = ParseState::Error;
}
}
}
}
if state == ParseState::Done || (state == ParseState::ReadingNumber && digits.len() == 10) {
Some(digits)
} else {
None
}
}
// 使用外部API验证的实现
// [dependencies]
// reqwest = "0.11"
// tokio = { version = "1", features = ["full"] }
pub async fn number_with_api_validation(user_number: &str) -> Option<String> {
let cleaned_number = number(user_number)?;
// 这里可以调用外部API验证电话号码是否真实存在
// let client = reqwest::Client::new();
// let response = client
// .post("https://api.phonenumberverification.com/validate")
// .json(&serde_json::json!({"number": cleaned_number}))
// .send()
// .await;
//
// if let Ok(resp) = response {
// if resp.status().is_success() {
// return Some(cleaned_number);
// }
// }
Some(cleaned_number) // 为示例直接返回
}
总结
通过 phone-number 练习,我们学到了:
- 字符串处理:掌握了从复杂字符串中提取和验证数据的技巧
- 正则表达式:学会了使用正则表达式进行模式匹配和数据提取
- 错误处理:深入理解了Option和Result类型在数据验证中的应用
- 业务规则实现:了解了如何将复杂的业务规则转换为代码实现
- 性能优化:学会了预分配内存和使用高效算法等优化技巧
- 数据封装:理解了如何设计结构体来封装和操作复杂数据
这些技能在实际开发中非常有用,特别是在数据处理、表单验证、用户输入处理等场景中。电话号码处理虽然是一个具体的应用问题,但它涉及到了字符串处理、正则表达式、错误处理、业务规则实现等许多核心概念,是学习Rust实用编程的良好起点。
通过这个练习,我们也看到了Rust在数据处理和验证方面的强大能力,以及如何用安全且高效的方式实现复杂的业务规则。这种结合了安全性和性能的语言特性正是Rust的魅力所在。