aspect-ratio-cli 0.1.3

A simple Rust library to calculate aspect ratio
use clap::CommandFactory;
use clap::Parser;
use std::error::Error;
use std::fmt;

/// Simple program to calculate the aspect ratio of a given width and height
///
/// ## Examples
/// ```sh
/// aspect-ratio 1920 1080
/// aspect-ratio 1920x1080
/// aspect-ratio 1920:1080
/// ```
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Width and height, e.g. 1920 1080 or 1920x1080 or 1920:1080
    #[arg(required = true)]
    dims: Vec<String>,
}

mod aspect_ratio {
    /// Function to calculate the greatest common divisor (GCD) of two numbers
    pub fn gcd(mut a: u32, mut b: u32) -> u32 {
        while b != 0 {
            let temp: u32 = b;
            b = a % b;
            a = temp;
        }
        a
    }

    /// Function to calculate the aspect ratio of a given width and height
    pub fn get_aspect_ratio(width: u32, height: u32) -> (u32, u32) {
        if width == 0 || height == 0 {
            return (width, height);
        }
        let divisor: u32 = gcd(width, height);
        (width / divisor, height / divisor)
    }
}

#[derive(Debug)]
/// Custom error type for parsing dimensions
enum ParseError {
    InvalidFormat,
    InvalidNumbers,
    NonPositiveNumbers,
}

/// Implementing the Display trait for ParseError
impl fmt::Display for ParseError {
    /// This trait is used to convert the error into a human-readable string
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ParseError::InvalidFormat => write!(f, "Invalid input format"),
            ParseError::InvalidNumbers => write!(f, "Invalid numbers provided"),
            ParseError::NonPositiveNumbers => {
                write!(f, "Width and height must be positive numbers")
            }
        }
    }
}

/// Implementing the Error trait for ParseError
impl Error for ParseError {}

/// Function to parse the command line arguments and extract width and height
fn parse_dimensions(args: &[String]) -> Result<(u32, u32), ParseError> {
    match args {
        [input] => {
            let input: &str = input.as_str();
            let (w, h) = if let Some((w, h)) = input.split_once(['x', ':']) {
                (w.trim(), h.trim())
            } else {
                return Err(ParseError::InvalidFormat);
            };
            let width: u32 = w.parse().map_err(|_| ParseError::InvalidNumbers)?;
            let height: u32 = h.parse().map_err(|_| ParseError::InvalidNumbers)?;
            if width == 0 || height == 0 {
                return Err(ParseError::NonPositiveNumbers);
            }
            Ok((width, height))
        }
        [w, h] => {
            let width: u32 = w.parse().map_err(|_| ParseError::InvalidNumbers)?;
            let height: u32 = h.parse().map_err(|_| ParseError::InvalidNumbers)?;
            if width == 0 || height == 0 {
                return Err(ParseError::NonPositiveNumbers);
            }
            Ok((width, height))
        }
        _ => Err(ParseError::InvalidFormat),
    }
}

/// Main function to parse command line arguments and calculate aspect ratio
fn main() {
    let args: Args = Args::parse();

    if let Some(_shell) = std::env::args()
        .nth(1)
        .filter(|a: &String| a == "completions")
    {
        use clap_complete::{generate, Shell};
        use std::io;

        let mut cmd: clap::Command = Args::command();
        let shell_name: String = std::env::args()
            .nth(2)
            .unwrap_or_else(|| "bash".to_string());
        let shell: Shell = match shell_name.as_str() {
            "bash" => Shell::Bash,
            "zsh" => Shell::Zsh,
            "fish" => Shell::Fish,
            "powershell" => Shell::PowerShell,
            "elvish" => Shell::Elvish,
            _ => {
                eprintln!("Unsupported shell: {}", shell_name);
                std::process::exit(1);
            }
        };
        generate(shell, &mut cmd, "aspect-ratio", &mut io::stdout());
        return;
    }

    match parse_dimensions(&args.dims) {
        Ok((width, height)) => {
            let (w, h) = aspect_ratio::get_aspect_ratio(width, height);
            println!("{}:{}", w, h);
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            Args::command().print_help().unwrap();
            println!();
            std::process::exit(1);
        }
    }
}