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

相关推荐
百***58841 小时前
MacOS升级ruby版本
开发语言·macos·ruby
执笔论英雄1 小时前
【大模型训练】forward_backward_func返回多个micro batch 损失
开发语言·算法·batch
草莓熊Lotso2 小时前
C++ STL map 系列全方位解析:从基础使用到实战进阶
java·开发语言·c++·人工智能·经验分享·网络协议·everything
q***71852 小时前
QoS质量配置
开发语言·智能路由器·php
草莓熊Lotso2 小时前
《算法闯关指南:优选算法--模拟》--41.Z 字形变换,42.外观数列
开发语言·c++·算法
shura10142 小时前
如何优雅地实现参数校验
java·开发语言
基于底层的菜鸟2 小时前
网络抓包工具——tcpdump &&icmpv6抓包
网络·测试工具·tcpdump
20岁30年经验的码农3 小时前
Python语言基础文档
开发语言·python
tang777894 小时前
如何保护网络隐私?从理解代理IP开始
网络·tcp/ip·php