#[cfg(test)]
mod tests;
pub mod util;
use std::str::FromStr;
use nom::{
bytes::complete::{tag, take_till1, take_while, take_while1},
character::complete::{anychar, digit0, multispace0},
combinator::{map, map_opt, map_res, opt, peek, verify},
error::{context, Error as NomError, ErrorKind as NomErrorKind},
multi::many1,
sequence::{delimited, preceded, terminated, tuple},
};
use crate::{
element::SimpleElement,
parse::util::{Error, ErrorKind, Input, Result},
Compound, Direction, Element, Equation, State,
};
pub fn parse_equation(orig_i: Input) -> Result<Equation> {
let (i, lhs) = context(
"splitting equation",
take_till1(|c: char| c == '<' || c == '-'),
)(orig_i)?;
let (rhs, tag) = context(
"direction of equation",
take_while1(|c: char| c == '<' || c == '-' || c == '>'),
)(i)?;
let direction = Direction::from_str(tag)
.map_err(|_| nom::Err::Error(NomError::new(i, NomErrorKind::Verify).into()))?;
let (_, left_cmp) = context("left side", parse_side)(lhs)?;
let (i, right_cmp) = context("right side", parse_side)(rhs)?;
let mut orig_i = orig_i.to_string();
orig_i.truncate(orig_i.trim_end().len());
Ok((
i,
Equation {
left: left_cmp,
right: right_cmp,
direction,
equation: orig_i,
..Default::default()
},
))
}
fn parse_side(i: Input) -> Result<Vec<Compound>> {
preceded(multispace0, many1(compound_and_plus))(i)
}
pub fn parse_element(orig_i: Input) -> Result<Element> {
let (i, (c, name)) = context(
"element name",
tuple((
context(
"starting element letter",
verify(anychar, |c| c.is_uppercase() && c.is_alphabetic()),
),
context(
"rest of element name",
take_while(|i: char| i.is_alphabetic() && i.is_lowercase()),
),
)),
)(orig_i)?;
let mut c = c.to_string();
c.push_str(name);
map_res(
map_opt(opt(digit0::<_, Error<&str>>), |s| {
s.map(str::parse::<usize>)
}),
move |num| {
SimpleElement {
name: c.clone(),
count: num.unwrap_or(1),
}
.into_element()
},
)(i)
.map_err(|e| match e {
nom::Err::Error(inner)
if inner
.errors
.first()
.map(|e| matches!(e.1, ErrorKind::InvalidElement(_)))
.unwrap_or_default() =>
{
nom::Err::Failure(inner)
}
e => e,
})
}
pub fn parse_compound(i: Input) -> Result<Compound> {
let (i, (num, elements)) = tuple((
context(
"compound coefficient",
map_opt(opt(digit0), |s: Option<&str>| s.map(str::parse::<usize>)),
),
context(
"optionally bracketed elements",
map(many1(bracketed_elements), |v| {
v.into_iter().flatten().collect::<Vec<_>>()
}),
),
))(i)?;
let (i, state) = match delimited(
context(
"leading bracket for compound state",
tag::<_, _, NomError<&str>>("("),
),
context(
"compound state",
map_res(take_while(char::is_alphabetic), State::from_str),
),
context("closing bracket for compound state", tag(")")),
)(i)
{
Ok((i, state)) => (i, Some(state)),
Err(e) => {
match e {
nom::Err::Error(inner) if inner.code == NomErrorKind::MapRes => {
return Err(nom::Err::Error(inner.into()))
}
_ => {}
}
(i, None)
}
};
Ok((
i,
Compound {
elements,
coefficient: num.unwrap_or(1),
state,
concentration: 0.0,
},
))
}
fn bracketed_elements(orig_i: Input) -> Result<Vec<Element>> {
let (i, b) = peek(anychar)(orig_i)?;
if i.chars().next().unwrap_or('a').is_lowercase() {
return Err(nom::Err::Error(
NomError::new(orig_i, NomErrorKind::Verify).into(),
));
}
let (i, deep) = if b == '(' {
let (i, _) = anychar::<_, Error<&str>>(i).unwrap();
(i, true)
} else {
(i, false)
};
let (i, mut elements) = context("elements in compound", many1(parse_element))(i)?;
let (i, b) = match peek(anychar::<_, NomError<&str>>)(i) {
Ok((i, b)) => (i, b),
Err(e) => match e {
nom::Err::Error(e) if e.code == NomErrorKind::Eof => return Ok((i, elements)),
nom::Err::Error(e) => return Err(nom::Err::Error(e.into())),
nom::Err::Failure(e) => return Err(nom::Err::Failure(e.into())),
nom::Err::Incomplete(_) => unreachable!(),
},
};
let (i, coef) = if b == ')' && deep {
let (i, _) = anychar::<_, Error<&str>>(i).unwrap();
let opt_num = opt(digit0);
context(
"coefficient for brackets",
map_opt(opt_num, |s: Option<&str>| s.map(str::parse::<usize>)),
)(i)?
} else {
(i, Ok(1))
};
for el in &mut elements {
el.count *= coef.as_ref().unwrap_or(&1);
}
Ok((i, elements))
}
fn compound_and_plus(i: Input) -> Result<Compound> {
terminated(
context("compound", parse_compound),
take_while(|c: char| c.is_whitespace() || c == '+'),
)(i)
}