Rust 练习册 :ISBN Verifier与字符串验证
在出版行业中,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的验证规则如下:
- ISBN由10个字符组成(不包括连字符)
- 前9位必须是数字,第10位可以是数字或字母’X’(代表10)
- 验证公式:(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. 核心要求
- 格式检查:验证字符串是否符合ISBN-10格式
- 字符处理:处理数字和校验字符’X’
- 数学验证:执行ISBN验证算法
- 连字符处理:忽略连字符
- 错误处理:处理各种无效输入
2. 技术要点
- 字符串处理:字符过滤和转换
- 迭代器使用:高效处理字符序列
- 数学计算:执行模运算验证
- 边界情况:处理各种无效输入
完整实现
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验证在实际开发中有以下应用:
- 电商平台:验证图书商品的ISBN信息
- 图书馆系统:管理图书库存和检索
- 出版系统:验证出版物的标识符
- 数据清洗:处理和验证图书数据
- 条码扫描:验证扫描到的ISBN信息
- 数据库录入:在录入图书信息时验证ISBN
- API服务:提供ISBN验证服务
- 移动应用:图书扫描和识别应用
算法复杂度分析
-
时间复杂度:O(n)
- 需要遍历字符串中的每个字符,其中n是字符串长度
-
空间复杂度: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 练习,我们学到了:
- 字符串处理:掌握了字符过滤、转换和验证技巧
- 算法实现:学会了实现ISBN验证算法
- 错误处理:熟练使用Result和Option类型处理可能失败的操作
- 迭代器使用:深入理解了Rust迭代器的强大功能
- 性能优化:了解了不同实现方式的性能特点
- 边界情况处理:学会了处理各种输入边界情况
这些技能在实际开发中非常有用,特别是在处理数据验证、格式检查、字符串处理等场景中。ISBN验证虽然是一个具体的业务问题,但它涉及到了字符串处理、算法实现、错误处理等许多核心概念,是学习Rust实用编程的良好起点。
通过这个练习,我们也看到了Rust在字符串处理和验证方面的强大能力,以及如何用安全且高效的方式实现数据验证算法。这种结合了安全性和性能的语言特性正是Rust的魅力所在。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)