Skip to content

Commit

Permalink
Add support for mathematical functions (#14)
Browse files Browse the repository at this point in the history
Makes Numbrs support parsing and evaluating mathematical functions.
  • Loading branch information
kdkasad authored Apr 1, 2024
2 parents d6a40b6 + 9ea3207 commit ddfaed0
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 19 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The goal of Numbrs is twofold:
- [x] Perform arithmetic on quantities
- [x] Convert quantities between units
- [ ] (optional) Define custom base quantities (e.g. currency)
- [ ] GCD and LCM calculations
- [x] GCD and LCM calculations
- [ ] Modular arithmetic

## Installation
Expand Down Expand Up @@ -207,6 +207,29 @@ The positive value *N* will round to *N* places to the right of the decimal
point. The negative value *-N* will round so that *N* places to the left of the
decimal point contain zeros.

### Functions

Numbrs supoprt some built-in functions. Functions are called by preceding a
group (e.g. parentheses) with the function identifier.

Example:

$ numbrs
> sin(0)
0
> cos(180 degrees)
-1.00000

Currently, the following functions are supported:

| Function | Idenfifier | Number of arguments |
| --- | --- | --- |
| Sine | `sin` | 1 |
| Cosine | `cos` | 1 |
| Absolute value | `abs` | 1 |
| Square root | `sqrt` | 1 |
| Natural logarithm | `ln` | 1 |

### Units

Numbrs supports processing amounts of physical quantities, commonly known as
Expand Down
32 changes: 31 additions & 1 deletion src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ use std::fmt::{self, Display};

use num::BigRational;

use crate::{operation::Operation, rat_util_macros::rat, unit::Units};
use crate::{functions::Function, operation::Operation, rat_util_macros::rat, unit::Units};

/// # Physical quantity
///
Expand Down Expand Up @@ -207,6 +207,33 @@ impl BinaryExpression {
}
}

/// # Function call expression
///
/// A [`FunctionCall`] is an [AST node][1] type which represents calling a
/// function with some arguments.
///
/// The function is represented by the [`Function`] enum.
///
/// The arguments are represented by a [`Vec<Node>`]. The number of arguments
/// must match the number of arguments given in the function variant's
/// documentation.
///
/// [1]: crate::ast
#[derive(Debug, Clone, PartialEq)]
pub struct FunctionCall {
pub(crate) function: Function,
pub(crate) args: Vec<Node>,
}

impl FunctionCall {
/// # Create a new function
///
/// Uses the given [function][Function] and argument list.
pub fn new(function: Function, args: Vec<Node>) -> Self {
Self { function, args }
}
}

/// # Expression result value
///
/// Evaluating an expression returns a [`Value`], which is either a
Expand Down Expand Up @@ -279,6 +306,9 @@ pub enum Node {
///
/// See [`ast::Quantity`][Quantity].
Quantity(Quantity),

/// Function call
FunctionCall(FunctionCall),
}

impl From<BigRational> for Node {
Expand Down
6 changes: 5 additions & 1 deletion src/bin/numbrs/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ mod completion;

use std::{cell::RefCell, rc::Rc};

use numbrs::{affixes::standard_prefixes, ast::Value, runtime::{Runtime, RuntimeError}};
use numbrs::{
affixes::standard_prefixes,
ast::Value,
runtime::{Runtime, RuntimeError},
};
use rustyline::{error::ReadlineError, CompletionType, Editor};

use self::textio::*;
Expand Down
58 changes: 58 additions & 0 deletions src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::{
affixes::{resolve_unit, try_get_prefix_scale},
ast::*,
dimension::Dimension,
functions::Function,
operation::Operation,
rat_util_macros::rat,
runtime::Runtime,
Expand Down Expand Up @@ -453,6 +454,32 @@ impl Node {

Node::Number(num) => Ok(num.into()),
Node::Quantity(q) => Ok(q.into()),
Node::FunctionCall(node) => {
let mut args: Vec<BigRational> = Vec::with_capacity(node.args.len());
for arg in node.args {
let val = arg.eval(env)?;
match val {
Value::Quantity(quantity) => {
if quantity.is_value() {
let num = quantity.units.scale(&quantity.mag);
args.push(num);
} else {
return Err(EvalError::NonPureValue(quantity.into()));
}
}
Value::Unit(units) => {
if units.is_dimensionless() {
let num = units.scale(&rat!(1));
args.push(num);
} else {
return Err(EvalError::NonPureValue(units.into()));
}
}
Value::Number(num) => args.push(num),
}
}
node.function.eval(args)
}
}
}
}
Expand Down Expand Up @@ -579,6 +606,37 @@ pub enum EvalError {
/// [`Operation`] in its tuple fields.
#[error("Invalid unary operation `{1}` for type `{0}`")]
InvalidUnaryOperation(Box<Value>, Operation),

/// # Wrong number of function arguments
///
/// This error occurs when a function is called with the wrong number of
/// arguments.
///
/// The tuple fields of this error are:
/// 1. The function that was called
/// 2. The number of arguments expected
/// 3. The number of arguments given
#[error("Function `{0}` expects {1} arguments, got {2}")]
NumberOfFunctionArguments(Function, usize, usize),

/// # Non-integer argument given to a function which requires integers.
///
/// The referenced function and the offending argument are stored in this
/// error variant's tuple fields.
#[error("Function `{0}` requires integer arguments, got `{1}`")]
NonIntegerFunctionArgument(Function, Value),

/// # Non-pure value used in a context where a pure value is required
///
/// The invalid value is stored in the tuple field.
#[error("Non-number value `{0}` used in a context where a pure number value is required")]
NonPureValue(Value),

/// # Overflow occurred during evaluation
///
/// This should only be returned by [`Function`] calls.
#[error("Number `{0}` too large. Overflow occurred.")]
Overflow(BigRational),
}

#[cfg(test)]
Expand Down
Loading

0 comments on commit ddfaed0

Please sign in to comment.