Adding Unit Measurement Option to Rust Weather CLI

Mục tiêu: Enhancing the Rust Weather CLI by adding a command-line option to choose between Celsius and Fahrenheit units.


Adding Command-Line Option for Units in Rust Weather CLI

In this task, we’ll add a command-line option to specify the unit of measurement for weather data. This will enhance the usability of our Weather CLI tool by allowing users to choose between Celsius and Fahrenheit.

Step 1: Add Dependencies

First, we need to add the clap crate to our Cargo.toml for command-line argument parsing:

[dependencies]
clap = "3.2"

Step 2: Update Main Function

Modify the main() function to accept and parse command-line arguments:

use clap::{App, Arg};

fn main() {
    // Parse command-line arguments
    let matches = App::new("weather-cli")
        .version("1.0")
        .author("Your Name")
        .about("A command-line weather tool")
        .arg(
            Arg::with_name("unit")
                .short("u")
                .long("unit")
                .value_name("UNIT")
                .help("Unit of measurement [C for Celsius, F for Fahrenheit]")
                .default_value("C")
                .possible_values(&["C", "F"]),
        )
        .get_matches();

    let unit = matches.value_of("unit").unwrap_or("C");

    // Rest of your program logic here
}

Step 3: Create Unit Enum

Define an enum for units to handle different cases:

#[derive(Debug)]
enum Unit {
    Celsius,
    Fahrenheit,
}

impl Unit {
    fn from_str(s: &str) -> Unit {
        match s.to_uppercase() {
            "C" => Unit::Celsius,
            "F" => Unit::Fahrenheit,
            _ => Unit::Celsius, // Default to Celsius
        }
    }
}

Step 4: Update Configuration

Modify your configuration handling to include the unit:

use std::fs::File;
use std::io::Read;
use std::path::Path;

struct Config {
    api_key: String,
    unit: Unit,
}

impl Config {
    fn new() -> Config {
        Config {
            api_key: String::new(),
            unit: Unit::Celsius,
        }
    }

    fn read_config(&mut self) {
        let path = "config.txt";
        let mut file = File::open(path).expect("Unable to open config file");
        let mut contents = String::new();
        file.read_to_string(&mut contents).expect("Unable to read config file");

        for line in contents.lines() {
            let mut parts = line.split('=');
            let key = parts.next().unwrap();
            let value = parts.next().unwrap();

            match key.trim() {
                "api_key" => self.api_key = value.trim().to_string(),
                "unit" => {
                    self.unit = Unit::from_str(value.trim());
                }
                _ => println!("Unexpected key in config file: {}", key),
            }
        }
    }
}

Step 5: Display Help Message

You can now test the CLI with:

cargo run -- --help

This will display:

Usage: weather-cli [OPTIONS]

A command-line weather tool

Options:
  -u, --unit=UNIT    Unit of measurement [C for Celsius, F for Fahrenheit] [default: C]
  -h, --help         Print help information
  -V, --version      Print version information

Next Steps

  1. Modify API Requests: Update your API request logic to include the unit parameter based on the selected unit.
  2. Adjust Display Formatting: Modify how weather data is displayed based on the selected unit.
  3. Update Default Configuration: Ensure your default configuration file includes the unit setting.

What to Read More About

  1. Clap Documentation
  2. Rust Command-Line Parsing
  3. Rust Enums

This implementation provides a clean way to handle different units of measurement while maintaining good code structure and error handling.

Adding Support for Different Units of Measurement in Rust Weather CLI Tool

Mục tiêu: Enhancing the Rust Weather CLI Tool to support different units of measurement (Celsius and Fahrenheit) through command-line options, API integration, and error handling.


Adding Support for Different Units of Measurement in Rust Weather CLI Tool

Now that we’ve built the foundation of our Weather CLI Tool, let’s enhance it by adding support for different units of measurement. This feature will allow users to choose between Celsius and Fahrenheit, making the tool more user-friendly.

