use num_rational::BigRational;
use num_traits::ToPrimitive;
use pnc_engine::Value;
#[derive(Debug, Clone)]
pub enum PlotUnit {
AllUnitless,
SingleUnit(pnc_engine::UnitId),
Temperature,
Incompatible,
}
#[derive(Debug)]
pub enum PlotSkip {
NoNumeric,
Conversion,
}
#[derive(Debug, Clone)]
pub struct Series {
pub points: Vec<usize>,
pub label: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PlotCache {
pub series: Series,
pub unit_class: PlotUnit,
pub x_min: usize,
pub x_max: usize,
pub y_min_idx: usize,
pub y_max_idx: usize,
pub y_min: num_rational::BigRational,
pub y_max: num_rational::BigRational,
pub y_min_label: String,
pub y_max_label: String,
pub width: u16,
pub height: u16,
}
pub fn unit_class(units: &[Option<pnc_engine::UnitId>]) -> PlotUnit {
if units.is_empty() {
return PlotUnit::Incompatible;
}
let mut has_unit = false;
let mut first_unit: Option<pnc_engine::UnitId> = None;
let mut all_same = true;
let mut has_temperature = false;
let mut has_unknown = false;
for u in units {
match u {
None => {
if has_unit {
return PlotUnit::Incompatible;
}
}
Some(unit) => {
if !has_unit {
first_unit = Some(unit.clone());
has_unit = true;
} else if Some(unit.clone()) != first_unit {
all_same = false;
}
match unit.dimension() {
Some(pnc_engine::Dimension::Temperature) => has_temperature = true,
None => has_unknown = true,
_ => {}
}
}
}
}
if !has_unit {
return PlotUnit::AllUnitless;
}
if all_same {
return PlotUnit::SingleUnit(first_unit.unwrap());
}
if has_temperature && !has_unknown {
let all_temp = units.iter().all(|u| match u {
Some(unit) => unit.dimension() == Some(pnc_engine::Dimension::Temperature),
None => false,
});
if all_temp {
return PlotUnit::Temperature;
}
}
PlotUnit::Incompatible
}
pub fn series_from_stack(stack: &[Value]) -> Result<Series, PlotSkip> {
if stack.is_empty() {
return Err(PlotSkip::NoNumeric);
}
let points: Vec<usize> = (0..stack.len()).collect();
Ok(Series {
points,
label: None,
})
}
pub fn normalize_value(value: &Value, class: &PlotUnit) -> Result<BigRational, PlotSkip> {
match value {
Value::Int(n) => Ok(BigRational::from_integer(n.clone())),
Value::Rational(r) => Ok(r.clone()),
Value::Unit { number, unit: _ } => {
match class {
PlotUnit::Temperature => {
pnc_engine::unit::convert(value, pnc_engine::UnitId::Kelvin)
.map(|v| match v {
Value::Unit { number, .. } => match number.as_ref() {
Value::Int(n) => BigRational::from_integer(n.clone()),
Value::Rational(r) => r.clone(),
_ => BigRational::from_integer(num_bigint::BigInt::from(0)),
},
Value::Int(n) => BigRational::from_integer(n.clone()),
Value::Rational(r) => r.clone(),
})
.map_err(|_| PlotSkip::Conversion)
}
_ => match number.as_ref() {
Value::Int(n) => Ok(BigRational::from_integer(n.clone())),
Value::Rational(r) => Ok(r.clone()),
_ => Err(PlotSkip::NoNumeric),
},
}
}
}
}
pub fn to_f64_saturated(v: &BigRational) -> Option<f64> {
let f = v.to_f64()?;
if f.is_finite() { Some(f) } else { None }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plot_types_constructible() {
let s = Series {
points: vec![0, 1],
label: Some("test".to_string()),
};
assert_eq!(s.points.len(), 2);
let u = PlotUnit::AllUnitless;
match u {
PlotUnit::AllUnitless => {}
_ => panic!("expected AllUnitless"),
}
let u2 = PlotUnit::SingleUnit(pnc_engine::UnitId::Celsius);
match u2 {
PlotUnit::SingleUnit(id) => {
let _ = id.symbol();
}
_ => panic!("expected SingleUnit"),
}
let skip = PlotSkip::NoNumeric;
match skip {
PlotSkip::NoNumeric => {}
_ => panic!("expected NoNumeric"),
}
let skip = PlotSkip::Conversion;
match skip {
PlotSkip::Conversion => {}
_ => {}
}
let cache = PlotCache {
series: s,
unit_class: PlotUnit::AllUnitless,
x_min: 0,
x_max: 1,
y_min_idx: 0,
y_max_idx: 1,
y_min: BigRational::from_integer(num_bigint::BigInt::from(0)),
y_max: BigRational::from_integer(num_bigint::BigInt::from(1)),
y_min_label: "0".to_string(),
y_max_label: "1".to_string(),
width: 10,
height: 5,
};
assert_eq!(cache.width, 10);
}
#[test]
fn unit_class_basic() {
let uc = unit_class(&[None, None]);
match uc {
PlotUnit::AllUnitless => {}
_ => panic!("expected AllUnitless"),
}
}
#[test]
fn series_from_stack_basic() {
use pnc_engine::Value;
let stack = vec![Value::Int(1.into())];
let s = series_from_stack(&stack).unwrap();
assert_eq!(s.points.len(), 1);
}
#[test]
fn normalize_value_basic() {
use pnc_engine::Value;
let v = Value::Int(42.into());
let r = normalize_value(&v, &PlotUnit::AllUnitless).unwrap();
assert_eq!(r, BigRational::from_integer(num_bigint::BigInt::from(42)));
}
#[test]
fn to_f64_saturated_basic() {
let v = BigRational::from_integer(num_bigint::BigInt::from(42));
assert_eq!(to_f64_saturated(&v), Some(42.0));
}
}