use crate::context::Context;
use crate::number::{Dimension, Number};
use crate::numeric::{Digits, Numeric};
use crate::reply::{PropertyReply, SubstanceReply};
use crate::value::Show;
use std::collections::BTreeMap;
use std::iter::once;
use std::ops::{Add, Div, Mul};
use std::sync::Arc;
macro_rules! try_div {
($x:expr, $y:expr, $context:expr) => {
(&$x / &$y).ok_or_else(|| {
format!(
"Division by zero: <{}> / <{}>",
$x.show($context),
$y.show($context)
)
})?
};
}
#[derive(Debug, Clone)]
pub struct Property {
pub input: Number,
pub input_name: String,
pub output: Number,
pub output_name: String,
pub doc: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Properties {
pub name: String,
pub properties: BTreeMap<String, Property>,
}
#[derive(Debug, Clone)]
pub struct Substance {
pub amount: Number,
pub properties: Arc<Properties>,
}
pub enum SubstanceGetError {
Generic(String),
Conformance(Number, Number),
}
impl Substance {
pub fn rename(self, name: String) -> Substance {
Substance {
amount: self.amount,
properties: Arc::new(Properties {
name,
properties: self.properties.properties.clone(),
}),
}
}
pub fn get(&self, name: &str) -> Result<Number, SubstanceGetError> {
if self.amount.dimless() {
self.properties
.properties
.get(name)
.ok_or_else(|| {
SubstanceGetError::Generic(format!(
"No such property {} of {}",
name, self.properties.name
))
})
.map(|prop| {
(&(&self.amount * &prop.output).unwrap() / &prop.input)
.expect("Non-zero property")
})
} else {
for prop in self.properties.properties.values() {
if name == prop.output_name {
let input = (&prop.input / &self.amount)
.ok_or_else(|| SubstanceGetError::Generic("Division by zero".to_owned()))?;
if input.dimless() {
let res = (&prop.output / &input).ok_or_else(|| {
SubstanceGetError::Generic("Division by zero".to_owned())
})?;
return Ok(res);
} else {
return Err(SubstanceGetError::Conformance(
self.amount.clone(),
prop.input.clone(),
));
}
} else if name == prop.input_name {
let output = (&prop.output / &self.amount)
.ok_or_else(|| SubstanceGetError::Generic("Division by zero".to_owned()))?;
if output.dimless() {
let res = (&prop.input / &output).ok_or_else(|| {
SubstanceGetError::Generic("Division by zero".to_owned())
})?;
return Ok(res);
} else {
return Err(SubstanceGetError::Conformance(
self.amount.clone(),
prop.output.clone(),
));
}
}
}
Err(SubstanceGetError::Generic(format!(
"No such property {} of {}",
name, self.properties.name
)))
}
}
pub fn get_in_unit(
&self,
unit: Number,
context: &Context,
bottom_name: BTreeMap<String, isize>,
bottom_const: Numeric,
base: u8,
digits: Digits,
) -> Result<SubstanceReply, String> {
if self.amount.dimless() {
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: self
.properties
.properties
.iter()
.map(|(k, v)| {
let (input, output) = if v.input.dimless() {
let res = (&v.output * &self.amount).unwrap();
(None, try_div!(res, v.input, context))
} else {
(Some(v.input.clone()), v.output.clone())
};
let (input, output) = if output.unit != unit.unit {
if let Some(input) = input {
if input.unit == unit.unit {
(Some(output), input)
} else {
return Ok(None);
}
} else {
return Ok(None);
}
} else {
(input, output)
};
let output_show = context
.show(
&try_div!(output, unit, context),
&unit,
bottom_name.clone(),
bottom_const.clone(),
base,
digits,
)
.value;
let output = try_div!(output, unit, context);
let input: Option<Number> = input;
Ok(Some(PropertyReply {
name: k.clone(),
value: if let Some(input) = input.as_ref() {
let input_pretty = input.prettify(context);
let mut output_pretty = output;
output_pretty.unit = bottom_name
.iter()
.map(|(k, v)| (Dimension::new(&k), *v as i64))
.collect();
let mut res = try_div!(output_pretty, input_pretty, context)
.to_parts(context);
let value = (&unit / input)
.expect("Already known safe")
.to_parts(context);
res.quantity = value.quantity;
res
} else {
output_show
},
doc: v.doc.clone(),
}))
})
.filter_map(|x| x.map(|x| x.map(Ok)).unwrap_or_else(|e| Some(Err(e))))
.collect::<Result<Vec<PropertyReply>, String>>()?,
})
} else {
let func = |(_k, v): (&String, &Property)| {
let input = try_div!(v.input, self.amount, context);
let output = try_div!(v.output, self.amount, context);
let (name, input, output) = if input.dimless() {
if v.output.unit != unit.unit {
return Ok(None);
}
let div = try_div!(v.output, input, context);
(v.output_name.clone(), None, div)
} else if output.dimless() {
if v.input.unit != unit.unit {
return Ok(None);
}
let div = try_div!(v.input, output, context);
(v.input_name.clone(), None, div)
} else {
return Ok(None);
};
let output_show = context
.show(
&try_div!(output, unit, context),
&unit,
bottom_name.clone(),
bottom_const.clone(),
base,
digits,
)
.value;
let output = try_div!(output, unit, context);
let input: Option<Number> = input;
Ok(Some(PropertyReply {
name,
value: if let Some(input) = input.as_ref() {
let input_pretty = input.prettify(context);
let mut output_pretty = output;
output_pretty.unit = bottom_name
.iter()
.map(|(k, v)| (Dimension::new(&k), *v as i64))
.collect();
let mut res =
try_div!(output_pretty, input_pretty, context).to_parts(context);
let value = (&unit / input)
.expect("Already known safe")
.to_parts(context);
res.quantity = value.quantity;
res
} else {
output_show
},
doc: v.doc.clone(),
}))
};
let amount = PropertyReply {
name: self
.amount
.to_parts(context)
.quantity
.unwrap_or_else(|| "amount".to_owned()),
value: self.amount.to_parts(context),
doc: None,
};
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: once(Ok(Some(amount)))
.chain(self.properties.properties.iter().map(func))
.collect::<Result<Vec<Option<PropertyReply>>, String>>()?
.into_iter()
.flatten()
.collect(),
})
}
}
pub fn to_reply(&self, context: &Context) -> Result<SubstanceReply, String> {
if self.amount.dimless() {
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: self
.properties
.properties
.iter()
.map(|(k, v)| {
let (input, output) = if v.input.dimless() {
let res = (&v.output * &self.amount).unwrap();
(None, try_div!(res, v.input, context))
} else {
(Some(v.input.clone()), v.output.clone())
};
Ok(PropertyReply {
name: k.clone(),
value: if let Some(input) = input.as_ref() {
let input_pretty = input.prettify(context);
let output_pretty = output.prettify(context);
let mut res = try_div!(output_pretty, input_pretty, context)
.to_parts(context);
let value = (&output / input)
.expect("Already known safe")
.to_parts(context);
res.quantity = value.quantity;
res
} else {
output.to_parts(context)
},
doc: v.doc.clone(),
})
})
.collect::<Result<Vec<PropertyReply>, String>>()?,
})
} else {
let func = |(_k, v): (&String, &Property)| {
let input = try_div!(v.input, self.amount, context);
let output = try_div!(v.output, self.amount, context);
let (name, input, output) = if input.dimless() {
let div = try_div!(v.output, input, context);
(v.output_name.clone(), None, div)
} else if output.dimless() {
let div = try_div!(v.input, output, context);
(v.input_name.clone(), None, div)
} else {
return Ok(None);
};
let input: Option<Number> = input;
Ok(Some(PropertyReply {
name,
value: if let Some(input) = input.as_ref() {
let input_pretty = input.prettify(context);
let output_pretty = output.prettify(context);
let mut res =
try_div!(output_pretty, input_pretty, context).to_parts(context);
let value = (&output / input)
.expect("Already known safe")
.to_parts(context);
res.quantity = value.quantity;
res
} else {
output.to_parts(context)
},
doc: v.doc.clone(),
}))
};
let amount = PropertyReply {
name: self
.amount
.to_parts(context)
.quantity
.unwrap_or_else(|| "amount".to_owned()),
value: self.amount.to_parts(context),
doc: None,
};
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: once(Ok(Some(amount)))
.chain(self.properties.properties.iter().map(func))
.collect::<Result<Vec<Option<PropertyReply>>, String>>()?
.into_iter()
.flatten()
.collect(),
})
}
}
}
impl Show for Substance {
fn show(&self, context: &Context) -> String {
format!(
"{} {}",
self.amount.to_parts(context).format("n u p"),
self.properties.name
)
}
}
impl<'a, 'b> Mul<&'b Number> for &'a Substance {
type Output = Result<Substance, String>;
fn mul(self, other: &'b Number) -> Self::Output {
Ok(Substance {
amount: (&self.amount * other)
.ok_or_else(|| "Multiplication of numbers should not fail".to_owned())?,
properties: self.properties.clone(),
})
}
}
impl<'a, 'b> Div<&'b Number> for &'a Substance {
type Output = Result<Substance, String>;
fn div(self, other: &'b Number) -> Self::Output {
Ok(Substance {
amount: (&self.amount / other).ok_or_else(|| "Division by zero".to_owned())?,
properties: self.properties.clone(),
})
}
}
impl<'a, 'b> Add<&'b Substance> for &'a Substance {
type Output = Result<Substance, String>;
#[allow(clippy::suspicious_arithmetic_impl)]
fn add(self, other: &'b Substance) -> Self::Output {
let res = Substance {
amount: Number::one(),
properties: Arc::new(Properties {
name: format!(
"{} {} + {} {}",
self.amount.to_parts_simple().format("n u"),
self.properties.name,
other.amount.to_parts_simple().format("n u"),
other.properties.name,
),
properties: self
.properties
.properties
.iter()
.filter_map(|(k, prop1)| {
let prop2 = match other.properties.properties.get(k) {
Some(v) => v,
None => return None,
};
let mol = Number::one_unit(Dimension::new("mol"));
if prop1.input_name != prop2.input_name
|| prop1.output_name != prop2.output_name
|| prop1.input.unit != prop2.input.unit
|| prop1.output.unit != prop2.output.unit
|| prop1.input != mol
|| prop2.input != mol
{
return None;
}
Some((
k.clone(),
Property {
output: (&(&self.amount * &prop1.output).unwrap()
+ &(&other.amount * &prop2.output).unwrap())
.expect("Add"),
input_name: prop1.input_name.clone(),
input: mol,
output_name: prop1.output_name.clone(),
doc: None,
},
))
})
.collect(),
}),
};
if res.properties.properties.is_empty() {
Err("No shared properties".to_string())
} else {
Ok(res)
}
}
}