use cosmwasm_std::{Decimal256, Uint256};
use num_bigint::BigUint;
use num_rational::{Ratio, Rational64};
use num_traits::{FromBytes, ToBytes, ToPrimitive, Zero};
use std::fmt::{Debug, Display};
use std::str::FromStr;
use thiserror::Error as thiserrorError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fraction(Ratio<BigUint>);
impl Fraction {
pub fn new(numerator: BigUint, denominator: BigUint) -> Result<Self, FractionError> {
if denominator.is_zero() {
return Err(FractionError::ZeroDenominator);
}
Ok(Self::new_raw(numerator, denominator))
}
pub fn new_raw(numerator: BigUint, denominator: BigUint) -> Self {
Self(Ratio::new_raw(numerator, denominator))
}
pub fn into_raw(self) -> (BigUint, BigUint) {
self.0.into_raw()
}
pub fn from_decimal_string(decimal_str: &str) -> Result<Fraction, FractionError> {
let dec = Decimal256::from_str(decimal_str)?;
Ok(dec.into())
}
pub fn ratio(&self) -> &Ratio<BigUint> {
&self.0
}
pub fn numerator(&self) -> &BigUint {
self.ratio().numer()
}
pub fn denominator(&self) -> &BigUint {
self.ratio().denom()
}
pub fn reduced(self) -> Self {
Self(self.0.reduced())
}
pub fn to_human_precision(self, base_precision: u8, quote_precision: u8) -> Self {
let ratio = Ratio::new(
BigUint::from(10u64.pow(base_precision as u32)),
BigUint::from(10u64.pow(quote_precision as u32)),
)
.reduced();
self * ratio.into()
}
}
#[derive(Debug, thiserrorError, PartialEq)]
pub enum FractionError {
#[error("Cosmwasm Error: {0}")]
CosmwasmError(#[from] cosmwasm_std::StdError),
#[error("Numerator too large to fit in f64")]
NumeratorOverflow,
#[error("Denominator too large to fit in f64")]
DenominatorOverflow,
#[error("Denominator is zero")]
ZeroDenominator,
}
impl Display for Fraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.ratio(), f)
}
}
impl From<Fraction> for Ratio<BigUint> {
fn from(value: Fraction) -> Self {
value.0
}
}
impl From<Ratio<BigUint>> for Fraction {
fn from(value: Ratio<BigUint>) -> Self {
Self(value)
}
}
impl std::ops::Mul for Fraction {
type Output = Fraction;
fn mul(self, rhs: Self) -> Self::Output {
let self_ratio: Ratio<BigUint> = self.into();
let rhs_ratio: Ratio<BigUint> = rhs.into();
(self_ratio * rhs_ratio).into()
}
}
impl From<Rational64> for Fraction {
fn from(value: Rational64) -> Self {
let numerator = value.numer().unsigned_abs();
let denominator = value.denom().unsigned_abs();
Fraction::new_raw(BigUint::from(numerator), BigUint::from(denominator))
}
}
impl From<Fraction> for Decimal256 {
fn from(fraction: Fraction) -> Self {
let mut n_bytes = fraction.numerator().to_le_bytes();
n_bytes.resize(32, 0);
let numerator = Uint256::from_le_bytes(n_bytes.try_into().unwrap());
let mut d_bytes = fraction.denominator().to_le_bytes();
d_bytes.resize(32, 0);
let denominator = Uint256::from_le_bytes(d_bytes.try_into().unwrap());
Decimal256::from_ratio(numerator, denominator)
}
}
impl TryFrom<Fraction> for f64 {
type Error = FractionError;
fn try_from(value: Fraction) -> Result<Self, Self::Error> {
let numerator = value
.numerator()
.to_f64()
.ok_or(FractionError::NumeratorOverflow)?;
let denominator = value
.denominator()
.to_f64()
.ok_or(FractionError::DenominatorOverflow)?;
if denominator == 0.0 {
return Err(FractionError::ZeroDenominator);
}
Ok(numerator / denominator)
}
}
impl From<Decimal256> for Fraction {
fn from(value: Decimal256) -> Self {
let numerator = BigUint::from_le_bytes(&value.atomics().to_le_bytes());
let denominator = BigUint::from(10u8).pow(value.decimal_places());
Fraction::new_raw(numerator, denominator).reduced()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fraction_to_decimal256() {
let fraction = Fraction::new_raw(BigUint::from(100u8), BigUint::from(200u8));
let decimal: Decimal256 = fraction.into();
assert_eq!(decimal, Decimal256::percent(50));
let fraction = Fraction::new_raw(BigUint::from(3u8), BigUint::from(4u8));
let decimal: Decimal256 = fraction.into();
assert_eq!(decimal, Decimal256::percent(75));
}
#[test]
fn test_decimal256_to_fraction() {
let decimal = Decimal256::percent(50);
let fraction: Fraction = decimal.into();
assert_eq!(*fraction.numerator(), BigUint::from(1u8));
assert_eq!(*fraction.denominator(), BigUint::from(2u8));
let decimal = Decimal256::percent(75);
let fraction: Fraction = decimal.into();
assert_eq!(*fraction.numerator(), BigUint::from(3u8));
assert_eq!(*fraction.denominator(), BigUint::from(4u8));
}
#[test]
fn test_rational64_to_fraction() {
let rational = Rational64::new(3, 4);
let fraction: Fraction = rational.into();
assert_eq!(*fraction.numerator(), BigUint::from(3u8));
assert_eq!(*fraction.denominator(), BigUint::from(4u8));
let rational = Rational64::new(-5, 10);
let fraction: Fraction = rational.into();
assert_eq!(*fraction.numerator(), BigUint::from(1u8));
assert_eq!(*fraction.denominator(), BigUint::from(2u8));
}
#[test]
fn test_fraction_to_f64() {
let fraction = Fraction::new_raw(BigUint::from(3u8), BigUint::from(4u8));
let result: f64 = fraction
.try_into()
.expect("Failed to convert Fraction to f64");
assert_eq!(result, 0.75);
let fraction = Fraction::new_raw(BigUint::from(1u8), BigUint::from(3u8));
let result: f64 = fraction
.try_into()
.expect("Failed to convert Fraction to f64");
assert_eq!(result, 0.3333333333333333);
let fraction = Fraction::new_raw(BigUint::from(1u8), BigUint::from(0u8));
let result: Result<f64, FractionError> = fraction.try_into();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e, FractionError::ZeroDenominator);
}
}
#[test]
fn test_reduce() {
let price = Fraction::new_raw(BigUint::from(100u64), BigUint::from(10u64)).reduced();
assert_eq!(
price,
Fraction::new_raw(BigUint::from(10u64), BigUint::from(1u64))
)
}
#[test]
fn test_precision_changing() {
let price = Fraction::new_raw(BigUint::from(100u64), BigUint::from(1u64));
let base_precision = 2;
let quote_precision = 4;
assert_eq!(
price.to_human_precision(base_precision, quote_precision),
Fraction::new_raw(BigUint::from(1u64), BigUint::from(1u64))
);
}
}