#[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)
}
}