Convert "sl" Source Code from C to Rust

Back to Put All Images As Constants into One File

Three Ways to Use A Variable for Decision-Making

The three examples below just use simple example code. A little further down, we will implement one of those three methods in our real project.

Suppose we have a variable that can hold any one of several possibilities. For example, we might have a "flavor" variable that can be "apple, "peach", "banana", "strawberry", "grape", or "kiwi". It's easy to set that variable, but then we need to take action based on that variable's value. Here are three ways we can make decisions based on that variable.

Use an If-Then

This is a simple, easy-to-understand method.

main.rs
fn main() {
    
    // Here's the variable specifying which flavor has been chosen.
    let flavor = "apple";
    // let flavor = "peach";
    // let flavor = "banana";
    // let flavor = "strawberry";
    // let flavor = "grape";
    // let flavor = "kiwi";
    
    // Make decision based on that variable.
    if flavor == "apple" {
      println!("Apples are yummy.");
    }
    if flavor == "peach" {
      println!("I prefer apples.");
    }
    if flavor == "banana" {
      println!("Only in Banana Pudding.");
    }
    if flavor == "strawberry" {
      println!("My favorite ice cream!");
    }
    if flavor == "grape" {
      println!("My favorite flavor of gum!");
    }
    if flavor == "kiwi" {
      println!("Kiwi?! Really?!");
    }
} // end of main()

Use a "match" Statement

This is a cleaner method. It also has a required "none of the above" option (the "_" option). We could (should?) have done a "none of the above" option with the above If-Then method, but it's not quite as easy there, so we didn't bother.

main.rs
fn main() {
    
    // Here's the variable specifying which flavor has been chosen.
    let flavor = "apple";
    // let flavor = "peach";
    // let flavor = "banana";
    // let flavor = "strawberry";
    // let flavor = "grape";
    // let flavor = "kiwi";
    
    // Make decision based on that variable.
    match flavor {
        "apple"         =>  println!("Apples are yummy."),
        "peach"         =>  println!("I prefer apples."),
        "banana"        =>  println!("Only in Banana Pudding."),
        "strawberry"    =>  println!("My favorite ice cream!"),
        "grape"         =>  println!("My favorite flavor of gum!"),
        "kiwi"          =>  println!("Kiwi?! Really?!"),
        _               =>  println!("Uh oh; unknown flavor specified."),
    };
} // end of main()

Use a hashmap

This method builds a one-to-one matching "table" (like a spreadsheet, sort of) that has one column that holds the flavor, and a second column that holds the matching value to that flavor. This method is a bit harder to wrap your brain around, but it's pretty slick.

main.rs
                                                            
use std::collections::HashMap;

fn main() {

    let messages = HashMap::from([
        ("apple", "Apples are yummy."),
        ("peach", "I prefer apples."),
        ("banana", "Only in Banana Pudding."),
        ("strawberry", "My favorite ice cream!"),
        ("grape", "My favorite flavor of gum!"),
        ("kiwi", "Kiwi?! Really?!"),
    ]);

    // Here's the variable specifying which flavor has been chosen.
    // let flavor = "apple";
    // let flavor = "peach";
    // let flavor = "banana";
    let flavor = "strawberry";
    // let flavor = "grape";
    // let flavor = "kiwi";

    // Make decision based on that variable.
    println!("{}", messages.get(flavor).unwrap());
} // end of main()

You build a hashmap named "messages" which holds the "hash table" (or "look-up table", or "spreadsheet"); later, you send the chosen flavor to that hashmap, and it returns a "Return"-type package. Imagine United Parcel Service (UPS) delivering a package to your front door. Imagine that they deliver every package with a note on top that says either "OK" or "Error". When you open an "OK" package, you find the item you were expecting, but if you open an "Error" package, you find a broken item with a note that says, "This item was broken during shipping", or "This item was already broken when UPS picked it up", or something similar. This is similar to a "Return"-type of data in Rust. The proper thing to do is to examine the return type (is it an "Ok" or an "Err"?), and then proceed accordingly. But in our example above, we don't care that much about doing "the right thing" with any errors, so we just "unwrap()" the package without worrying about the "Ok" or "Err", trusting that whatever we find in the package will be usable for our purposes.

If We Were to Implement the HashMap Into Our "sl" Project

For our "sl" project, we can use any of the above three methods to convert a variable specifying the image to draw into the displaying of that image, but the If-Then method is probably the least-preferable. Since the hashmap method is probably less better-documented in the wild than is the match method, I'll show here how to use it, if for no other reason than to add to the world's documentation of how to use it, but I think we'll wind up going with the match method. But if we were to use the hashmap method, here's how our mods would look:

main.rs
/* main.rs */

mod images;

use images::*;
use std::collections::HashMap;

fn main() {
    let kind = get_kind();
    
    let image_vecvec = string_to_vecvecstrings(kind);
    
    display_image(image_vecvec);
} // end of main()

...

fn get_kind() -> String {

    // Our string is stored in a constant in the "images.rs" file.

    // Build a hashmap.
    let image_str: HashMap<&str, &str> = HashMap::from([
        ("D51", D51), // Maps the string "D51" to the constant D51 stored in "images.rs".
        ("LITTLE", LITTLE),
        ("C51", C51),
        ("BOAT", BOAT),
        ("TWINENGINE", TWINENGINE),
        ("PLANE", PLANE),
        ("MOTORCYCLE", MOTORCYCLE),
        ("JACK", JACK),
        ("COALCAR", COALCAR),
        ("FERRIS", FERRIS),
    ]);
    
    // Our choice of image kind is ...
    // let kind = "D51";
    // let kind = "LITTLE";
    // let kind = "C51";
    let kind = "BOAT";
    // let kind = "TWINENGINE";
    // let kind = "PLANE";
    // let kind = "MOTORCYCLE";
    // let kind = "JACK";
    // let kind = "COALCAR";
    // let kind = "FERRIS";
    
    // Find our choice in the hashmap, and return it.
    return image_str.get(kind).unwrap().to_string();
    return BOAT.to_string();
} // end of get_kind

...
images.rs
/* images.rs */

/*  Feel free to add your own images, and modify the rest of the program code accordingly.
    - Ideally all the lines of all the frames of a particular image should be of
        the same length. But if not, the first line of the first frame should be
        the longest, as that's the line the program uses to determine an image's
        length.
    - If there are multiple frames/cels of the image, a blank line must separate the frames.
    - The single quotes at the end of the lines are optional; they merely serve as a visual
      indicator of the end of the line. Some of the image lines have the single quote;
        some do not. The program will strip them out if they are included here.
    - The "r" before the string tells the compiler to read the string in "raw"
        mode, so that backslashes are not interpreted as escape characters.
    - If a hash/splat ("#") is used in an image, the "r" must be modified to be "r#", and
        the end of the string must be closed with a "#". You can see this in both the
        TWINENGINE and MOTORCYCLE declarations.
    - Put an entry for your new image into the "Build a hashmap" section of the "get_image()"
        function.
...

But, as mentioned, we'll instead use the match method. This will give us a little more flexibility. For example, the user running the program will be able to specify "p" or "P" or "plane" or "pLANe", etc, rather than a strict "PLANE". We'll set this up in the next lesson, Getting User-Provided Options on the Command-Line.