在出版行业中,ISBN(International Standard Book Number,国际标准书号)是识别图书产品的唯一标识符。ISBN验证是确保书号格式正确的重要过程。在 Exercism 的 “isbn-verifier” 练习中,我们需要实现一个ISBN验证器,能够检查ISBN-10格式的书号是否有效。这不仅能帮助我们掌握字符串处理和验证算法,还能深入学习Rust中的字符处理、错误处理和数学计算技巧。

什么是ISBN验证?

ISBN(International Standard Book Number)是国际标准书号,用于唯一标识图书产品。ISBN-10是旧版ISBN格式,由10位数字组成,最后一位是校验码。

ISBN-10的验证规则如下:

  1. ISBN由10个字符组成(不包括连字符)
  2. 前9位必须是数字,第10位可以是数字或字母’X’(代表10)
  3. 验证公式:(d₁×10 + d₂×9 + d₃×8 + d₄×7 + d₅×6 + d₆×5 + d₇×4 + d₈×3 + d₉×2 + d₁₀×1) % 11 == 0

例如:

  • “3-598-21508-8” 验证过程:
    (3×10 + 5×9 + 9×8 + 8×7 + 2×6 + 1×5 + 5×4 + 0×3 + 8×2 + 8×1) % 11
    = (30 + 45 + 72 + 56 + 12 + 5 + 20 + 0 + 16 + 8) % 11
    = 264 % 11 = 0 ✓ 有效

让我们先看看练习提供的函数签名:

/// Determines whether the supplied string is a valid ISBN number
pub fn is_valid_isbn(isbn: &str) -> bool {
    unimplemented!("Is {:?} a valid ISBN number?", isbn);
}

我们需要实现一个函数,验证给定的字符串是否为有效的ISBN-10。

设计分析

1. 核心要求

  1. 格式检查:验证字符串是否符合ISBN-10格式
  2. 字符处理:处理数字和校验字符’X’
  3. 数学验证:执行ISBN验证算法
  4. 连字符处理:忽略连字符
  5. 错误处理:处理各种无效输入

2. 技术要点

  1. 字符串处理:字符过滤和转换
  2. 迭代器使用:高效处理字符序列
  3. 数学计算:执行模运算验证
  4. 边界情况:处理各种无效输入

完整实现

1. 基础实现

/// Determines whether the supplied string is a valid ISBN number
pub fn is_valid_isbn(isbn: &str) -> bool {
    // 移除连字符并转换为大写
    let clean_isbn: String = isbn.chars().filter(|c| *c != '-').collect();
    
    // 检查长度
    if clean_isbn.len() != 10 {
        return false;
    }
    
    // 检查字符有效性
    let mut total = 0;
    for (i, c) in clean_isbn.chars().enumerate() {
        let digit = match c {
            '0'..='9' => c.to_digit(10).unwrap(),
            'X' if i == 9 => 10, // 'X'只能在最后一位
            _ => return false,   // 无效字符
        };
        
        total += digit * (10 - i) as u32;
    }
    
    // 验证模11等于0
    total % 11 == 0
}

2. 使用函数式编程的实现

/// Determines whether the supplied string is a valid ISBN number
pub fn is_valid_isbn(isbn: &str) -> bool {
    // 移除连字符
    let clean_isbn: String = isbn.chars().filter(|&c| c != '-').collect();
    
    // 检查长度
    if clean_isbn.len() != 10 {
        return false;
    }
    
    // 检查每个字符并计算总和
    let chars: Vec<char> = clean_isbn.chars().collect();
    
    // 检查第一位到第九位是否都是数字
    if !chars[0..9].iter().all(|c| c.is_ascii_digit()) {
        return false;
    }
    
    // 检查第十位是否有效
    if !chars[9].is_ascii_digit() && chars[9] != 'X' {
        return false;
    }
    
    // 计算验证和
    let sum: u32 = chars
        .iter()
        .enumerate()
        .map(|(i, &c)| {
            let digit = if c == 'X' { 10 } else { c.to_digit(10).unwrap() };
            digit * (10 - i) as u32
        })
        .sum();
    
    // 验证模11等于0
    sum % 11 == 0
}

