pnc-engine 0.1.0

Core engine for PNC: arbitrary-precision RPN evaluation, units, and persistence
Documentation
use crate::error::PncError;
use crate::value::Value;
use num_bigint::BigInt;
use num_rational::BigRational;
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum UnitId {
    Celsius,
    Fahrenheit,
    Kelvin,
    Unknown(String),
}

impl fmt::Display for UnitId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            UnitId::Celsius => write!(f, "c"),
            UnitId::Fahrenheit => write!(f, "f"),
            UnitId::Kelvin => write!(f, "k"),
            UnitId::Unknown(s) => write!(f, "{}", s),
        }
    }
}

fn to_rational(v: &Value) -> Result<BigRational, PncError> {
    match v {
        Value::Int(n) => Ok(BigRational::from_integer(n.clone())),
        Value::Rational(r) => Ok(r.clone()),
        _ => Err(PncError::InvalidUnitConversion),
    }
}

fn kelvin_offset() -> BigRational {
    BigRational::new(BigInt::from(27315), BigInt::from(100))
}

fn to_kelvin(value: &Value) -> Result<BigRational, PncError> {
    match value {
        Value::Unit { number, unit } => {
            let n = to_rational(number)?;
            match unit {
                UnitId::Celsius => Ok(n + kelvin_offset()),
                UnitId::Fahrenheit => Ok((n - BigInt::from(32))
                    * BigRational::new(BigInt::from(5), BigInt::from(9))
                    + kelvin_offset()),
                UnitId::Kelvin => Ok(n),
                _ => Err(PncError::InvalidUnitConversion),
            }
        }
        _ => Err(PncError::InvalidUnitConversion),
    }
}

fn rational_to_value(r: BigRational) -> Value {
    if r.denom() == &BigInt::from(1) {
        Value::Int(r.numer().clone())
    } else {
        Value::Rational(r)
    }
}

fn from_kelvin(k: BigRational, target: UnitId) -> Value {
    match target {
        UnitId::Celsius => Value::Unit {
            number: Box::new(rational_to_value(k - kelvin_offset())),
            unit: target,
        },
        UnitId::Fahrenheit => {
            let f = (k - kelvin_offset()) * BigRational::new(BigInt::from(9), BigInt::from(5))
                + BigInt::from(32);
            Value::Unit {
                number: Box::new(rational_to_value(f)),
                unit: target,
            }
        }
        UnitId::Kelvin => Value::Unit {
            number: Box::new(rational_to_value(k)),
            unit: target,
        },
        UnitId::Unknown(_) => Value::Unit {
            number: Box::new(rational_to_value(k)),
            unit: target,
        },
    }
}

pub fn convert(value: &Value, target: UnitId) -> Result<Value, PncError> {
    let k = to_kelvin(value)?;
    Ok(from_kelvin(k, target))
}