Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion core/src/eval.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::sync::Arc;

use crate::{
Expand Down Expand Up @@ -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<I: Interrupt>(
input: &str,
Expand All @@ -80,8 +103,9 @@ pub(crate) fn evaluate_to_spans<I: Interrupt>(
int: &I,
) -> FResult<(Vec<Span>, 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((
Expand Down
25 changes: 25 additions & 0 deletions core/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
75 changes: 75 additions & 0 deletions core/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
17 changes: 17 additions & 0 deletions documentation/chapters/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading