rink_core/runtime/
substance.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use super::Show;
6use crate::loader::Context;
7use crate::output::{Digits, PropertyReply, SubstanceReply};
8use crate::types::{BaseUnit, Number, Numeric};
9use std::collections::BTreeMap;
10use std::iter::once;
11use std::ops::{Add, Div, Mul};
12use std::sync::Arc;
13
14macro_rules! try_div {
15    ($x:expr, $y:expr, $context:expr) => {
16        (&$x / &$y).ok_or_else(|| {
17            format!(
18                "Division by zero: <{}> / <{}>",
19                $x.show($context),
20                $y.show($context)
21            )
22        })?
23    };
24}
25
26#[derive(Debug, Clone)]
27pub struct Property {
28    pub input: Number,
29    pub input_name: String,
30    pub output: Number,
31    pub output_name: String,
32    pub doc: Option<String>,
33}
34
35#[derive(Debug, Clone)]
36pub struct Properties {
37    pub name: String,
38    pub properties: BTreeMap<String, Property>,
39}
40
41#[derive(Debug, Clone)]
42pub struct Substance {
43    pub amount: Number,
44    pub properties: Arc<Properties>,
45}
46
47pub enum SubstanceGetError {
48    Generic(String),
49    Conformance(Number, Number),
50}
51
52impl Substance {
53    pub fn rename(self, name: String) -> Substance {
54        Substance {
55            amount: self.amount,
56            properties: Arc::new(Properties {
57                name,
58                properties: self.properties.properties.clone(),
59            }),
60        }
61    }
62
63    pub fn get(&self, name: &str) -> Result<Number, SubstanceGetError> {
64        if self.amount.dimless() {
65            self.properties
66                .properties
67                .get(name)
68                .ok_or_else(|| {
69                    SubstanceGetError::Generic(format!(
70                        "No such property {} of {}",
71                        name, self.properties.name
72                    ))
73                })
74                .map(|prop| {
75                    (&(&self.amount * &prop.output).unwrap() / &prop.input)
76                        .expect("Non-zero property")
77                })
78        } else {
79            for prop in self.properties.properties.values() {
80                if name == prop.output_name {
81                    let input = (&prop.input / &self.amount)
82                        .ok_or_else(|| SubstanceGetError::Generic("Division by zero".to_owned()))?;
83                    if input.dimless() {
84                        let res = (&prop.output / &input).ok_or_else(|| {
85                            SubstanceGetError::Generic("Division by zero".to_owned())
86                        })?;
87                        return Ok(res);
88                    } else {
89                        return Err(SubstanceGetError::Conformance(
90                            self.amount.clone(),
91                            prop.input.clone(),
92                        ));
93                    }
94                } else if name == prop.input_name {
95                    let output = (&prop.output / &self.amount)
96                        .ok_or_else(|| SubstanceGetError::Generic("Division by zero".to_owned()))?;
97                    if output.dimless() {
98                        let res = (&prop.input / &output).ok_or_else(|| {
99                            SubstanceGetError::Generic("Division by zero".to_owned())
100                        })?;
101                        return Ok(res);
102                    } else {
103                        return Err(SubstanceGetError::Conformance(
104                            self.amount.clone(),
105                            prop.output.clone(),
106                        ));
107                    }
108                }
109            }
110            Err(SubstanceGetError::Generic(format!(
111                "No such property {} of {}",
112                name, self.properties.name
113            )))
114        }
115    }
116
117    /// Analogous to Context::show()
118    pub fn get_in_unit(
119        &self,
120        unit: Number,
121        context: &Context,
122        bottom_name: BTreeMap<String, isize>,
123        bottom_const: Numeric,
124        base: u8,
125        digits: Digits,
126    ) -> Result<SubstanceReply, String> {
127        if self.amount.dimless() {
128            Ok(SubstanceReply {
129                name: self.properties.name.clone(),
130                doc: context.registry.docs.get(&self.properties.name).cloned(),
131                amount: self.amount.to_parts(context),
132                properties: self
133                    .properties
134                    .properties
135                    .iter()
136                    .map(|(k, v)| {
137                        let (input, output) = if v.input.dimless() {
138                            let res = (&v.output * &self.amount).unwrap();
139                            (None, try_div!(res, v.input, context))
140                        } else {
141                            (Some(v.input.clone()), v.output.clone())
142                        };
143                        let (input, output) = if output.unit != unit.unit {
144                            if let Some(input) = input {
145                                if input.unit == unit.unit {
146                                    (Some(output), input)
147                                } else {
148                                    return Ok(None);
149                                }
150                            } else {
151                                return Ok(None);
152                            }
153                        } else {
154                            (input, output)
155                        };
156                        let output_show = context
157                            .show(
158                                &try_div!(output, unit, context),
159                                &unit,
160                                bottom_name.clone(),
161                                bottom_const.clone(),
162                                base,
163                                digits,
164                            )
165                            .value;
166                        let output = try_div!(output, unit, context);
167                        let input: Option<Number> = input;
168                        Ok(Some(PropertyReply {
169                            name: k.clone(),
170                            value: if let Some(input) = input.as_ref() {
171                                let input_pretty = input.prettify(context);
172                                let mut output_pretty = output;
173                                output_pretty.unit = bottom_name
174                                    .iter()
175                                    .map(|(k, v)| (BaseUnit::new(k), *v as i64))
176                                    .collect();
177                                let mut res = try_div!(output_pretty, input_pretty, context)
178                                    .to_parts(context);
179                                let value = (&unit / input)
180                                    .expect("Already known safe")
181                                    .to_parts(context);
182                                res.quantity = value.quantity;
183                                res
184                            } else {
185                                output_show
186                            },
187                            doc: v.doc.clone(),
188                        }))
189                    })
190                    .filter_map(|x| x.map(|x| x.map(Ok)).unwrap_or_else(|e| Some(Err(e))))
191                    .collect::<Result<Vec<PropertyReply>, String>>()?,
192            })
193        } else {
194            let func = |(_k, v): (&String, &Property)| {
195                let input = try_div!(v.input, self.amount, context);
196                let output = try_div!(v.output, self.amount, context);
197                let (name, input, output) = if input.dimless() {
198                    if v.output.unit != unit.unit {
199                        return Ok(None);
200                    }
201                    let div = try_div!(v.output, input, context);
202                    (v.output_name.clone(), None, div)
203                } else if output.dimless() {
204                    if v.input.unit != unit.unit {
205                        return Ok(None);
206                    }
207                    let div = try_div!(v.input, output, context);
208                    (v.input_name.clone(), None, div)
209                } else {
210                    return Ok(None);
211                };
212                let output_show = context
213                    .show(
214                        &try_div!(output, unit, context),
215                        &unit,
216                        bottom_name.clone(),
217                        bottom_const.clone(),
218                        base,
219                        digits,
220                    )
221                    .value;
222                let output = try_div!(output, unit, context);
223                let input: Option<Number> = input;
224                Ok(Some(PropertyReply {
225                    name,
226                    value: if let Some(input) = input.as_ref() {
227                        let input_pretty = input.prettify(context);
228                        let mut output_pretty = output;
229                        output_pretty.unit = bottom_name
230                            .iter()
231                            .map(|(k, v)| (BaseUnit::new(k), *v as i64))
232                            .collect();
233                        let mut res =
234                            try_div!(output_pretty, input_pretty, context).to_parts(context);
235                        let value = (&unit / input)
236                            .expect("Already known safe")
237                            .to_parts(context);
238                        res.quantity = value.quantity;
239                        res
240                    } else {
241                        output_show
242                    },
243                    doc: v.doc.clone(),
244                }))
245            };
246            let amount = PropertyReply {
247                name: self
248                    .amount
249                    .to_parts(context)
250                    .quantity
251                    .unwrap_or_else(|| "amount".to_owned()),
252                value: self.amount.to_parts(context),
253                doc: None,
254            };
255            Ok(SubstanceReply {
256                name: self.properties.name.clone(),
257                doc: context.registry.docs.get(&self.properties.name).cloned(),
258                amount: self.amount.to_parts(context),
259                properties: once(Ok(Some(amount)))
260                    .chain(self.properties.properties.iter().map(func))
261                    .collect::<Result<Vec<Option<PropertyReply>>, String>>()?
262                    .into_iter()
263                    .flatten()
264                    .collect(),
265            })
266        }
267    }
268
269    pub fn to_reply(&self, context: &Context) -> Result<SubstanceReply, String> {
270        if self.amount.dimless() {
271            Ok(SubstanceReply {
272                name: self.properties.name.clone(),
273                doc: context.registry.docs.get(&self.properties.name).cloned(),
274                amount: self.amount.to_parts(context),
275                properties: self
276                    .properties
277                    .properties
278                    .iter()
279                    .map(|(k, v)| {
280                        let (input, output) = if v.input.dimless() {
281                            let res = (&v.output * &self.amount).unwrap();
282                            (None, try_div!(res, v.input, context))
283                        } else {
284                            (Some(v.input.clone()), v.output.clone())
285                        };
286                        Ok(PropertyReply {
287                            name: k.clone(),
288                            value: if let Some(input) = input.as_ref() {
289                                let input_pretty = input.prettify(context);
290                                let output_pretty = output.prettify(context);
291                                let mut res = try_div!(output_pretty, input_pretty, context)
292                                    .to_parts(context);
293                                let value = (&output / input)
294                                    .expect("Already known safe")
295                                    .to_parts(context);
296                                res.quantity = value.quantity;
297                                res
298                            } else {
299                                output.to_parts(context)
300                            },
301                            doc: v.doc.clone(),
302                        })
303                    })
304                    .collect::<Result<Vec<PropertyReply>, String>>()?,
305            })
306        } else {
307            let func = |(_k, v): (&String, &Property)| {
308                let input = try_div!(v.input, self.amount, context);
309                let output = try_div!(v.output, self.amount, context);
310                let (name, input, output) = if input.dimless() {
311                    let div = try_div!(v.output, input, context);
312                    (v.output_name.clone(), None, div)
313                } else if output.dimless() {
314                    let div = try_div!(v.input, output, context);
315                    (v.input_name.clone(), None, div)
316                } else {
317                    return Ok(None);
318                };
319                let input: Option<Number> = input;
320                Ok(Some(PropertyReply {
321                    name,
322                    value: if let Some(input) = input.as_ref() {
323                        let input_pretty = input.prettify(context);
324                        let output_pretty = output.prettify(context);
325                        let mut res =
326                            try_div!(output_pretty, input_pretty, context).to_parts(context);
327                        let value = (&output / input)
328                            .expect("Already known safe")
329                            .to_parts(context);
330                        res.quantity = value.quantity;
331                        res
332                    } else {
333                        output.to_parts(context)
334                    },
335                    doc: v.doc.clone(),
336                }))
337            };
338            let amount = PropertyReply {
339                name: self
340                    .amount
341                    .to_parts(context)
342                    .quantity
343                    .unwrap_or_else(|| "amount".to_owned()),
344                value: self.amount.to_parts(context),
345                doc: None,
346            };
347            Ok(SubstanceReply {
348                name: self.properties.name.clone(),
349                doc: context.registry.docs.get(&self.properties.name).cloned(),
350                amount: self.amount.to_parts(context),
351                properties: once(Ok(Some(amount)))
352                    .chain(self.properties.properties.iter().map(func))
353                    .collect::<Result<Vec<Option<PropertyReply>>, String>>()?
354                    .into_iter()
355                    .flatten()
356                    .collect(),
357            })
358        }
359    }
360}
361
362impl Show for Substance {
363    fn show(&self, context: &Context) -> String {
364        format!(
365            "{} {}",
366            self.amount.to_parts(context).format("n u p"),
367            self.properties.name
368        )
369    }
370}
371
372impl<'a, 'b> Mul<&'b Number> for &'a Substance {
373    type Output = Result<Substance, String>;
374
375    fn mul(self, other: &'b Number) -> Self::Output {
376        Ok(Substance {
377            amount: (&self.amount * other)
378                .ok_or_else(|| "Multiplication of numbers should not fail".to_owned())?,
379            properties: self.properties.clone(),
380        })
381    }
382}
383
384impl<'a, 'b> Div<&'b Number> for &'a Substance {
385    type Output = Result<Substance, String>;
386
387    fn div(self, other: &'b Number) -> Self::Output {
388        Ok(Substance {
389            amount: (&self.amount / other).ok_or_else(|| "Division by zero".to_owned())?,
390            properties: self.properties.clone(),
391        })
392    }
393}
394
395impl<'a, 'b> Add<&'b Substance> for &'a Substance {
396    type Output = Result<Substance, String>;
397
398    #[allow(clippy::suspicious_arithmetic_impl)]
399    fn add(self, other: &'b Substance) -> Self::Output {
400        let res = Substance {
401            amount: Number::one(),
402            properties: Arc::new(Properties {
403                name: format!(
404                    "{} {} + {} {}",
405                    self.amount.to_parts_simple().format("n u"),
406                    self.properties.name,
407                    other.amount.to_parts_simple().format("n u"),
408                    other.properties.name,
409                ),
410                properties: self
411                    .properties
412                    .properties
413                    .iter()
414                    .filter_map(|(k, prop1)| {
415                        let prop2 = match other.properties.properties.get(k) {
416                            Some(v) => v,
417                            None => return None,
418                        };
419                        let mol = Number::one_unit(BaseUnit::new("mol"));
420                        if prop1.input_name != prop2.input_name
421                            || prop1.output_name != prop2.output_name
422                            || prop1.input.unit != prop2.input.unit
423                            || prop1.output.unit != prop2.output.unit
424                            || prop1.input != mol
425                            || prop2.input != mol
426                        {
427                            return None;
428                        }
429                        Some((
430                            k.clone(),
431                            Property {
432                                output: (&(&self.amount * &prop1.output).unwrap()
433                                    + &(&other.amount * &prop2.output).unwrap())
434                                    .expect("Add"),
435                                input_name: prop1.input_name.clone(),
436                                input: mol,
437                                output_name: prop1.output_name.clone(),
438                                doc: None,
439                            },
440                        ))
441                    })
442                    .collect(),
443            }),
444        };
445        if res.properties.properties.is_empty() {
446            Err("No shared properties".to_string())
447        } else {
448            Ok(res)
449        }
450    }
451}