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, Copy, PartialEq, Eq)]
pub enum Dimension {
Temperature,
Length,
Mass,
Time,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum UnitId {
Celsius,
Fahrenheit,
Kelvin,
Millimeter,
Centimeter,
Meter,
Kilometer,
Mile,
Foot,
Inch,
Yard,
Milligram,
Gram,
Kilogram,
Pound,
Ounce,
Millisecond,
Second,
Minute,
Hour,
Day,
Week,
Unknown(String),
}
impl fmt::Display for UnitId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.symbol())
}
}
impl UnitId {
pub fn symbol(&self) -> &str {
match self {
UnitId::Celsius => "c",
UnitId::Fahrenheit => "f",
UnitId::Kelvin => "k",
UnitId::Millimeter => "mm",
UnitId::Centimeter => "cm",
UnitId::Meter => "m",
UnitId::Kilometer => "km",
UnitId::Mile => "mi",
UnitId::Foot => "ft",
UnitId::Inch => "in",
UnitId::Yard => "yd",
UnitId::Milligram => "mg",
UnitId::Gram => "g",
UnitId::Kilogram => "kg",
UnitId::Pound => "lb",
UnitId::Ounce => "oz",
UnitId::Millisecond => "ms",
UnitId::Second => "s",
UnitId::Minute => "min",
UnitId::Hour => "h",
UnitId::Day => "d",
UnitId::Week => "wk",
UnitId::Unknown(s) => s,
}
}
pub fn from_symbol(s: &str) -> Option<UnitId> {
match s {
"c" => Some(UnitId::Celsius),
"f" => Some(UnitId::Fahrenheit),
"k" => Some(UnitId::Kelvin),
"mm" => Some(UnitId::Millimeter),
"cm" => Some(UnitId::Centimeter),
"m" => Some(UnitId::Meter),
"km" => Some(UnitId::Kilometer),
"mi" => Some(UnitId::Mile),
"ft" => Some(UnitId::Foot),
"in" => Some(UnitId::Inch),
"yd" => Some(UnitId::Yard),
"mg" => Some(UnitId::Milligram),
"g" => Some(UnitId::Gram),
"kg" => Some(UnitId::Kilogram),
"lb" => Some(UnitId::Pound),
"oz" => Some(UnitId::Ounce),
"ms" => Some(UnitId::Millisecond),
"s" => Some(UnitId::Second),
"min" => Some(UnitId::Minute),
"h" => Some(UnitId::Hour),
"d" => Some(UnitId::Day),
"wk" => Some(UnitId::Week),
_ => None,
}
}
pub fn dimension(&self) -> Option<Dimension> {
match self {
UnitId::Celsius | UnitId::Fahrenheit | UnitId::Kelvin => Some(Dimension::Temperature),
UnitId::Millimeter
| UnitId::Centimeter
| UnitId::Meter
| UnitId::Kilometer
| UnitId::Mile
| UnitId::Foot
| UnitId::Inch
| UnitId::Yard => Some(Dimension::Length),
UnitId::Milligram | UnitId::Gram | UnitId::Kilogram | UnitId::Pound | UnitId::Ounce => {
Some(Dimension::Mass)
}
UnitId::Millisecond
| UnitId::Second
| UnitId::Minute
| UnitId::Hour
| UnitId::Day
| UnitId::Week => Some(Dimension::Time),
UnitId::Unknown(_) => None,
}
}
pub(crate) fn factor(&self) -> BigRational {
match self {
UnitId::Celsius => BigRational::from_integer(BigInt::from(1)),
UnitId::Fahrenheit => BigRational::new(BigInt::from(5), BigInt::from(9)),
UnitId::Kelvin => BigRational::from_integer(BigInt::from(1)),
UnitId::Millimeter => BigRational::new(BigInt::from(1), BigInt::from(1000)),
UnitId::Centimeter => BigRational::new(BigInt::from(1), BigInt::from(100)),
UnitId::Meter => BigRational::from_integer(BigInt::from(1)),
UnitId::Kilometer => BigRational::from_integer(BigInt::from(1000)),
UnitId::Mile => BigRational::new(BigInt::from(1609344), BigInt::from(1000)),
UnitId::Foot => BigRational::new(BigInt::from(3048), BigInt::from(10000)),
UnitId::Inch => BigRational::new(BigInt::from(254), BigInt::from(10000)),
UnitId::Yard => BigRational::new(BigInt::from(9144), BigInt::from(10000)),
UnitId::Milligram => BigRational::new(BigInt::from(1), BigInt::from(1000000)),
UnitId::Gram => BigRational::new(BigInt::from(1), BigInt::from(1000)),
UnitId::Kilogram => BigRational::from_integer(BigInt::from(1)),
UnitId::Pound => BigRational::new(BigInt::from(45359237), BigInt::from(100000000)),
UnitId::Ounce => BigRational::new(BigInt::from(45359237), BigInt::from(1600000000)),
UnitId::Millisecond => BigRational::new(BigInt::from(1), BigInt::from(1000)),
UnitId::Second => BigRational::from_integer(BigInt::from(1)),
UnitId::Minute => BigRational::from_integer(BigInt::from(60)),
UnitId::Hour => BigRational::from_integer(BigInt::from(3600)),
UnitId::Day => BigRational::from_integer(BigInt::from(86400)),
UnitId::Week => BigRational::from_integer(BigInt::from(604800)),
UnitId::Unknown(_) => BigRational::from_integer(BigInt::from(1)),
}
}
pub(crate) fn offset(&self) -> BigRational {
match self {
UnitId::Celsius => BigRational::new(BigInt::from(27315), BigInt::from(100)),
UnitId::Fahrenheit => BigRational::new(BigInt::from(45967), BigInt::from(180)),
_ => BigRational::from_integer(BigInt::from(0)),
}
}
pub fn units_in_dimension(d: Dimension) -> &'static [UnitId] {
match d {
Dimension::Temperature => &[UnitId::Celsius, UnitId::Fahrenheit, UnitId::Kelvin],
Dimension::Length => &[
UnitId::Millimeter,
UnitId::Centimeter,
UnitId::Meter,
UnitId::Kilometer,
UnitId::Mile,
UnitId::Foot,
UnitId::Inch,
UnitId::Yard,
],
Dimension::Mass => &[
UnitId::Milligram,
UnitId::Gram,
UnitId::Kilogram,
UnitId::Pound,
UnitId::Ounce,
],
Dimension::Time => &[
UnitId::Millisecond,
UnitId::Second,
UnitId::Minute,
UnitId::Hour,
UnitId::Day,
UnitId::Week,
],
}
}
}
pub fn convert(value: &Value, target: UnitId) -> Result<Value, PncError> {
match value {
Value::Int(n) => {
return Ok(Value::Unit {
number: Box::new(Value::Int(n.clone())),
unit: target,
});
}
Value::Rational(r) => {
return Ok(Value::Unit {
number: Box::new(Value::Rational(r.clone())),
unit: target,
});
}
_ => {}
}
if let Value::Unit {
number,
unit: source,
} = value
{
if *source == target {
return Ok(value.clone());
}
if source.dimension().is_none() || target.dimension().is_none() {
return Err(PncError::InvalidUnitConversion);
}
if source.dimension() != target.dimension() {
return Err(PncError::InvalidUnitConversion);
}
let num = match number.as_ref() {
Value::Int(n) => BigRational::from_integer(n.clone()),
Value::Rational(r) => r.clone(),
_ => return Err(PncError::InvalidUnitConversion),
};
let base = num * source.factor() + source.offset();
let result = (base - target.offset()) / target.factor();
crate::ops::check_precision(&Value::Rational(result.clone()))
.map_err(|_| PncError::PrecisionBudgetExceeded)?;
let normalized = if result.denom() == &BigInt::from(1) {
Value::Int(result.numer().clone())
} else {
Value::Rational(result)
};
return Ok(Value::Unit {
number: Box::new(normalized),
unit: target,
});
}
Err(PncError::InvalidUnitConversion)
}