From 75f9bb502829183c764faef862a1a79b6791bf03 Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:11:52 -0400 Subject: [PATCH] Continue from the previous result when a line starts with an operator If a line begins with a binary operator that can't start an expression on its own (like `* 2` or `to miles`), fend now treats it as continuing from the previous result, so it becomes `_ * 2` or `_ to miles`. This matches how pocket and on-screen calculators behave. Operators with a prefix form (`+`, `-`, `/`) are left untouched so `-5` and `/2` keep their existing meanings, and nothing changes when there is no previous result to continue from. Closes #306 --- core/src/eval.rs | 26 +++++++++- core/src/lexer.rs | 25 +++++++++ core/tests/integration_tests.rs | 75 +++++++++++++++++++++++++++ documentation/chapters/expressions.md | 17 ++++++ 4 files changed, 142 insertions(+), 1 deletion(-) diff --git a/core/src/eval.rs b/core/src/eval.rs index 5cfe44ef..6ddc2220 100644 --- a/core/src/eval.rs +++ b/core/src/eval.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::sync::Arc; use crate::{ @@ -72,6 +73,28 @@ fn parse_attrs(mut input: &str) -> (Attrs, &str) { (attrs, input) } +/// If a previous result is available (stored in `_`) and the input begins with +/// a binary operator that cannot otherwise start an expression (such as `* 2` +/// or `to miles`), rewrite it to continue from that result, e.g. `_ * 2` or +/// `_ to miles`. This mirrors how pocket and on-screen calculators let an +/// operator key off the previously-displayed value. +fn continue_from_previous_result<'a, I: Interrupt>( + input: &'a str, + context: &crate::Context, + int: &I, +) -> Cow<'a, str> { + if !context.variables.contains_key("_") { + return Cow::Borrowed(input); + } + let mut tokens = lexer::lex(input, context, int); + if let Some(Ok(lexer::Token::Symbol(symbol))) = tokens.next() + && symbol.expects_preceding_operand() + { + return Cow::Owned(format!("_ {input}")); + } + Cow::Borrowed(input) +} + /// This also saves the calculation result in a variable `_` and `ans` pub(crate) fn evaluate_to_spans( input: &str, @@ -80,8 +103,9 @@ pub(crate) fn evaluate_to_spans( int: &I, ) -> FResult<(Vec, Attrs)> { let (attrs, input) = parse_attrs(input); + let input = continue_from_previous_result(input, context, int); let mut spans = vec![]; - let value = evaluate_to_value(input, scope, attrs, &mut spans, context, int)?; + let value = evaluate_to_value(input.as_ref(), scope, attrs, &mut spans, context, int)?; context.variables.insert("_".to_string(), value.clone()); context.variables.insert("ans".to_string(), value.clone()); Ok(( diff --git a/core/src/lexer.rs b/core/src/lexer.rs index 6e71049e..05f84828 100644 --- a/core/src/lexer.rs +++ b/core/src/lexer.rs @@ -44,6 +44,31 @@ pub(crate) enum Symbol { Permutation, } +impl Symbol { + /// Returns `true` if this is an infix operator that needs an operand on its + /// left-hand side and has no prefix (unary) form, so it cannot begin an + /// expression on its own. This lets input like `* 2` or `to miles` continue + /// from the previous result. `+`, `-` and `/` are deliberately excluded + /// because they double as prefix operators (e.g. `-5` or `/2`). + pub(crate) fn expects_preceding_operand(self) -> bool { + matches!( + self, + Self::Mul + | Self::Mod | Self::Pow + | Self::BitwiseAnd + | Self::BitwiseOr + | Self::BitwiseXor + | Self::UnitConversion + | Self::ShiftLeft + | Self::ShiftRight + | Self::DoubleEquals + | Self::NotEquals + | Self::Combination + | Self::Permutation + ) + } +} + impl fmt::Display for Symbol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { let s = match self { diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 4ac63ae0..7b042e9e 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6167,3 +6167,78 @@ fn decimal_separator_comma() { "1,69 AUD" ); } + +/// Evaluates each input in turn on a shared context (as the REPL does, keeping +/// `_` around between lines) and returns the main result of the final input. +fn eval_continued(inputs: &[&str]) -> String { + let mut context = Context::new(); + context.set_exchange_rate_handler_v2(fend_core::test_utils::DummyCurrencyHandler); + let mut result = String::new(); + for input in inputs { + result = evaluate(input, &mut context) + .unwrap() + .get_main_result() + .to_string(); + } + result +} + +#[test] +fn continue_from_previous_result_multiplication() { + assert_eq!(eval_continued(&["8", "* 2"]), "16"); +} + +#[test] +fn continue_from_previous_result_power() { + assert_eq!(eval_continued(&["8", "^ 2"]), "64"); +} + +#[test] +fn continue_from_previous_result_modulo() { + assert_eq!(eval_continued(&["17", "mod 5"]), "2"); +} + +#[test] +fn continue_from_previous_result_bit_shift() { + assert_eq!(eval_continued(&["1", "<< 4"]), "16"); +} + +#[test] +fn continue_from_previous_result_xor() { + assert_eq!(eval_continued(&["6", "xor 3"]), "5"); +} + +#[test] +fn continue_from_previous_result_unit_conversion() { + assert_eq!(eval_continued(&["255", "to hex"]), "ff"); +} + +#[test] +fn continue_from_previous_result_can_be_chained() { + assert_eq!(eval_continued(&["8", "* 2", "^ 2"]), "256"); +} + +#[test] +fn continue_from_previous_result_respects_leading_attribute() { + assert_eq!(eval_continued(&["3.5", "@noapprox * 2"]), "7"); +} + +#[test] +fn continue_from_previous_result_leaves_unary_minus_alone() { + // `-` has a prefix form, so `- 5` is negative five rather than `_ - 5`. + assert_eq!(eval_continued(&["8", "- 5"]), "-5"); +} + +#[test] +fn continue_from_previous_result_leaves_unary_division_alone() { + // `/` has a prefix form (`/2` == `1/2`), so it is not rewritten either. + assert_eq!(eval_continued(&["8", "/ 2"]), "0.5"); +} + +#[test] +fn no_previous_result_to_continue_from() { + // Without a previous result there is nothing to continue from, so a leading + // binary operator remains an error. + let mut context = Context::new(); + assert!(evaluate("* 2", &mut context).is_err()); +} diff --git a/documentation/chapters/expressions.md b/documentation/chapters/expressions.md index 9f8457f1..0dd70715 100644 --- a/documentation/chapters/expressions.md +++ b/documentation/chapters/expressions.md @@ -156,6 +156,23 @@ The most recent calculation result is stored in a special variable `_` (or `ans` 220 ``` +As a shorthand, if a line begins with an operator that would otherwise need a +value on its left, fend continues from the previous result automatically, just +like a pocket calculator: + +``` +> 5 * 10 +50 +> * 2 +100 +> to hex +64 +``` + +This only applies to operators that can't start an expression on their own, so +`-5` and `/2` still mean negative five and one half rather than continuing from +the previous result. + ## Units fend supports many units, such as `kg`, `lb`, `N`, `lightyear`, etc. You can interchangeably use `to`, `as` and `in` to convert between units.