use crate::domain::errors::{AppError, Result};
use crate::domain::evaluator::Evaluator;
use crate::domain::expression::{self, Statement};
use crate::domain::format::FormatSettings;
use crate::domain::history::LineResult;
use crate::domain::quantity::Quantity;
use crate::domain::units;
use crate::domain::variables::VariableStore;
const RESERVED_NAMES: &[&str] = &["ans", "pi", "e"];
const CONTINUING_OPERATORS: [char; 5] = ['+', '-', '*', '/', '^'];
fn continues_from_ans(expr: &str) -> bool {
expr.trim_start().starts_with(CONTINUING_OPERATORS)
}
fn substitute_variables(
expr: &str,
variables: &VariableStore,
render: impl Fn(&Quantity) -> Option<String>,
) -> String {
let mut prepared = expr.to_string();
for (name, value) in variables.iter() {
if !expression::references(&prepared, name) {
continue;
}
if let Some(literal) = render(value) {
prepared = expression::substitute_identifier_with(
&prepared, name, &literal,
);
}
}
prepared
}
pub(super) fn evaluate_line(
evaluator: &dyn Evaluator,
variables: &mut VariableStore,
settings: &FormatSettings,
input: &str,
ans: Option<Quantity>,
) -> LineResult {
let code = expression::strip_comment(input);
if code.trim().is_empty() {
return (None, None);
}
match expression::classify(code) {
Statement::SaveAns(name) => save_ans(variables, &name, ans),
Statement::Assign { name, expr } => {
assign(evaluator, variables, settings, &name, &expr, ans)
}
Statement::Expression(expr) => {
match eval_expression(evaluator, variables, settings, &expr, ans) {
Ok(value) => (Some(value), None),
Err(error) => (None, Some(error.to_string())),
}
}
}
}
fn save_ans(
variables: &mut VariableStore,
name: &str,
ans: Option<Quantity>,
) -> LineResult {
if let Some(error) = reject_name(name) {
return (None, Some(error.to_string()));
}
match ans {
Some(value) => {
variables.set(name, value.clone());
(Some(value), None)
}
None => (None, Some(AppError::NoAnswerToSave.to_string())),
}
}
fn assign(
evaluator: &dyn Evaluator,
variables: &mut VariableStore,
settings: &FormatSettings,
name: &str,
expr: &str,
ans: Option<Quantity>,
) -> LineResult {
if let Some(error) = reject_name(name) {
return (None, Some(error.to_string()));
}
match eval_expression(evaluator, variables, settings, expr, ans) {
Ok(value) => {
variables.set(name, value.clone());
(Some(value), None)
}
Err(error) => (None, Some(error.to_string())),
}
}
pub(super) fn eval_expression(
evaluator: &dyn Evaluator,
variables: &VariableStore,
settings: &FormatSettings,
expr: &str,
ans: Option<Quantity>,
) -> Result<Quantity> {
let trimmed = expr.trim();
if trimmed == "ans" {
return ans.ok_or(AppError::NoPreviousAnswer);
}
if let Some(quantity) = variables.get(trimmed) {
return Ok(quantity.clone());
}
if needs_units(expr, variables, ans.as_ref()) {
return eval_with_rink(variables, settings, expr, ans);
}
let value = eval_with_meval(evaluator, variables, settings, expr, ans)?;
Ok(Quantity::dimensionless(value))
}
fn needs_units(
expr: &str,
variables: &VariableStore,
ans: Option<&Quantity>,
) -> bool {
if split_conversion(expr).is_some() {
return true;
}
if ans.is_some_and(|a| !a.is_dimensionless())
&& (continues_from_ans(expr) || expression::references(expr, "ans"))
{
return true;
}
for (name, value) in variables.iter() {
if !value.is_dimensionless() && expression::references(expr, name) {
return true;
}
}
expr.split(|c: char| !c.is_ascii_alphabetic())
.filter(|token| !token.is_empty())
.any(|token| {
!matches!(token, "pi" | "e" | "ans")
&& variables.get(token).is_none()
&& units::is_unit(token)
})
}
fn split_conversion(expr: &str) -> Option<(&str, &str)> {
if let Some((source, target)) = expr.split_once("->") {
return Some((source, target));
}
expr.split_once(" to ")
}
fn eval_with_rink(
variables: &VariableStore,
settings: &FormatSettings,
expr: &str,
ans: Option<Quantity>,
) -> Result<Quantity> {
let prepared = substitute_for_units(variables, settings, expr, ans)?;
if let Some((_, target)) = split_conversion(expr) {
let target = target.trim();
if !target.is_empty() {
let (value, _) = units::eval(&prepared)?;
return Ok(Quantity::new(value, target.to_string()));
}
}
let (base_value, unit) = units::eval(&prepared)?;
let Some(unit) = unit else {
return Ok(Quantity::dimensionless(base_value));
};
let display_unit = simple_literal_unit(expr)
.unwrap_or_else(|| units::prettify_unit(&unit));
let value = base_value / units::scale_of(&display_unit)?;
Ok(Quantity::new(value, display_unit))
}
fn substitute_for_units(
variables: &VariableStore,
settings: &FormatSettings,
expr: &str,
ans: Option<Quantity>,
) -> Result<String> {
let mut prepared = prepare_units_expr(expr, settings.decimal_separator);
if continues_from_ans(&prepared) && ans.is_some() {
prepared = format!("ans {prepared}");
}
if expression::references(&prepared, "ans") {
let value = ans.as_ref().ok_or(AppError::NoPreviousAnswer)?;
prepared = expression::substitute_identifier_with(
&prepared,
"ans",
&quantity_literal(value),
);
}
Ok(substitute_variables(&prepared, variables, |value| {
Some(quantity_literal(value))
}))
}
fn simple_literal_unit(expr: &str) -> Option<String> {
let (head, last) = expr.trim().rsplit_once(char::is_whitespace)?;
let head = head.trim();
let number_chars = |c: char| {
c.is_ascii_digit() || matches!(c, '.' | 'e' | 'E' | '+' | '-')
};
if head.is_empty()
|| !head.chars().all(number_chars)
|| !head.chars().any(|c| c.is_ascii_digit())
|| !units::is_unit(last)
{
return None;
}
Some(last.to_string())
}
fn quantity_literal(quantity: &Quantity) -> String {
match quantity.unit_symbol() {
Some(symbol) => format!("({} {})", quantity.display_value(), symbol),
None => format!("({})", quantity.display_value()),
}
}
fn prepare_units_expr(expr: &str, decimal_separator: char) -> String {
let replaced = expr.replace("**", "^");
if decimal_separator == ',' {
replaced.replace(',', ".")
} else {
replaced
}
}
fn eval_with_meval(
evaluator: &dyn Evaluator,
variables: &VariableStore,
settings: &FormatSettings,
expr: &str,
ans: Option<Quantity>,
) -> Result<f64> {
let prepared = expression::preprocess(expr, settings.decimal_separator);
let ans_number = ans
.as_ref()
.filter(|a| a.is_dimensionless())
.map(Quantity::display_value);
let prepared = expression::prepend_ans(&prepared, ans_number);
let prepared = expression::substitute_ans(&prepared, ans_number);
let prepared = substitute_variables(&prepared, variables, |value| {
value
.is_dimensionless()
.then(|| value.display_value().to_string())
});
evaluator.eval(&prepared, settings.angle_mode)
}
pub(super) fn reject_name(name: &str) -> Option<AppError> {
if !expression::is_valid_var_name(name) {
return Some(AppError::InvalidVariableName(name.to_string()));
}
if RESERVED_NAMES.contains(&name) {
return Some(AppError::ReservedName(name.to_string()));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn store(pairs: &[(&str, Quantity)]) -> VariableStore {
let mut variables = VariableStore::new();
for (name, value) in pairs {
variables.set(name, value.clone());
}
variables
}
fn metres(value: f64) -> Quantity {
Quantity::new(value, "m".to_string())
}
#[test]
fn a_leading_operator_marks_a_continuation_of_the_previous_answer() {
for expr in ["* 2", " + 1", "-3", "/2", "^2"] {
assert!(continues_from_ans(expr), "{expr}");
}
for expr in ["2 * 3", "sin(1)", "", " "] {
assert!(!continues_from_ans(expr), "{expr}");
}
}
#[test]
fn substitution_rewrites_referenced_variables_only() {
let variables = store(&[
("x", Quantity::dimensionless(2.0)),
("unused", Quantity::dimensionless(9.0)),
]);
let out = substitute_variables("x + 1", &variables, |value| {
Some(value.display_value().to_string())
});
assert_eq!(out, "2 + 1");
}
#[test]
fn a_declined_variable_is_left_in_place() {
let variables = store(&[("d", metres(3.0))]);
let out = substitute_variables("d + 1", &variables, |_| None);
assert_eq!(out, "d + 1");
}
#[test]
fn a_conversion_splits_on_both_spellings() {
assert_eq!(split_conversion("3 m -> cm"), Some(("3 m ", " cm")));
assert_eq!(split_conversion("3 m to cm"), Some(("3 m", "cm")));
assert_eq!(split_conversion("3 + 4"), None);
}
#[test]
fn a_plain_number_and_unit_keeps_the_typed_symbol() {
assert_eq!(simple_literal_unit("50 kN"), Some("kN".to_string()));
assert_eq!(simple_literal_unit("1.5e3 m"), Some("m".to_string()));
assert_eq!(simple_literal_unit("50 kN + 1 kN"), None);
assert_eq!(simple_literal_unit("kN"), None);
assert_eq!(simple_literal_unit("50 notaunit"), None);
}
#[test]
fn a_quantity_renders_as_a_parenthesised_rink_literal() {
assert_eq!(quantity_literal(&metres(3.0)), "(3 m)");
assert_eq!(quantity_literal(&Quantity::dimensionless(3.0)), "(3)",);
}
#[test]
fn the_units_preprocessor_keeps_spaces_and_maps_the_decimal_mark() {
assert_eq!(prepare_units_expr("2 ** 3", '.'), "2 ^ 3");
assert_eq!(prepare_units_expr("1,5 m", ','), "1.5 m");
assert_eq!(prepare_units_expr("1,5 m", '.'), "1,5 m");
}
#[test]
fn reserved_and_malformed_variable_names_are_rejected() {
for name in ["ans", "pi", "e"] {
assert!(
matches!(reject_name(name), Some(AppError::ReservedName(_))),
"{name}",
);
}
assert!(matches!(
reject_name("1abc"),
Some(AppError::InvalidVariableName(_)),
));
assert!(reject_name("width").is_none());
}
#[test]
fn the_router_sends_a_conversion_to_the_units_engine() {
let variables = VariableStore::new();
assert!(needs_units("3 m -> cm", &variables, None));
}
#[test]
fn the_router_keeps_bare_arithmetic_and_constants_on_meval() {
let variables = VariableStore::new();
for expr in ["2 + 3", "sin(pi)", "e^2", "ans * 2"] {
assert!(!needs_units(expr, &variables, None), "{expr}");
}
}
#[test]
fn a_unit_bearing_variable_routes_its_expression_to_the_units_engine() {
let variables = store(&[("d", metres(3.0))]);
assert!(needs_units("d * 2", &variables, None));
}
#[test]
fn a_dimensionless_variable_named_like_a_unit_stays_on_meval() {
let variables = store(&[("m", Quantity::dimensionless(4.0))]);
assert!(!needs_units("m * 2", &variables, None));
}
#[test]
fn continuing_from_a_unit_bearing_answer_routes_to_the_units_engine() {
let variables = VariableStore::new();
let ans = metres(3.0);
assert!(needs_units("* 2", &variables, Some(&ans)));
let plain = Quantity::dimensionless(3.0);
assert!(!needs_units("* 2", &variables, Some(&plain)));
}
}