big-rational-str 0.1.7

Format and parse BigRationals to/from decimal numbers
Documentation
#[cfg(test)]
mod tests;

use std::borrow::Cow;
use std::collections::HashMap;
use std::str::FromStr;
use num::{BigRational, FromPrimitive, BigInt, Zero, One, Signed, Num};
use num::bigint::ParseBigIntError;
use num::rational::ParseRatioError;
use regex::{Regex};

#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    #[error("invalid BigRational string: {0}")]
    InvalidPattern(String),
    #[error(transparent)]
    ParseRatio(#[from] ParseRatioError),
    #[error(transparent)]
    ParseBigInt(#[from] ParseBigIntError),
}

lazy_static::lazy_static! {
    static ref TEN: BigInt = BigInt::from_u32(10).unwrap();
    static ref NINE: BigInt = BigInt::from_u32(9).unwrap();
    static ref REGEX: Regex = Regex::new(r"^(?P<neg>-)?(?P<int>\d+)(\.(?P<frac>\d*)(\((?P<repeat>\d+)\))?)?$").unwrap();
}

pub fn big_rational_to_string(n: BigRational) -> String {
    big_rational_ref_to_string(&n)
}

pub fn big_rational_ref_to_string(n: &BigRational) -> String {
    let abs_n = if n.is_negative() {
        Cow::Owned(n.abs())
    } else {
        Cow::Borrowed(n)
    };
    let mut fraction = String::new();
    let mut map = HashMap::new();
    let mut rem = abs_n.numer() % abs_n.denom();
    while !rem.is_zero() && !map.contains_key(&rem) {
        map.insert(rem.clone(), fraction.len());
        rem *= TEN.clone();
        fraction.push_str(&(rem.clone() / abs_n.denom()).to_string());
        rem %= abs_n.denom();
    }
    let mut output = if n.is_negative() {
        "-".to_owned()
    } else {
        String::new()
    };
    output.push_str(&(abs_n.numer() / abs_n.denom()).to_string());
    if rem.is_zero() {
        if !fraction.is_empty() {
            return format!("{output}.{fraction}");
        }
    } else {
        return format!("{output}.{}({})", &fraction[..map[&rem]], &fraction[map[&rem]..]);
    }
    output
}

pub fn str_to_big_rational(string: &str) -> Result<BigRational, ParseError> {
    if string.contains("/") {
        let a = BigRational::from_str(string)?;
        return Ok(a);
    }
    match REGEX.captures(string) {
        Some(captures) => {
            let neg = captures.name("neg").is_some();
            let int = &captures["int"];
            let fraction = captures.name("frac").map(|a| a.as_str());
            let repeating = captures.name("repeat").map(|a| a.as_str());

            Ok(match fraction {
                None => {
                    let int = BigRational::from_str(int)?;
                    if neg {
                        -int
                    } else {
                        int
                    }
                }
                Some(fraction) => match repeating {
                    None => frac(neg, int, fraction)?,
                    Some(repeating) => repeat(neg, int, fraction, repeating)?
                }
            })
        }
        None => Err(ParseError::InvalidPattern(string.to_owned()))
    }
}


fn frac(neg: bool, integer: &str, fractional: &str) -> Result<BigRational, ParseBigIntError> {
    let mut a = BigInt::one();
    for _ in 0..fractional.len() {
        a *= &*TEN;
    }
    let b = BigRational::new(BigInt::from_str(fractional)?, a) + BigInt::from_str(integer)?;
    Ok(if neg {
        -b
    } else {
        b
    })
}

fn repeat(neg: bool, integer: &str, fractional: &str, repeating: &str) -> Result<BigRational, ParseError> {
    let mut a = BigRational::one();
    for _ in 0..fractional.len() {
        a *= &*TEN;
    }
    let b = match BigRational::from_str(fractional) {
        Ok(a) => a,
        Err(_) => BigRational::zero()
    } / &a;
    let mut c = NINE.clone();
    for _ in 1..repeating.len() {
        c *= &*TEN;
        c += &*NINE;
    }
    let d = BigRational::from_str(repeating)? / c / a;
    let e = b + d + BigInt::from_str(integer)?;
    Ok(if neg {
        -e
    } else {
        e
    })
}

pub trait BigRationalExt: Sized {
    fn from_dec_str(str: &str) -> Result<Self, ParseError>;
    fn to_dec_string(&self) -> String;
}

impl BigRationalExt for BigRational {
    fn from_dec_str(str: &str) -> Result<Self, ParseError> {
        str_to_big_rational(str)
    }

    fn to_dec_string(&self) -> String {
        big_rational_ref_to_string(self)
    }
}