Rust 练习册 31:啤酒歌与字符串格式化艺术

在编程学习过程中,我们经常会遇到一些看似简单但实际涉及复杂逻辑处理的问题。"Beer Song"(啤酒歌)就是这样一个经典练习,它源自一首传统的倒数歌曲,需要根据不同的数字生成不同的歌词。在 Exercism 的 "beer-song" 练习中,我们将实现这首经典的倒数歌曲,这不仅能帮助我们掌握字符串格式化的技巧,还能深入学习 Rust 中的模式匹配和条件逻辑处理。

啤酒歌的规则

啤酒歌是一首简单的倒数歌曲,歌词遵循以下模式:

  1. 对于 n > 1:

    复制代码
    {n} bottles of beer on the wall, {n} bottles of beer.
    Take one down and pass it around, {n-1} bottles of beer on the wall.
  2. 对于 n = 1:

    复制代码
    1 bottle of beer on the wall, 1 bottle of beer.
    Take it down and pass it around, no more bottles of beer on the wall.
  3. 对于 n = 0:

    复制代码
    No more bottles of beer on the wall, no more bottles of beer.
    Go to the store and buy some more, 99 bottles of beer on the wall.

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

rust 复制代码
pub fn verse(n: u32) -> String {
    unimplemented!("emit verse {}", n)
}

pub fn sing(start: u32, end: u32) -> String {
    unimplemented!("sing verses {} to {}, inclusive", start, end)
}

我们需要实现这两个函数,其中:

  1. verse 函数生成单节歌词
  2. sing 函数生成从 start 到 end 的完整歌曲(包含两端)

算法实现

1. 单节歌词实现

首先实现 verse 函数,这是整个练习的核心:

rust 复制代码
pub fn verse(n: u32) -> String {
    match n {
        0 => String::from("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"),
        1 => String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
        2 => String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
        _ => format!(
            "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
            n,
            n,
            n - 1
        ),
    }
}

这个实现使用了模式匹配来处理不同的情况:

  • 0 的特殊情况
  • 1 的特殊情况
  • 2 的特殊情况(因为 "1 bottle" 是单数)
  • 其他情况的通用格式

2. 完整歌曲实现

接下来实现 sing 函数:

rust 复制代码
pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

这个实现使用了函数式编程的方法:

  • 生成从 end 到 start 的数字范围
  • 反转范围以实现倒数效果
  • 对每个数字应用 verse 函数
  • 将结果连接成完整歌曲

测试用例分析

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

rust 复制代码
#[test]
fn test_verse_0() {
    assert_eq!(beer::verse(0), "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n");
}

第0节歌词的特殊处理。

rust 复制代码
#[test]
fn test_verse_1() {
    assert_eq!(beer::verse(1), "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n");
}

第1节歌词中使用 "bottle"(单数)和 "it"。

rust 复制代码
#[test]
fn test_verse_2() {
    assert_eq!(beer::verse(2), "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n");
}

第2节歌词中下一句使用 "1 bottle"(单数)。

rust 复制代码
#[test]
fn test_song_3_0() {
    assert_eq!(beer::sing(3, 0), "3 bottles of beer on the wall, 3 bottles of beer.\nTake one down and pass it around, 2 bottles of beer on the wall.\n\n2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n\n1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n\nNo more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n");
}

完整歌曲的生成,注意节与节之间用空行分隔。

优化实现

我们可以进一步优化实现,使其更加清晰和高效:

rust 复制代码
pub fn verse(n: u32) -> String {
    match n {
        0 => no_more_bottles(),
        1 => one_bottle(),
        2 => two_bottles(),
        _ => many_bottles(n),
    }
}

fn no_more_bottles() -> String {
    String::from("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n")
}

fn one_bottle() -> String {
    String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n")
}

fn two_bottles() -> String {
    String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n")
}

fn many_bottles(n: u32) -> String {
    format!(
        "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
        n,
        n,
        n - 1
    )
}

pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

更加灵活的实现

为了更好地处理复数形式和文本变化,我们可以创建辅助函数:

rust 复制代码
pub fn verse(n: u32) -> String {
    match n {
        0 => format!(
            "{} {} of beer on the wall, {} {} of beer.\n{}",
            bottles_count(n),
            bottles(n),
            bottles_count(n),
            bottles(n),
            "Go to the store and buy some more, 99 bottles of beer on the wall.\n"
        ),
        1 => format!(
            "{} {} of beer on the wall, {} {} of beer.\nTake it down and pass it around, {} {} of beer on the wall.\n",
            bottles_count(n),
            bottles(n),
            bottles_count(n),
            bottles(n),
            bottles_count(n - 1),
            bottles(n - 1)
        ),
        _ => format!(
            "{} {} of beer on the wall, {} {} of beer.\nTake one down and pass it around, {} {} of beer on the wall.\n",
            bottles_count(n),
            bottles(n),
            bottles_count(n),
            bottles(n),
            bottles_count(n - 1),
            bottles(n - 1)
        ),
    }
}

fn bottles_count(n: u32) -> String {
    match n {
        0 => String::from("no more"),
        _ => n.to_string(),
    }
}

fn bottles(n: u32) -> String {
    if n == 1 {
        String::from("bottle")
    } else {
        String::from("bottles")
    }
}

pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

使用宏的实现

我们还可以使用宏来简化重复的格式化代码:

rust 复制代码
macro_rules! verse_template {
    ($n:expr, $action:expr) => {
        format!(
            "{} {} of beer on the wall, {} {} of beer.\n{}",
            bottles_count($n),
            bottles($n),
            bottles_count($n),
            bottles($n),
            $action
        )
    };
}