3. 优化实现

/// Determines whether the supplied string is a valid ISBN number
pub fn is_valid_isbn(isbn: &str) -> bool {
    let mut total = 0;
    let mut position = 0;
    
    for c in isbn.chars() {
        if c == '-' {
            continue; // 跳过连字符
        }
        
        let digit = match c {
            '0'..='9' => c.to_digit(10).unwrap(),
            'X' if position == 9 => 10, // 'X'只能在最后一位
            _ => return false, // 无效字符
        };
        
        if position >= 10 {
            return false; // 太多数字
        }
        
        total += digit * (10 - position);
        position += 1;
    }
    
    // 必须正好10位数字
    if position != 10 {
        return false;
    }
    
    // 验证模11等于0
    total % 11 == 0
}

测试用例分析

通过查看测试用例,我们可以更好地理解需求:

#[test]
fn test_valid() {
    assert!(is_valid_isbn("3-598-21508-8"));
}

有效的ISBN应该返回true。

#[test]
fn test_invalid_check_digit() {
    assert!(!is_valid_isbn("3-598-21508-9"));
}

校验位错误的ISBN应该返回false。

#[test]
fn test_valid_check_digit_of_10() {
    assert!(is_valid_isbn("3-598-21507-X"));
}

校验位为’X’(代表10)的有效ISBN应该返回true。

#[test]
fn test_invalid_character_as_check_digit() {
    assert!(!is_valid_isbn("3-598-21507-A"));
}

校验位为无效字符的ISBN应该返回false。

#[test]
fn test_invalid_character_in_isbn() {
    assert!(!is_valid_isbn("3-598-P1581-X"));
}

ISBN中包含无效字符应该返回false。

#[test]
fn test_valid_isbn_without_dashes() {
    assert!(is_valid_isbn("3598215088"));
}

没有连字符的有效ISBN应该返回true。

#[test]
fn test_invalid_isbn_without_dashes_and_too_long() {
    assert!(!is_valid_isbn("3598215078X"));
}

太长的ISBN应该返回false。

#[test]
fn empty_isbn() {
    assert!(!is_valid_isbn(""));
}

空字符串应该返回false。

性能优化版本

考虑性能的优化实现:

/// Determines whether the supplied string is a valid ISBN number
pub fn is_valid_isbn(isbn: &str) -> bool {
    let mut total = 0u32;
    let mut position = 0;
    
    // 使用字节处理ASCII字符串以提高性能
    for &byte in isbn.as_bytes() {
        match byte {
            b'-' => continue, // 跳过连字符
            b'0'..=b'9' => {
                if position >= 10 {
                    return false; // 太多数字
                }
                let digit = (byte - b'0') as u32;
                total += digit * (10 - position) as u32;
                position += 1;
            }
            b'X' => {
                if position != 9 {
                    return false; // 'X'只能在最后一位
                }
                total += 10 * (10 - position) as u32;
                position += 1;
            }
            _ => return false, // 无效字符
        }
    }
    
    // 必须正好10位数字
    position == 10 && total % 11 == 0
}

// 使用预分配向量的版本
pub fn is_valid_isbn_precise(isbn: &str) -> bool {
    // 预分配向量以避免重新分配
    let mut digits = Vec::with_capacity(10);
    
    for c in isbn.chars() {
        if c != '-' {
            match c {
                '0'..='9' => digits.push(c.to_digit(10).unwrap()),
                'X' => {
                    if digits.len() == 9 {
                        digits.push(10);
                    } else {
                        return false; // 'X'只能在最后一位
                    }
                }
                _ => return false, // 无效字符
            }
        }
    }
    
    // 必须正好10位数字
    if digits.len() != 10 {
        return false;
    }
    
    // 计算验证和
    let sum: u32 = digits
        .iter()
        .enumerate()
        .map(|(i, &digit)| digit * (10 - i) as u32)
        .sum();
    
    sum % 11 == 0
}

错误处理和边界情况

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

#[derive(Debug, PartialEq)]
pub enum IsbnError {
    InvalidLength,
    InvalidCharacter(char),
    InvalidCheckDigit,
    MissingCheckDigit,
}

