4
\$\begingroup\$

I've recently been trying to learn more rust and I have just finished chapter 3 of the Rust book, which has a list of projects to do to practice.

I have tried to write a program that prints the 12 Days of Christmas and I would like to know if it is idiomatic or if any major improvementsc could be made.

Thanks in advance.

const LYRICS: [&str; 12] = [
    "Twelve drummers drumming",
    "Eleven pipers piping",
    "Ten lords a-leaping",
    "Nine ladies dancing",
    "Eight maids a-milking",
    "Seven swans a-swimming",
    "Six geese a-laying",
    "Five golden rings",
    "Four calling birds",
    "Three french hens",
    "Two turtle doves, and",
    "A partridge in a pear tree",
];

fn gen_lyrics(day: i32) -> String {
    let ordinal = match day {
        1 => "1st".to_string(),
        2 => "2nd".to_string(),
        3 => "3rd".to_string(),
        _ => day.to_string() + "th",
    };

    let mut start = format!(
        "On the {} day of Christmas, my true love gave to me:\n",
        ordinal
    );

    for i in (0..day).rev() {
        start.push_str(&LYRICS[11 - (i as usize)]);

        if i != 0 {
            start.push_str("\n");
        }
    }

    start
}

fn main() {
    for i in 1..13 {
        println!("{}\n", gen_lyrics(i))
    }
}
\$\endgroup\$

1 Answer 1

7
\$\begingroup\$

This looks pretty good! I'm still fairly new to Rust myself, so I'm not sure how familiar I am with the idioms beyond those outlined in the book. That said, there's a few improvements I can suggest.

Unsigned integer for day

Since day is never negative, you can use an unsigned integer instead of an i32 here:

fn gen_lyrics(day: usize) -> String

If you use usize specifically, you also avoid the cast to usize here:

start.push_str(&LYRICS[11 - (i as usize)]);

That would then just be:

start.push_str(&LYRICS[11 - i]);

Ordinal suffix match expression

You can avoid some .to_string() calls and repetition of numbers by making the match expression return just the suffix of the ordinal. You can then append that suffix to the number itself in the format!() call:

let ordinal_suffix = match day {
    1 => "st",
    2 => "nd",
    3 => "rd",
    _ => "th",
};

let mut start = format!(
    "On the {}{} day of Christmas, my true love gave to me:\n",
    day,
    ordinal_suffix
);

Inclusive range

You can use ..= instead of .. to create an inclusive range:

fn main() {
    for i in 1..=12 {
        println!("{}\n", gen_lyrics(i))
    }
}

Iterators and the skip method

You can iterate directly over the LYRICS array, skipping over the number of lines you don't want to include:

for line in (&LYRICS).iter().skip(12 - day) {
    start.push_str(line);
    start.push_str("\n");
}

The problem with this is that there's no longer an i variable to check whether an iteration is the last line or not, so it can't conditionally insert a newline. The code above will always insert a newline.

But we can cheat a little 😉

If you remove the newline from the end of the start variable:

"On the {} day of Christmas, my true love gave to me:"

You can push the newline first, that way you no longer need a conditional in the loop:

for line in (&LYRICS).iter().skip(12 - day) {
    start.push_str("\n");
    start.push_str(line);
}

Bringing it all together

This is the code with all the changes applied:

const LYRICS: [&str; 12] = [
    "Twelve drummers drumming",
    "Eleven pipers piping",
    "Ten lords a-leaping",
    "Nine ladies dancing",
    "Eight maids a-milking",
    "Seven swans a-swimming",
    "Six geese a-laying",
    "Five golden rings",
    "Four calling birds",
    "Three french hens",
    "Two turtle doves, and",
    "A partridge in a pear tree",
];

fn gen_lyrics(day: usize) -> String {
    let ordinal_suffix = match day {
        1 => "st",
        2 => "nd",
        3 => "rd",
        _ => "th",
    };

    let mut start = format!(
        "On the {}{} day of Christmas, my true love gave to me:",
        day,
        ordinal_suffix
    );

    for line in (&LYRICS).iter().skip(12 - day) {
        start.push_str("\n");
        start.push_str(line);
    }

    start
}

fn main() {
    for i in 1..=12 {
        println!("{}\n", gen_lyrics(i))
    }
}
\$\endgroup\$
8
  • 1
    \$\begingroup\$ Thanks for this great answer - I hope to see more from you in future! \$\endgroup\$ Commented Jan 8, 2020 at 9:30
  • \$\begingroup\$ I think you can just start.push_str((&LYRICS).iter().skip(12 - day).join("\n")) \$\endgroup\$
    – Alexander
    Commented Jan 8, 2020 at 14:57
  • 1
    \$\begingroup\$ What's an idiomatic way to print an iterator separated by spaces in Rust? \$\endgroup\$
    – Shepmaster
    Commented Jan 8, 2020 at 19:32
  • 1
    \$\begingroup\$ no longer an i variable to checkIterator::enumerate. \$\endgroup\$
    – Shepmaster
    Commented Jan 8, 2020 at 19:33
  • 1
    \$\begingroup\$ (&LYRICS).iter() can just be LYRICS.iter() \$\endgroup\$
    – Shepmaster
    Commented Jan 8, 2020 at 19:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.