factor-rs 1.2.1

A command-line program for listing the prime factors of a number or fraction
Documentation
use std::{
    env,
    fmt,
    io::{self, Write as _},
    num::ParseIntError,
    str,
    sync::atomic::{AtomicBool, Ordering},
};
use radixal::IntoDigits;
use thiserror::Error;
use factor_rs::Fraction;

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Arg
{
    Integer { sign: bool, val: u128 },
    Fraction { sign: bool, num: u128, denom: u128 },
    Decimal { sign: bool, mantissa: u128, exp: u8 },
}

fn main()
{
    for arg in parse_args()
    {
        print!("{arg} = ");
        io::stdout().flush().expect("failed to flush stdout");

        let frac: Fraction = arg.into();
        let mut factors = frac.factorize();

        let mut reduced = Fraction::ONE;
        let mut print_reduced = false;

        if let Some(fact) = factors.next()
        {
            print!("{fact}");

            reduced *= Into::<Fraction>::into(fact);
            if fact.has_exponent() { print_reduced = true }

            for fact in factors
            {
                print!(" * {fact}");

                reduced *= Into::<Fraction>::into(fact);
                print_reduced = true;
            }
        }

        if print_reduced && matches!(arg, Arg::Fraction { .. } | Arg::Decimal { .. })
        {
            if frac != reduced { print!(" = {reduced}") }
            else
            {
                match reduced
                {
                    Fraction { num: 0, denom: 0, .. } => print!(" = 0/0"),
                    Fraction { sign: false, num: 2.., denom: 0 } => print!(" = 1/0"),
                    Fraction { sign: true, num: 2.., denom: 0 } => print!(" = -1/0"),
                    _ => (),
                }
            }
        }

        println!();
    }
}

fn parse_args() -> Vec<Arg>
{
    let print_try_help = AtomicBool::new(false);
    let mut print_help = false;
    let mut print_version = false;
    let mut seen_double_dash = false;

    let res = env::args_os()
        .enumerate()
        .skip(1)
        .filter_map(|(i, s)| s
            .into_string()
            .map_err(|_|
            {
                eprintln!("error: argument {i} is not valid unicode");
                print_try_help.store(true, Ordering::Relaxed);
            })
            .ok()
        )
        .filter(|s| seen_double_dash || match s.trim().strip_prefix("--")
        {
            Some("help") => { print_help = true; false },
            Some("version") => { print_version = true; false },
            Some("") => { seen_double_dash = true; false },
            Some(opt) =>
            {
                eprintln!("error: unknown option '{opt}'");
                print_try_help.store(true, Ordering::Relaxed);
                false
            },
            None => true,
        })
        .filter_map(|s| s
            .parse()
            .map_err(|e|
            {
                eprintln!("error: argument '{s}' is not a valid number: {e}");
                print_try_help.store(true, Ordering::Relaxed);
            })
            .ok()
        )
        .collect();

    if print_version { eprintln!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")) }

    if print_help
    {
        eprintln!("{}: {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_DESCRIPTION"));
        eprintln!("usage: {} [NUMBERS]...",
        {
            #[cfg(windows)] { concat!(env!("CARGO_BIN_NAME"), ".exe") }
            #[cfg(not(windows))] { env!("CARGO_BIN_NAME") }
        });
        eprintln!("options:");
        eprintln!("  --help     Print help");
        eprintln!("  --version  Print version");
    }
    else if print_try_help.into_inner() { eprintln!("for more information, try '--help'") }

    res
}

impl str::FromStr for Arg
{
    type Err = ArgError;
    fn from_str(s: &str) -> Result<Arg, ArgError> { Arg::try_from(s) }
}

impl TryFrom<&str> for Arg
{
    type Error = ArgError;

    fn try_from(s: &str) -> Result<Arg, ArgError>
    {
        fn pair<T>(mut iter: impl Iterator<Item = T>) -> Result<(T, T), usize>
        {
            let Some(first) = iter.next() else { return Err(0) };
            let Some(second) = iter.next() else { return Err(1) };

            let rest = iter.count();
            if rest != 0 { return Err(2 + rest); }

            Ok((first, second))
        }

        match pair(s.split('/'))
        {
            Ok((num, denom)) => match pair(s.split('.'))
            {
                Ok(_) => Err(ArgError::DecimalInFraction),
                Err(0 | 1) => parse_frac(num, denom),
                Err(n) => Err(ArgError::TooManyDecimals(n - 1)),
            },
            Err(0 | 1) => match pair(s.split('.'))
            {
                Ok((whole, decimal)) => parse_decimal(whole, decimal),
                Err(0 | 1) => parse_int(s),
                Err(n) => Err(ArgError::TooManyDecimals(n - 1)),
            },
            Err(n) => Err(ArgError::TooManyFractions(n - 1)),
        }
    }
}

fn parse_int(val: &str) -> Result<Arg, ArgError>
{
    let (sign, val) = parse_signed(val.trim()).map_err(ArgError::IntegerInvalid)?;

    Ok(Arg::Integer { sign, val })
}