/// 验证ISBN并返回详细错误信息
pub fn validate_isbn_detailed(isbn: &str) -> Result<bool, IsbnError> {
    let clean_isbn: String = isbn.chars().filter(|&c| c != '-').collect();
    
    // 检查长度
    if clean_isbn.len() != 10 {
        return Err(IsbnError::InvalidLength);
    }
    
    let chars: Vec<char> = clean_isbn.chars().collect();
    
    // 检查前9位是否都是数字
    for (i, &c) in chars[0..9].iter().enumerate() {
        if !c.is_ascii_digit() {
            return Err(IsbnError::InvalidCharacter(c));
        }
    }
    
    // 检查最后一位
    let last_char = chars[9];
    if !last_char.is_ascii_digit() && last_char != 'X' {
        return Err(IsbnError::InvalidCheckDigit);
    }
    
    // 计算验证和
    let sum: u32 = chars
        .iter()
        .enumerate()
        .map(|(i, &c)| {
            let digit = if c == 'X' { 10 } else { c.to_digit(10).unwrap() };
            digit * (10 - i) as u32
        })
        .sum();
    
    if sum % 11 == 0 {
        Ok(true)
    } else {
        Err(IsbnError::InvalidCheckDigit)
    }
}

/// Determines whether the supplied string is a valid ISBN number
pub fn is_valid_isbn(isbn: &str) -> bool {
    validate_isbn_detailed(isbn).unwrap_or(false)
}

// 支持ISBN-13的版本
pub fn is_valid_isbn_any(isbn: &str) -> bool {
    if isbn.contains('-') && isbn.chars().filter(|&c| c == '-').count() == 3 {
        // 可能是ISBN-10
        is_valid_isbn_10(isbn)
    } else if isbn.len() == 13 && isbn.starts_with("978") {
        // 可能是ISBN-13
        is_valid_isbn_13(isbn)
    } else {
        false
    }
}

fn is_valid_isbn_10(isbn: &str) -> bool {
    let mut total = 0u32;
    let mut position = 0;
    
    for &byte in isbn.as_bytes() {
        match byte {
            b'-' => continue,
            b'0'..=b'9' => {
                if position >= 10 {
                    return false;
                }
                let digit = (byte - b'0') as u32;
                total += digit * (10 - position) as u32;
                position += 1;
            }
            b'X' => {
                if position != 9 {
                    return false;
                }
                total += 10 * (10 - position) as u32;
                position += 1;
            }
            _ => return false,
        }
    }
    
    position == 10 && total % 11 == 0
}

fn is_valid_isbn_13(isbn: &str) -> bool {
    if isbn.len() != 13 {
        return false;
    }
    
    // 检查是否都是数字
    if !isbn.chars().all(|c| c.is_ascii_digit()) {
        return false;
    }
    
    // ISBN-13验证算法
    let sum: u32 = isbn
        .chars()
        .take(12)
        .map(|c| c.to_digit(10).unwrap())
        .enumerate()
        .map(|(i, digit)| if i % 2 == 0 { digit } else { digit * 3 })
        .sum();
    
    let check_digit = (10 - (sum % 10)) % 10;
    let actual_check_digit = isbn.chars().last().unwrap().to_digit(10).unwrap();
    
    check_digit == actual_check_digit
}

扩展功能

基于基础实现,我们可以添加更多功能:

pub struct IsbnValidator;

impl IsbnValidator {
    pub fn new() -> Self {
        IsbnValidator
    }
    
    /// 验证ISBN-10
    pub fn is_valid_isbn10(&self, isbn: &str) -> bool {
        self.validate_isbn10(isbn).is_ok()
    }
    
    /// 验证ISBN-13
    pub fn is_valid_isbn13(&self, isbn: &str) -> bool {
        self.validate_isbn13(isbn).is_ok()
    }
    
    /// 验证任何ISBN格式
    pub fn is_valid_isbn(&self, isbn: &str) -> bool {
        self.is_valid_isbn10(isbn) || self.is_valid_isbn13(isbn)
    }
    