Step 1: Add a Command-Line Option for Units

We’ll start by modifying our command-line argument parsing to include an option for units. We’ll use the clap crate for this purpose.

use clap::{App, Arg};

// Inside your main function
let matches = App::new("weather-cli")
    .version("1.0")
    .author("Your Name")
    .about("A command-line tool for weather information")
    .arg(
        Arg::with_name("location")
            .help("The location for which to fetch the weather")
            .required(true)
            .index(1),
    )
    .arg(
        Arg::with_name("unit")
            .help("The unit of measurement to use [C/F]")
            .short("u")
            .long("unit")
            .default_value("C")
            .possible_values(&["C", "F"]),
    )
    .get_matches();

This code adds a new command-line option -u or --unit that accepts either “C” or “F” with a default value of “C”.

Step 2: Modify the API Request

Next, we’ll modify our API request to include the unit parameter based on the user’s selection. The OpenWeatherMap API supports this through the units parameter.

let unit = matches.value_of("unit").unwrap_or("C");
let api_key = get_api_key(); // Assume this function exists from previous steps
let location = matches.value_of("location").unwrap();

let url = format!(
    "http://api.openweathermap.org/data/2.5/weather?q={}&units={}&appid={}",
    location, unit, api_key
);

This code constructs the API URL with the selected unit.

Step 3: Parse and Display the Weather Information

Now, let’s update our response parsing and display logic to handle the different units appropriately.

match reqwest::get(&url) {
    Ok(response) => {
        let status = response.status();
        if status.is_success() {
            let weather_data = response.json::<WeatherData>().unwrap();

            println!("Weather in {}: ", weather_data.name);
            println!("Temperature: {}°{}", weather_data.main.temp, get_unit_symbol(unit));
            println!("Condition: {}", weather_data.weather[0].description);
            println!("Humidity: {}%", weather_data.main.humidity);
        } else {
            eprintln!("Failed to fetch weather data. Status: {}", status);
        }
    }
    Err(e) => eprintln!("Error making request: {}", e),
}

fn get_unit_symbol(unit: &str) -> String {
    match unit {
        "C" => String::from("C"),
        "F" => String::from("F"),
        _ => String::from("C"), // Default to Celsius
    }
}

This code updates the display to include the appropriate unit symbol based on the user’s selection.

Step 4: Error Handling

Let’s add error handling to ensure that only valid units are accepted and that the API request fails gracefully if an invalid unit is provided.

let unit = matches.value_of("unit").unwrap_or("C");

match unit {
    "C" | "F" => (),
    _ => {
        eprintln!("Invalid unit specified. Defaulting to Celsius.");
        unit = "C";
    }
}

This code ensures that even if an invalid unit is provided, the tool defaults to Celsius and informs the user.

Step 5: Testing

Now that we’ve implemented unit support, let’s test the tool with both units.

cargo run -- "London" --unit C
cargo run -- "London" --unit F

These commands should display the weather in London in Celsius and Fahrenheit respectively.

Next Steps

Now that we’ve added unit support, you can proceed to implement additional features like:

  1. Location Validation: Ensure that the provided location exists and is supported by the API.
  2. Configuration Options: Allow users to set default units and locations in a configuration file.
  3. Multiple Weather Providers: Add support for different weather APIs to provide more reliable data.

Further Reading

This enhancement makes our Weather CLI Tool more versatile and user-friendly, allowing users to choose their preferred unit of measurement.

Adjusting Display Formatting Based on Selected Unit

Mục tiêu: Adjusting the display formatting of weather data to match the selected unit system for a better user experience.


Adjusting Display Formatting Based on Selected Unit

Now that we have added support for different units of measurement, let’s focus on adjusting the display formatting to provide a better user experience. The goal is to ensure that the weather information is displayed in a user-friendly format that matches the selected unit system.

