Rust 练习册 :掌握字符串处理与迭代器链
在日常工作中,我们经常需要处理文本数据并从中提取有用的信息。例如,从长句中创建首字母缩写词就是一种常见的需求。今天我们要探讨的是 Exercism 上的 “acronym” 练习,它可以帮助我们深入理解 Rust 中的字符串处理、字符识别和迭代器链的使用。
问题描述
acronym 练习的目标是从给定的短语中提取首字母并组成缩写词。例如,“Portable Network Graphics” 应该变成 “PNG”,“GNU Image Manipulation Program” 应该变成 “GIMP”。
函数的基本签名如下:
pub fn abbreviate(phrase: &str) -> String {
unimplemented!("Given the phrase '{}', return its acronym", phrase);
}
我们需要实现这个函数,让它能够正确处理各种边界情况,如标点符号、驼峰命名法、全大写单词等。
基础实现思路
要解决这个问题,我们需要:
- 识别单词的首字母
- 处理各种分隔符(空格、标点符号等)
- 正确处理驼峰命名法(如 HyperText)
- 将所有首字母组合成最终的结果
简单实现方案
让我们从一个简单但有效的实现开始:
pub fn abbreviate(phrase: &str) -> String {
let mut result = String::new();
let mut new_word = true;
for c in phrase.chars() {
if c.is_alphabetic() {
if new_word {
result.push(c.to_ascii_uppercase());
new_word = false;
}
} else {
new_word = true;
}
}
result
}
这个实现遍历输入字符串的每个字符,当遇到字母且是新单词的第一个字母时,将其大写并添加到结果中。当遇到非字母字符时,标记下一个字母为新单词的开始。
更复杂的实现
然而上面的实现无法正确处理驼峰命名法。比如 “HyperText” 应该产生 “HT”,但上述代码只会产生 “H”。我们需要改进算法以检测驼峰命名中的单词边界:
pub fn abbreviate(phrase: &str) -> String {
let mut acronym = String::new();
let mut prev_is_lowercase = false;
let mut next_should_be_uppercase = true;
for c in phrase.chars() {
if c.is_ascii_uppercase() {
if next_should_be_uppercase || prev_is_lowercase {
acronym.push(c);
next_should_be_uppercase = false;
}
prev_is_lowercase = false;
} else if c.is_ascii_lowercase() {
if next_should_be_uppercase {
acronym.push(c.to_ascii_uppercase());
next_should_be_uppercase = false;
}
prev_is_lowercase = true;
} else if c.is_numeric() {
if next_should_be_uppercase {
// 数字不计入首字母缩写
next_should_be_uppercase = false;
}
prev_is_lowercase = false;
} else {
// 遇到分隔符,下一个字母应该是首字母
next_should_be_uppercase = true;
prev_is_lowercase = false;
}
}
acronym
}
使用迭代器的优雅实现
虽然手动循环可以工作,但 Rust 的迭代器提供了一种更声明式的表达方式。我们可以结合多种迭代器适配器来解决问题:
pub fn abbreviate(phrase: &str) -> String {
phrase
.split(|c: char| !c.is_alphanumeric())
.filter(|word| !word.is_empty())
.flat_map(|word| {
word.chars().take(1).chain(
word.chars()
.skip(1)
.filter(|c| c.is_ascii_uppercase()),
)
})
.collect::<String>()
.to_uppercase()
}
不过,这种方法仍然不能很好地处理驼峰命名法。让我们看一个更全面的解决方案:
完整的解决方案
pub fn abbreviate(phrase: &str) -> String {
let mut result = String::new();
let mut chars = phrase.chars().peekable();
let mut prev_char_was_lowercase = false;
while let Some(current_char) = chars.next() {
// 如果当前字符是字母
if current_char.is_alphabetic() {
// 检查是否应该被加入到缩写词中
let should_add =
// 如果是第一个字符
result.is_empty() ||
// 如果前一个字符不是小写字母,且当前字符是大写字母(驼峰命名)
(!prev_char_was_lowercase && current_char.is_uppercase()) ||
// 如果前一个字符不是字母(即新单词开始)
(!chars.clone().nth(0).map_or(true, |c| c.is_alphabetic()));
if should_add {
result.push(current_char.to_ascii_uppercase());
}
prev_char_was_lowercase = current_char.is_lowercase();
} else {
// 当前字符不是字母,则下一个字母应视为新单词开始
prev_char_was_lowercase = false;
}
}
result
}
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
#[test]
fn basic() {
assert_eq!(acronym::abbreviate("Portable Network Graphics"), "PNG");
}
这是最基本的用例,用空格分隔的单词。
#[test]
fn camelcase() {
assert_eq!(acronym::abbreviate("HyperText Markup Language"), "HTML");
}
这个测试展示了对驼峰命名法的支持,需要识别出 “HyperText” 中的 “H” 和 “T”。
#[test]
fn punctuation() {
assert_eq!(acronym::abbreviate("First In, First Out"), "FIFO");
}
标点符号应该被视为单词分隔符。
#[test]
fn all_caps_word_with_punctuation() {
assert_eq!(acronym::abbreviate("PHP: Hypertext Preprocessor"), "PHP");
}
这个有趣的例子展示了递归缩写的处理,“PHP” 本身就是 “PHP: Hypertext Preprocessor” 的缩写。
使用正则表达式的替代方法
对于熟悉正则表达式的人来说,另一种方法是使用正则表达式 crate:
// 需要在 Cargo.toml 中添加 regex 依赖
// [dependencies]
// regex = "1"
// use regex::Regex;
//
// pub fn abbreviate(phrase: &str) -> String {
// let re = Regex::new(r"[A-Z]+[a-z]*|[a-z]+").unwrap();
// re.find_iter(phrase)
// .map(|mat| mat.as_str().chars().next().unwrap().to_ascii_uppercase())
// .collect()
// }
性能考虑
在选择实现方案时,我们应该考虑性能因素:
- 字符串遍历是最基本的操作,避免不必要的内存分配
- 对于简单场景,手动字符遍历通常比正则表达式更快
- 使用
peekable()迭代器可以方便地向前查看字符 - 在构建结果字符串时,预估容量可以减少重新分配次数
总结
通过这个练习,我们学到了以下重要概念:
- 字符串处理 - 如何在 Rust 中高效处理 Unicode 字符串
- 字符分类 - 使用
is_alphabetic(),is_ascii_uppercase()等方法 - 迭代器链 - 将复杂操作分解为一系列简单的迭代器适配器
- 边界条件处理 - 如何处理各种特殊字符和格式
掌握这些技能不仅有助于解决类似的文本处理问题,也加深了对 Rust 字符串模型和迭代器系统的理解。在实际项目中,这些技术经常用于解析配置文件、处理用户输入、格式化输出等各种场景。
更多推荐

所有评论(0)