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 语言的魅力所在。

相关推荐
Beyond_System|系统之外13 分钟前
【学编程】Python基础编程题100道(21-60)
开发语言·python·算法
景熙552336 分钟前
15.Java 8 Stream 流入门到实战
java·开发语言·数据结构
xiaoye-duck1 小时前
《Linux 网络编程》深入理解 IP 协议(二):网段划分、私有 IP 与 NAT 地址转换
linux·网络·ip
Bruce_Liuxiaowei3 小时前
Python 实例赋值遮蔽类属性:一个从不报错的静默陷阱
开发语言·python·语法糖
梦醒沉醉4 小时前
std1.97.1——cmp模块细览
rust
广州宏帝箱包4 小时前
出口箱包的包装标准:防潮、防摔、运输安全的设计要点
大数据·网络
彧azz4 小时前
Linux 网络编程学习总结
linux·网络·笔记·学习·面试
updayday8544 小时前
离职域账号状态变更与Ping64操作记录核对
大数据·网络·数据库·安全·智能路由器
aramae4 小时前
MySQL内置函数(7)
开发语言·笔记·后端·mysql·其他
安易算力5 小时前
PUE优化工程实践:从1.5到1.2的制冷架构与气流组织改造路径
网络·python·容器·架构·kubernetes