fn parse_frac(num: &str, denom: &str) -> Result<Arg, ArgError>
{
    let (mut num_sign, num) = parse_signed(num.trim()).map_err(ArgError::NumeratorInvalid)?;
    let (mut denom_sign, denom) = parse_signed(denom.trim()).map_err(ArgError::DenominatorInvalid)?;

    if num == 0 { num_sign = false }
    if denom == 0 { denom_sign = false }
    let sign = num_sign ^ denom_sign;

    if denom == 1 { Ok(Arg::Integer { sign, val: num }) }
    else { Ok(Arg::Fraction { sign, num, denom }) }
}

fn parse_decimal(whole: &str, decimal: &str) -> Result<Arg, ArgError>
{
    let (sign, whole) = match whole.trim_start()
    {
        "" => (false, 0),
        "-" => (true, 0),
        s => parse_signed(s).map_err(ArgError::DecimalInvalid)?,
    };

    let decimal = decimal.trim_end().trim_end_matches('0');
    let exp = decimal.len();
    let decimal = if decimal.is_empty() { 0 }
    else { decimal.parse().map_err(ArgError::DecimalInvalid)? };

    if exp > 38 { return Err(ArgError::DecimalExpTooLarge(exp)) }
    let mantissa = whole
        .checked_mul(num::pow(10, exp))
        .and_then(|n| n.checked_add(decimal))
        .ok_or(ArgError::MantissaTooLarge)?;

    if exp == 0 { Ok(Arg::Integer { sign, val: mantissa }) }
    else { Ok(Arg::Decimal { sign, mantissa, exp: exp as u8 }) }
}

fn parse_signed(s: &str) -> Result<(bool, u128), ParseIntError>
{
    let mut sign = false;

    let s = if let Some(val) = s.strip_prefix('-') { sign = true; val } else { s };
    Ok((sign, s.parse()?))
}

#[derive(Error, Debug, Clone)]
enum ArgError
{
    #[error("cannot contain multiple fraction separators (expected 1, found {0})")]
    TooManyFractions(usize),
    #[error("cannot contain multiple decimal points (expected 1, found {0})")]
    TooManyDecimals(usize),
    #[error("decimal numbers are not allowed in fractions")]
    DecimalInFraction,
    #[error("decimal numbers cannot have more than 38 digits (found {0})")]
    DecimalExpTooLarge(usize),
    #[error("decimal number mantissa too large to fit in target type")]
    MantissaTooLarge,
    #[error("numerator invalid: {0}")]
    NumeratorInvalid(ParseIntError),
    #[error("denominator invalid: {0}")]
    DenominatorInvalid(ParseIntError),
    #[error("decimal number invalid: {0}")]
    DecimalInvalid(ParseIntError),
    #[error("integer invalid: {0}")]
    IntegerInvalid(ParseIntError),
}

impl From<Arg> for Fraction
{
    fn from(arg: Arg) -> Fraction
    {
        match arg
        {
            Arg::Integer { sign, val } => Fraction { sign, num: val, denom: 1 },
            Arg::Fraction { sign, num, denom } => Fraction { sign, num, denom },
            Arg::Decimal { sign, mantissa, exp } => Fraction
            {
                sign,
                num: mantissa,
                denom: num::pow(10, exp as usize),
            },
        }
    }
}

impl fmt::Display for Arg
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
    {
        match self
        {
            Arg::Integer { val: 0, .. } => write!(f, "0"),
            Arg::Integer { sign: false, val } => write!(f, "{val}"),
            Arg::Integer { sign: true, val } => write!(f, "-{val}"),
            Arg::Fraction { num: 0, denom: 0, .. } => write!(f, "0/0"),
            Arg::Fraction { sign: false, num, denom } => write!(f, "{num}/{denom}"),
            Arg::Fraction { sign: true, num, denom } => write!(f, "-{num}/{denom}"),
            Arg::Decimal { sign, mantissa, exp } => fmt_decimal(f, *sign, *mantissa, *exp),
        }
    }
}

fn fmt_decimal(f: &mut fmt::Formatter, sign: bool, mantissa: u128, exp: u8) -> fmt::Result
{
    let mut digits = mantissa
        .into_decimal_digits()
        .rev()
        .enumerate()
        .rev()
        .map(|(i, d)|
            (i as u8, char::from_digit(d as u32, 10).expect("invalid digit"))
        );

    let Some((i, digit)) = digits.next() else { return write!(f, "0.0") };
    if sign { write!(f, "-")? }

    if i < exp
    {
        write!(f, "0.")?;
        for _ in 0..(exp - i - 1) { write!(f, "0")? }
    }

    write!(f, "{digit}")?;
    let mut trailing_zero = if i == exp { write!(f, ".")?; true } else { false };

    for (i, digit) in digits
    {
        write!(f, "{digit}")?;
        trailing_zero = if i == exp { write!(f, ".")?; true } else { false };
    }

    if trailing_zero { write!(f, "0")? }

    Ok(())
}