    /// 获取ISBN详细信息
    pub fn get_isbn_info(&self, isbn: &str) -> Option<IsbnInfo> {
        if self.is_valid_isbn10(isbn) {
            Some(IsbnInfo {
                format: IsbnFormat::ISBN10,
                clean_value: isbn.chars().filter(|&c| c != '-').collect(),
                is_valid: true,
            })
        } else if self.is_valid_isbn13(isbn) {
            Some(IsbnInfo {
                format: IsbnFormat::ISBN13,
                clean_value: isbn.to_string(),
                is_valid: true,
            })
        } else {
            None
        }
    }
    
    /// 格式化ISBN
    pub fn format_isbn(&self, isbn: &str) -> Option<String> {
        if self.is_valid_isbn10(isbn) {
            let clean: String = isbn.chars().filter(|&c| c != '-').collect();
            Some(format!("{}-{}-{}-{}-{}",
                &clean[0..1],
                &clean[1..4],
                &clean[4..9],
                &clean[9..10],
                ""
            ))
        } else if self.is_valid_isbn13(isbn) {
            Some(format!("{}-{}-{}-{}-{}",
                &isbn[0..3],
                &isbn[3..4],
                &isbn[4..6],
                &isbn[6..12],
                &isbn[12..13]
            ))
        } else {
            None
        }
    }
    
    /// 转换ISBN-10到ISBN-13
    pub fn isbn10_to_isbn13(&self, isbn10: &str) -> Option<String> {
        if !self.is_valid_isbn10(isbn10) {
            return None;
        }
        
        let clean_isbn10: String = isbn10.chars().filter(|&c| c != '-').collect();
        let base = format!("978{}", &clean_isbn10[..9]);
        
        // 计算ISBN-13校验位
        let sum: u32 = base
            .chars()
            .map(|c| c.to_digit(10).unwrap())
            .enumerate()
            .map(|(i, digit)| if i % 2 == 0 { digit } else { digit * 3 })
            .sum();
        
        let check_digit = (10 - (sum % 10)) % 10;
        Some(format!("{}{}", base, check_digit))
    }
    
    fn validate_isbn10(&self, isbn: &str) -> Result<(), IsbnError> {
        let clean_isbn: String = isbn.chars().filter(|&c| c != '-').collect();
        
        if clean_isbn.len() != 10 {
            return Err(IsbnError::InvalidLength);
        }
        
        let mut total = 0u32;
        for (i, c) in clean_isbn.chars().enumerate() {
            let digit = match c {
                '0'..='9' => c.to_digit(10).unwrap(),
                'X' if i == 9 => 10,
                _ => return Err(IsbnError::InvalidCharacter(c)),
            };
            
            total += digit * (10 - i) as u32;
        }
        
        if total % 11 == 0 {
            Ok(())
        } else {
            Err(IsbnError::InvalidCheckDigit)
        }
    }
    
    fn validate_isbn13(&self, isbn: &str) -> Result<(), IsbnError> {
        if isbn.len() != 13 {
            return Err(IsbnError::InvalidLength);
        }
        
        if !isbn.chars().all(|c| c.is_ascii_digit()) {
            return Err(IsbnError::InvalidCharacter(' ')); // 简化处理
        }
        
        let sum: u32 = isbn
            .chars()
            .take(12)
            .map(|c| c.to_digit(10).unwrap())
            .enumerate()
            .map(|(i, digit)| if i % 2 == 0 { digit } else { digit * 3 })
            .sum();
        
        let check_digit = (10 - (sum % 10)) % 10;
        let actual_check_digit = isbn.chars().last().unwrap().to_digit(10).unwrap();
        
        if check_digit == actual_check_digit {
            Ok(())
        } else {
            Err(IsbnError::InvalidCheckDigit)
        }
    }
}

#[derive(Debug)]
pub enum IsbnFormat {
    ISBN10,
    ISBN13,
}

#[derive(Debug)]
pub struct IsbnInfo {
    pub format: IsbnFormat,
    pub clean_value: String,
    pub is_valid: bool,
}

#[derive(Debug, PartialEq)]
pub enum IsbnError {
    InvalidLength,
    InvalidCharacter(char),
    InvalidCheckDigit,
}

