Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Building a Command-Line Unit Converter with Functions, Control Flow, and Structs

Hello! In the previous lesson, you gave a TripSegment named fields and methods. This module project brings those pieces together with functions and branching: you’ll build a command-line converter that accepts a number and a conversion mode, then prints the result. Allow about 40 minutes to read, build, and run it.

The converter will support kilometres and miles in both directions, plus Celsius and Fahrenheit in both directions. It will use only the Rust standard library.


Decide what the command means

A command-line program receives text from the shell. Let’s give this one a two-argument interface:

cargo run -- 10 km-mi

The number is the value to convert; km-mi means kilometres to miles. The other modes will be mi-km, c-f, and f-c. The -- tells Cargo to pass what follows to your program rather than interpret it as Cargo options.

Before writing the converter, read how a Rust program obtains those arguments:

Parsing command line arguments

Read “Parsing command-line arguments” from Command Line Applications in Rust for the small amount of CLI machinery this project needs.

Read the opening explanation of command-line arguments and the “Getting the arguments” subsection. Start at CLI arguments, continue through the sample cargo run command and its output, and stop before “CLI arguments as data types.” Notice that argument zero names the program, so the first value you supply is at position one.

The reading retrieves arguments as strings. Our program will additionally convert the first supplied string to an f64. We’ll keep the command format fixed rather than add a CLI library.


Separate the calculations from the command

Create a fresh binary project in your macOS shell:

cargo new unit_converter
cd unit_converter

Before handling input, define the four calculations. One mile is 1.609344 kilometres. Unlike distance conversion, temperature conversion includes an offset:

Open src/main.rs. Replace its contents with this first part; you’ll add main underneath shortly.

const KM_PER_MILE: f64 = 1.609_344;

fn km_to_mi(km: f64) -> f64 {
    km / KM_PER_MILE
}

fn mi_to_km(mi: f64) -> f64 {
    mi * KM_PER_MILE
}

fn c_to_f(celsius: f64) -> f64 {
    celsius * 9.0 / 5.0 + 32.0
}

fn f_to_c(fahrenheit: f64) -> f64 {
    (fahrenheit - 32.0) * 5.0 / 9.0
}

Each function has one job and can be understood without knowing where its input came from. The underscores in 1.609_344 make the literal easier to read; they do not change its value. We use floating-point arithmetic because conversion results are not generally whole numbers.


Give one requested conversion a type

The value and mode belong to the same request. As with TripSegment, a struct makes that relationship explicit. Add this below the four functions:

struct Conversion {
    value: f64,
    mode: String,
}

impl Conversion {
    fn new(value: f64, mode: String) -> Self {
        Self { value, mode }
    }

    fn print_result(&self) {
        if self.mode == "km-mi" {
            println!("{:.2} km = {:.2} mi", self.value, km_to_mi(self.value));
        } else if self.mode == "mi-km" {
            println!("{:.2} mi = {:.2} km", self.value, mi_to_km(self.value));
        } else if self.mode == "c-f" {
            println!("{:.2} C = {:.2} F", self.value, c_to_f(self.value));
        } else if self.mode == "f-c" {
            println!("{:.2} F = {:.2} C", self.value, f_to_c(self.value));
        } else {
            eprintln!(
                "Unsupported mode: {}. Use km-mi, mi-km, c-f, or f-c.",
                self.mode
            );
        }
    }
}

new creates the request; print_result chooses the calculation. The else if chain checks the modes in order and executes one branch. An unrecognised mode reaches the final else rather than silently producing a number.

The &self receiver lets print_result read the request without changing it. In the format strings, {:.2} displays a number to two decimal places. That changes the display, not the underlying f64 calculation. eprintln! writes the unsupported-mode message to standard error, which is a useful place for diagnostics.


Connect the shell to the model

Now add main beneath the impl block:

fn main() {
    let value_text = std::env::args()
        .nth(1)
        .expect("Expected a number, for example: 10");

    let mode = std::env::args()
        .nth(2)
        .expect("Expected a mode, for example: km-mi");

    let value: f64 = value_text
        .parse()
        .expect("The first argument must be a number");

    let conversion = Conversion::new(value, mode);
    conversion.print_result();
}

Each call to std::env::args() starts reading the arguments afresh. .nth(1) obtains the first value after the program name, and .nth(2) obtains the second. Those arguments arrive as String values. The value: f64 annotation tells .parse() which numeric type to produce.

expect stops the program with a message if an argument is absent or the number cannot be parsed. That is enough to get this prototype working, though it is not yet graceful error handling. Later in the course, you’ll learn to represent and handle such failures explicitly.

Notice the division of responsibilities: main deals with input, Conversion associates a value with its mode and selects a branch, and the four small functions do the arithmetic. Adding a conversion calculation does not require changing how shell arguments are retrieved.


Run the converter

From the project directory, try each mode:

cargo run -- 10 km-mi
cargo run -- 1 mi-km
cargo run -- 0 c-f
cargo run -- 32 f-c

The corresponding result lines should be:

10.00 km = 6.21 mi
1.00 mi = 1.61 km
0.00 C = 32.00 F
32.00 F = 0.00 C

Also try cargo run -- 10 km-f. It should report an unsupported mode. A missing argument or a nonnumeric first argument will instead trigger one of the expect messages. This version ignores any arguments after the mode and does not set a failure exit status for an unsupported mode; both are deliberate limits of the small prototype, not features to rely on in a larger CLI tool.

One distinction is worth retaining: value is an f64, while mode is a String supplied by the shell and stored in the struct. The next module will examine why passing and storing those two kinds of value have different ownership implications.


What you’ve built

You now have a runnable Cargo project whose CLI input becomes a Conversion struct. Its method uses control flow to select among reusable, typed conversion functions. The shell-facing code and the arithmetic remain separate, making each part easier to change.

This completes the fundamentals module. Next, you’ll investigate ownership and drop points—the rules behind how Rust manages values such as the converter’s String without a garbage collector.

Can't find a good explanation? Sign up and we'll make it for you

Sign up