pub fn verse(n: u32) -> String {
    match n {
        0 => verse_template!(
            n,
            "Go to the store and buy some more, 99 bottles of beer on the wall.\n"
        ),
        1 => verse_template!(
            n,
            format!(
                "Take it down and pass it around, {} {} of beer on the wall.\n",
                bottles_count(n - 1),
                bottles(n - 1)
            )
        ),
        _ => verse_template!(
            n,
            format!(
                "Take one down and pass it around, {} {} of beer on the wall.\n",
                bottles_count(n - 1),
                bottles(n - 1)
            )
        ),
    }
}

fn bottles_count(n: u32) -> String {
    match n {
        0 => String::from("no more"),
        _ => n.to_string(),
    }
}

fn bottles(n: u32) -> String {
    if n == 1 {
        String::from("bottle")
    } else {
        String::from("bottles")
    }
}

pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

性能优化版本

考虑到性能,我们可以预分配字符串容量:

rust 复制代码
pub fn verse(n: u32) -> String {
    let mut result = String::with_capacity(200); // 预估容量
    
    match n {
        0 => result.push_str("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"),
        1 => result.push_str("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
        2 => result.push_str("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
        _ => {
            use std::fmt::Write;
            write!(
                result,
                "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
                n,
                n,
                n - 1
            ).unwrap();
        }
    }
    
    result
}

pub fn sing(start: u32, end: u32) -> String {
    let capacity = (start - end + 1) as usize * 200; // 估算总容量
    let mut result = String::with_capacity(capacity);
    
    for i in (end..=start).rev() {
        result.push_str(&verse(i));
        if i > end {
            result.push('\n');
        }
    }
    
    result
}

错误处理和边界情况

在实际应用中,我们需要考虑输入验证:

rust 复制代码
#[derive(Debug, PartialEq)]
pub enum BeerSongError {
    InvalidRange,
}

pub fn verse(n: u32) -> String {
    match n {
        0 => String::from("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"),
        1 => String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
        2 => String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
        _ => format!(
            "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
            n,
            n,
            n - 1
        ),
    }
}

pub fn sing(start: u32, end: u32) -> String {
    if start < end {
        // 可以选择返回错误或交换参数
        return sing(end, start);
    }
    
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

实际应用场景

虽然啤酒歌看起来只是一个练习,但它在实际开发中有以下应用:

  1. 模板引擎:学习如何根据数据生成文本
  2. 代码生成:根据规则生成重复性代码
  3. 教育工具:教授条件逻辑和字符串处理
  4. 测试用例:生成标准化的测试数据

扩展功能

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

rust 复制代码
pub struct BeerSong {
    max_bottles: u32,
}

impl BeerSong {
    pub fn new(max_bottles: u32) -> Self {
        BeerSong { max_bottles }
    }
    
    pub fn verse(&self, n: u32) -> String {
        match n {
            0 => format!(
                "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, {} bottles of beer on the wall.\n",
                self.max_bottles
            ),
            1 => String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
            2 => String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
            _ => format!(
                "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
                n,
                n,
                n - 1
            ),
        }
    }
    
    pub fn sing(&self, start: u32, end: u32) -> String {
        if start < end {
            return String::new(); // 或者返回错误
        }
        
        (end..=start)
            .rev()
            .map(|n| self.verse(n))
            .collect::<Vec<String>>()
            .join("\n")
    }
}

总结

通过 beer-song 练习,我们学到了:

  1. 字符串格式化:掌握了 Rust 中多种字符串格式化方法
  2. 模式匹配:熟练使用 match 表达式处理不同情况
  3. 条件逻辑:学会了如何处理复杂的条件分支
  4. 函数组合:理解了如何将简单函数组合成复杂功能
  5. 性能优化:了解了字符串预分配等优化技巧
  6. 错误处理:学会了如何处理边界情况

这些技能在实际开发中非常有用,特别是在生成报告、构建模板系统、处理自然语言和进行文本操作时。啤酒歌虽然看起来简单,但它涉及到了字符串处理、条件逻辑和格式化输出等许多核心概念,是学习 Rust 文本处理的良好起点。

通过这个练习,我们也看到了 Rust 在处理字符串和文本格式化方面的强大能力,以及如何用清晰的代码表达复杂的逻辑。这种优雅的实现方式正是 Rust 语言的魅力所在。

相关推荐
花褪残红青杏小5 分钟前
Rust图像处理第8节-暗角 & 复古胶片特效:四周衰减中心高亮
rust·webassembly·图形学
独孤留白15 小时前
从C到Rust:Rust 的 Trait 不是Interface,那是什么?
rust
花褪残红青杏小1 天前
Rust图像处理第7节-马赛克像素化:分块取平均色实现打码风格
rust·webassembly·图形学
doiito2 天前
【Agent Harness】Gliding Horse 设计细节 -- 不跟风开发自己的AI Agent
架构·rust·agent
doiito2 天前
【Agent Harness】Gliding Horse 核心设计理念,不跟风开发自己的AI Agent
ai·rust·架构设计·系统设计·ai agent
花褪残红青杏小2 天前
Rust图像处理第6节- 均值模糊 & 中值模糊:3×3 邻域的两种经典玩法
rust·webassembly·图形学
子兮曰2 天前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
星栈2 天前
写 Dioxus Demo 不难,难的是把它写成项目
前端·rust·前端框架
mCell2 天前
【锐评】桌面端技术营销:别拿跑分当工程判断
前端·rust·electron
武子康3 天前
调查研究-201 Rust 里的 dev build 和 release build:为什么同一份代码性能差这么多?
后端·架构·rust