完整教程: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电话号码需要满足以下条件:

  1. 总共10位数字(不包括国家代码)
  2. 如果有11位数字,第一位必须是1(美国国家代码)
  3. 地区代码不能以0或1开头
  4. 交换代码不能以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. 核心要求

  1. 数据清理:从用户输入中提取数字,去除所有非数字字符
  2. 格式验证:验证电话号码是否符合NANP标准
  3. 长度检查:检查电话号码长度是否正确
  4. 数字验证:验证地区代码和交换代码的首位不能是0或1

2. 技术要点

  1. 字符串处理:高效处理和过滤字符串中的字符
  2. 正则表达式:使用正则表达式进行模式匹配和提取
  3. 错误处理:使用Option类型处理无效输入
  4. 数据验证:实现复杂的业务规则验证

完整实现

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 => {
  // 检查第一位是否为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. 优化实现

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. 使用正则表达式的实现

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或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)
          }

错误处理和边界情况

考虑更多边界情况的实现:

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)
                            }

实际应用场景

电话号码处理在实际开发中有以下应用:

  1. 通讯应用:电话、短信、视频通话应用
  2. 电商平台:用户注册、订单联系信息
  3. 社交网络:用户资料、好友联系
  4. 金融服务:银行、支付应用的用户验证
  5. 医疗健康:预约系统、患者联系
  6. 物流配送:快递、外卖的联系信息
  7. 企业管理系统:客户关系管理、员工信息
  8. 政府服务:公共服务、政务应用

算法复杂度分析

  1. 时间复杂度:O(n)

    • 其中n是输入字符串的长度,需要遍历每个字符
  2. 空间复杂度: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 练习,我们学到了:

  1. 字符串处理:掌握了从复杂字符串中提取和验证数据的技巧
  2. 正则表达式:学会了使用正则表达式进行模式匹配和数据提取
  3. 错误处理:深入理解了Option和Result类型在数据验证中的应用
  4. 业务规则实现:了解了如何将复杂的业务规则转换为代码实现
  5. 性能优化:学会了预分配内存和使用高效算法等优化技巧
  6. 数据封装:理解了如何设计结构体来封装和操作复杂数据

这些技能在实际开发中非常有用,特别是在数据处理、表单验证、用户输入处理等场景中。电话号码处理虽然是一个具体的应用问题,但它涉及到了字符串处理、正则表达式、错误处理、业务规则实现等许多核心概念,是学习Rust实用编程的良好起点。

通过这个练习,我们也看到了Rust在数据处理和验证方面的强大能力,以及如何用安全且高效的方式实现复杂的业务规则。这种结合了安全性和性能的语言特性正是Rust的魅力所在。

posted @ 2025-12-09 08:21  yangykaifa  阅读(13)  评论(0)    收藏  举报