Understanding the Requirements

  • Temperature Display: Depending on whether the user selected Celsius or Fahrenheit, we need to format the temperature values accordingly.
  • Wind Speed Display: Similarly, wind speed should be displayed in appropriate units (e.g., km/h or mph).
  • Consistency: The display should maintain consistent formatting throughout, making it easy for users to read and understand.

Approach

  1. Create a Unit Enumeration: We’ll create an enumeration to represent the different units of measurement. This will make our code cleaner and more maintainable.
  2. Modify the Display Function: We’ll update our display function to take the unit as a parameter and format the values accordingly.
  3. Helper Functions: We’ll create helper functions to convert and format numerical values based on the selected unit.

Implementation

Let’s implement these changes step by step.

// Add this enumeration to represent units of measurement
#[derive(Debug, PartialEq, Eq)]
pub enum Unit {
    Celsius,
    Fahrenheit,
}

// Add this helper function to convert temperature
fn convert_temperature(temp: f64, from: Unit, to: Unit) -> f64 {
    match (from, to) {
        (Unit::Celsius, Unit::Fahrenheit) => (temp * 9.0/5.0) + 32.0,
        (Unit::Fahrenheit, Unit::Celsius) => (temp - 32.0) * 5.0/9.0,
        _ => temp, // Same unit, no conversion needed
    }
}

// Add this helper function to format numerical values with appropriate units
fn format_value(value: f64, unit: Unit, is_temperature: bool) -> String {
    let formatted_value = format!("{:.1}", value);
    match unit {
        Unit::Celsius => {
            if is_temperature {
                format!("{}°C", formatted_value)
            } else {
                format!("{} km/h", formatted_value)
            }
        }
        Unit::Fahrenheit => {
            if is_temperature {
                format!("{}°F", formatted_value)
            } else {
                format!("{} mph", formatted_value)
            }
        }
    }
}

// Modify your display function to accept the unit parameter
fn display_weather_data(weather_data: &WeatherData, unit: Unit) {
    let temperature = weather_data.temperature;
    let feels_like = weather_data.feels_like;
    let wind_speed = weather_data.wind_speed;

    // Format temperature values
    let formatted_temp = format_value(temperature, unit, true);
    let formatted_feels_like = format_value(feels_like, unit, true);

    // Format wind speed
    let formatted_wind_speed = format_value(wind_speed, unit, false);

    println!("Current Temperature: {}", formatted_temp);
    println!("Feels like: {}", formatted_feels_like);
    println!("Wind Speed: {}", formatted_wind_speed);
}

Explanation

  1. Unit Enumeration: We defined an enumeration Unit with variants Celsius and Fahrenheit. This makes it easy to handle different unit systems in a type-safe manner.
  2. Temperature Conversion: The convert_temperature function handles the conversion between Celsius and Fahrenheit. This is essential for ensuring that the displayed values are accurate regardless of the user’s preference.
  3. Value Formatting: The format_value function takes a numerical value, the selected unit, and a flag indicating whether it’s a temperature value. It returns a string with the value formatted to one decimal place and the appropriate unit symbol.
  4. Display Function: The display_weather_data function now takes the selected unit as a parameter. It uses the helper functions to format the temperature and wind speed values before printing them.

Example Usage

fn main() {
    // Assuming you have parsed the unit from command-line arguments
    let selected_unit = Unit::Fahrenheit; // or Unit::Celsius

    // Sample weather data
    let weather_data = WeatherData {
        temperature: 25.0,    // Assuming original data is in Celsius
        feels_like: 26.5,
        wind_speed: 15.0,
    };

    display_weather_data(&weather_data, selected_unit);
}

Next Steps

  • Enhancement: Consider adding support for other units of measurement like Kelvin for temperature or knots for wind speed.
  • Localization: You could further enhance the tool by localizing the unit symbols and formatting based on the user’s locale.

Further Reading

This implementation ensures that the weather data is displayed in a user-friendly format that matches the selected unit system, providing a seamless experience for users regardless of their preference.