Rust 练习册 :Phone Number与电话号码处理
电话号码处理是现代软件开发中常见的需求,特别是在通讯、社交、电商等应用中。在 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开头
让我们先看看练习提供的函数签名:
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. 基础实现
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 => {// 检查第一位是否为1if 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. 优化实现
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位号码,第一位必须是1if 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. 使用正则表达式的实现
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')
}
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
#[test]
fn test_cleans_the_number() {process_clean_case("(223) 456-7890", Some("2234567890"));
}
应该清理括号、空格和连字符等字符。
#[test]
fn test_cleans_numbers_with_dots() {process_clean_case("223.456.7890", Some("2234567890"));
}
应该清理点号等分隔符。
#[test]
fn test_cleans_numbers_with_multiple_spaces() {process_clean_case("223 456 7890 ", Some("2234567890"));
}
应该清理多余的空格。
#[test]
fn test_invalid_when_9_digits() {process_clean_case("123456789", None);
}
9位数字是无效的。
#[test]
fn test_invalid_when_11_digits_does_not_start_with_a_1() {process_clean_case("22234567890", None);
}
11位数字但不以1开头是无效的。
#[test]
fn test_valid_when_11_digits_and_starting_with_1() {process_clean_case("12234567890", Some("2234567890"));
}
11位数字且以1开头是有效的,应移除前导1。
#[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开头是有效的。
#[test]
fn test_invalid_when_more_than_11_digits() {process_clean_case("321234567890", None);
}
超过11位数字是无效的。
#[test]
fn test_invalid_with_letters() {process_clean_case("123-abc-7890", None);
}
包含字母是无效的。
#[test]
fn test_invalid_with_punctuations() {process_clean_case("123-@:!-7890", None);
}
包含特殊标点符号是无效的。
#[test]
fn test_invalid_if_area_code_starts_with_1_on_valid_11digit_number() {process_clean_case("1 (123) 456-7890", None);
}
地区代码以1开头是无效的。
#[test]
fn test_invalid_if_area_code_starts_with_0_on_valid_11digit_number() {process_clean_case("1 (023) 456-7890", None);
}
地区代码以0开头是无效的。
#[test]
fn test_invalid_if_area_code_starts_with_1() {process_clean_case("(123) 456-7890", None);
}
地区代码以1开头是无效的。
#[test]
fn test_invalid_if_exchange_code_starts_with_1() {process_clean_case("(223) 156-7890", None);
}
交换代码以1开头是无效的。
#[test]
fn test_invalid_if_exchange_code_starts_with_0() {process_clean_case("(223) 056-7890", None);
}
交换代码以0开头是无效的。
#[test]
fn test_invalid_if_exchange_code_starts_with_1_on_valid_11digit_number() {process_clean_case("1 (223) 156-7890", None);
}
交换代码以1开头是无效的。
#[test]
fn test_invalid_if_exchange_code_starts_with_0_on_valid_11digit_number() {process_clean_case("1 (223) 056-7890", None);
}
交换代码以0开头是无效的。
#[test]
fn test_invalid_if_area_code_starts_with_0() {process_clean_case("(023) 456-7890", None);
}
地区代码以0开头是无效的。
性能优化版本
考虑性能的优化实现:
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或1if bytes[0] == b'0' || bytes[0] == b'1' {return None;}// 检查交换代码(第4-6位)首位不能是0或1if 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)
}
错误处理和边界情况
考虑更多边界情况的实现:
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())
}
扩展功能
基于基础实现,我们可以添加更多功能:
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)
- 需要存储提取的数字字符
与其他实现方式的比较
// 使用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的魅力所在。