// 便利函数
pub fn is_valid_isbn(isbn: &str) -> bool {
    IsbnValidator::new().is_valid_isbn(isbn)
}

实际应用场景

ISBN验证在实际开发中有以下应用:

  1. 电商平台:验证图书商品的ISBN信息
  2. 图书馆系统:管理图书库存和检索
  3. 出版系统:验证出版物的标识符
  4. 数据清洗:处理和验证图书数据
  5. 条码扫描:验证扫描到的ISBN信息
  6. 数据库录入:在录入图书信息时验证ISBN
  7. API服务:提供ISBN验证服务
  8. 移动应用:图书扫描和识别应用

算法复杂度分析

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

    • 需要遍历字符串中的每个字符,其中n是字符串长度
  2. 空间复杂度:O(1)

    • 只使用常数额外空间(不包括输入字符串)

与其他实现方式的比较

// 使用正则表达式的实现
use regex::Regex;

pub fn is_valid_isbn_regex(isbn: &str) -> bool {
    let clean_isbn: String = isbn.chars().filter(|&c| c != '-').collect();
    
    if clean_isbn.len() != 10 {
        return false;
    }
    
    // 使用正则表达式验证格式
    let re = Regex::new(r"^(\d{9}[\dX])$").unwrap();
    if !re.is_match(&clean_isbn) {
        return false;
    }
    
    let sum: u32 = clean_isbn
        .chars()
        .enumerate()
        .map(|(i, c)| {
            let digit = if c == 'X' { 10 } else { c.to_digit(10).unwrap() };
            digit * (10 - i) as u32
        })
        .sum();
    
    sum % 11 == 0
}

// 使用递归的实现
pub fn is_valid_isbn_recursive(isbn: &str) -> bool {
    let clean_isbn: String = isbn.chars().filter(|&c| c != '-').collect();
    
    fn validate_recursive(chars: &[char], position: usize, total: u32) -> bool {
        if position == 10 {
            return total % 11 == 0;
        }
        
        if position >= chars.len() {
            return false;
        }
        
        let digit = match chars[position] {
            '0'..='9' => chars[position].to_digit(10).unwrap(),
            'X' if position == 9 => 10,
            _ => return false,
        };
        
        validate_recursive(chars, position + 1, total + digit * (10 - position) as u32)
    }
    
    if clean_isbn.len() != 10 {
        return false;
    }
    
    let chars: Vec<char> = clean_isbn.chars().collect();
    validate_recursive(&chars, 0, 0)
}

// 使用fold的函数式实现
pub fn is_valid_isbn_fold(isbn: &str) -> bool {
    let clean_isbn: String = isbn.chars().filter(|&c| c != '-').collect();
    
    if clean_isbn.len() != 10 {
        return false;
    }
    
    let chars: Vec<char> = clean_isbn.chars().collect();
    
    // 检查字符有效性
    if !chars[0..9].iter().all(|c| c.is_ascii_digit()) {
        return false;
    }
    
    if !chars[9].is_ascii_digit() && chars[9] != 'X' {
        return false;
    }
    
    // 使用fold计算总和
    let (sum, _) = chars
        .iter()
        .enumerate()
        .fold((0u32, true), |(acc, valid), (i, &c)| {
            if !valid {
                (acc, false)
            } else {
                let digit = if c == 'X' { 10 } else { c.to_digit(10).unwrap() };
                (acc + digit * (10 - i) as u32, true)
            }
        });
    
    sum % 11 == 0
}

总结

通过 isbn-verifier 练习,我们学到了:

  1. 字符串处理:掌握了字符过滤、转换和验证技巧
  2. 算法实现:学会了实现ISBN验证算法
  3. 错误处理:熟练使用Result和Option类型处理可能失败的操作
  4. 迭代器使用:深入理解了Rust迭代器的强大功能
  5. 性能优化:了解了不同实现方式的性能特点
  6. 边界情况处理:学会了处理各种输入边界情况

这些技能在实际开发中非常有用,特别是在处理数据验证、格式检查、字符串处理等场景中。ISBN验证虽然是一个具体的业务问题,但它涉及到了字符串处理、算法实现、错误处理等许多核心概念,是学习Rust实用编程的良好起点。

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