diff --git a/modules/mathops/README b/modules/mathops/README index 9ae17abbf68..03de9d1f6cf 100644 --- a/modules/mathops/README +++ b/modules/mathops/README @@ -92,11 +92,13 @@ modparam("mathops", "decimal_digits", 10) The function evaluates a given expression and writes the results in the output pseudo-variable. The expression may - contain any number of pseudo variables. + contain any number of pseudo variables. Evalutaion uses + tinyexpr (see https://github.com/codeplea/tinyexpr). Currently allowed syntax for specifying an expression: * Nested parentheses - * binary + - / * operators + * addition (+), subtraction/negation (-), multiplication (*), division (/), exponentiation (^) and modulus (%) with the normal operator precedence (the one exception being that exponentiation is evaluated left-to-right) + * C math functions: abs (calls to fabs), acos, asin, atan, ceil, cos, cosh, exp, floor, ln (calls to log), log (calls to log10), sin, sinh, sqrt, tan, tanh Meaning of the parameters is as follows: * expression - String containing a mathematical expression. diff --git a/modules/mathops/doc/mathops_admin.xml b/modules/mathops/doc/mathops_admin.xml index 82ff4f9320c..bf36a951c46 100644 --- a/modules/mathops/doc/mathops_admin.xml +++ b/modules/mathops/doc/mathops_admin.xml @@ -74,7 +74,7 @@ modparam("mathops", "decimal_digits", 10) The function evaluates a given expression and writes the results in the output pseudo-variable. The expression may contain any number of pseudo - variables. + variables. Evalutaion uses tinyexpr (see https://github.com/codeplea/tinyexpr). Currently allowed syntax for specifying an expression: @@ -83,7 +83,10 @@ modparam("mathops", "decimal_digits", 10) Nested parentheses - binary + - / * operators + addition (+), subtraction/negation (-), multiplication (*), division (/), exponentiation (^) and modulus (%) with the normal operator precedence (the one exception being that exponentiation is evaluated left-to-right) + + + C math functions: abs (calls to fabs), acos, asin, atan, ceil, cos, cosh, exp, floor, ln (calls to log), log (calls to log10), sin, sinh, sqrt, tan, tanh diff --git a/modules/mathops/math_funcs.c b/modules/mathops/math_funcs.c index 0a144eedc39..3ba98682446 100644 --- a/modules/mathops/math_funcs.c +++ b/modules/mathops/math_funcs.c @@ -38,6 +38,7 @@ #endif #include +#include #include "../../pvar.h" #include "../../dprint.h" @@ -45,6 +46,7 @@ #include "../../mem/mem.h" #include "../../trim.h" +#include "tinyexpr.h" #include "math_funcs.h" static char print_buffer[MATHOP_REAL_DIGITS + MATHOP_DECIMAL_DIGITS]; @@ -55,54 +57,6 @@ static token output[MAX_STACK_SIZE]; int top = 0; int pos = 0; -static int precedence(int op) -{ - switch (op) { - - case MATHOP_ADD: - case MATHOP_SUB: - return 1; - - case MATHOP_MUL: - case MATHOP_DIV: - return 2; - - default: - return 3; - } -} - -static int get_op(char symbol) -{ - switch (symbol) { - case MATHOP_PLUS: - return MATHOP_ADD; - - case MATHOP_MINUS: - return MATHOP_SUB; - - case MATHOP_MULT: - return MATHOP_MUL; - - case MATHOP_SLASH: - return MATHOP_DIV; - - default: - return -1; - } -} - -static int push_op(int type) -{ - if(top >= MAX_STACK_SIZE) { - LM_ERR("RPN Stack Full\n"); - return -1; - } - - stack[top].type = type; - top++; - return 0; -} static int push_number(double value) { @@ -137,20 +91,6 @@ static int pop_number(double *value) return 0; } -static void pop_to_output(void) -{ - output[pos++] = stack[--top]; -} - -static void pop_while_higher(int op_type) -{ - while (top > 0 && stack[top-1].type != MATHOP_LPAREN && - precedence(stack[top-1].type) >= precedence(op_type)) - { - pop_to_output(); - } -} - static int rpn_eval(const token* t) { double o1, o2; @@ -315,91 +255,6 @@ static int get_rpn_op(str *_s) return -1; } -/** - * Shunting-yard algorithm - * - * Converts an expression to Reverse Polish Notation - * Result is written to the 'output' buffer - */ -static int convert_to_rpn(str *exp) -{ - double d; - char *p; - int op; - str s; - - p = exp->s; - s.s = exp->s; - s.len = exp->len; - - while (s.len) { - - if (*s.s >= '0' && *s.s <= '9') { - errno = 0; - d = strtod(s.s, &p); - - s.len -= p - s.s; - s.s = p; - - if (errno == ERANGE) { - LM_WARN("Overflow in parsing a numeric value!\n"); - } - - output[pos].type = MATHOP_NUMBER; - output[pos].value = d; - pos++; - - trim_leading(&s); - continue; - } - - switch (*s.s) { - - case MATHOP_L_PAREN: - - push_op(MATHOP_LPAREN); - inc_and_trim(s); - break; - - case MATHOP_R_PAREN: - - while (top > 0 && stack[top-1].type != MATHOP_LPAREN) { - pop_to_output(); - } - - if (top == 0) { - LM_ERR("Parse expr error: mismatched parenthesis!\n"); - return -1; - } - - /* just pop the left parenthesis off the stack */ - top--; - - inc_and_trim(s); - - break; - - default: - - op = get_op(*s.s); - if (op < 0) { - LM_WARN("Parse expr error: Invalid operator! <%c>\n", *s.s); - return -1; - } - - pop_while_higher(op); - push_op(op); - - inc_and_trim(s); - } - } - - /* since ADD has lowest precedence, this will pop all remaining operators */ - pop_while_higher(MATHOP_ADD); - - return 0; -} - static int parse_rpn(str *exp) { double d; @@ -467,9 +322,9 @@ static int evaluate_rpn_output(double *result) /** - * Computes the result of a given expression + * Computes the result of a given RPN expression */ -int evaluate_exp(struct sip_msg *msg, str *exp, pv_spec_p result_var, short is_rpn) +int evaluate_rpn(struct sip_msg *msg, str *exp, pv_spec_p result_var) { double result; pv_value_t pv_val; @@ -480,16 +335,9 @@ int evaluate_exp(struct sip_msg *msg, str *exp, pv_spec_p result_var, short is_r top = 0; pos = 0; - if(is_rpn) { - if (parse_rpn(exp) != 0) { - LM_ERR("Failed to parse RPN!\n"); - return -1; - } - } else { - if (convert_to_rpn(exp) != 0) { - LM_ERR("Failed to convert expression to RPN form!\n"); - return -1; - } + if (parse_rpn(exp) != 0) { + LM_ERR("Failed to parse RPN!\n"); + return -1; } if (evaluate_rpn_output(&result) != 0) { @@ -512,6 +360,39 @@ int evaluate_exp(struct sip_msg *msg, str *exp, pv_spec_p result_var, short is_r return 1; } +/** + * Computes the result of a given math expression + */ +int evaluate_exp(struct sip_msg *msg, str *exp, pv_spec_p result_var) +{ + double result; + int error; + pv_value_t pv_val; + + trim(exp); + + result = te_interp(exp->s, &error); + + if (isnan(result)) { + LM_ERR("Failed to run math expression: <%.*s>\n", exp->len, exp->s); + return -1; + } + + sprintf(print_buffer, "%.*lf", decimal_digits, result); + + pv_val.flags = PV_VAL_STR; + pv_val.rs.s = print_buffer; + pv_val.rs.len = strlen(print_buffer); + + if (pv_set_value(msg, result_var, 0, &pv_val) != 0) + { + LM_ERR("SET output value failed.\n"); + return -1; + } + + return 1; +} + /** * Basic rounding to nearest integer functions: floor, ceil, trunc diff --git a/modules/mathops/math_funcs.h b/modules/mathops/math_funcs.h index a77ced0a9cb..a7b500152b1 100644 --- a/modules/mathops/math_funcs.h +++ b/modules/mathops/math_funcs.h @@ -26,6 +26,7 @@ #define __MATHOPS_H__ #include +#include "tinyexpr.h" #define MAX_STACK_SIZE 100 @@ -60,6 +61,7 @@ int basic_round_op(struct sip_msg *msg, str *n, pv_spec_p result_var, int round_dp_op(struct sip_msg *msg, str *n, pv_spec_p result_var, int digits); int round_sf_op(struct sip_msg *msg, str *n, pv_spec_p result_var, int digits); -int evaluate_exp(struct sip_msg *msg, str *exp, pv_spec_p result, short is_rpn); +int evaluate_rpn(struct sip_msg *msg, str *exp, pv_spec_p result_var); +int evaluate_exp(struct sip_msg *msg, str *exp, pv_spec_p result_var); #endif /* __MATHOPS_H__ */ diff --git a/modules/mathops/mathops.c b/modules/mathops/mathops.c index 322071bbff4..40d25157b0f 100644 --- a/modules/mathops/mathops.c +++ b/modules/mathops/mathops.c @@ -262,7 +262,7 @@ static int w_evaluate_exp(struct sip_msg *msg, char *exp, char *result) LM_DBG("Evaluating expression: %.*s\n", s.len, s.s); - return evaluate_exp(msg, &s, (pv_spec_p)result, 0); + return evaluate_exp(msg, &s, (pv_spec_p)result); } static int w_evaluate_rpn(struct sip_msg *msg, char *exp, char *result) @@ -277,7 +277,7 @@ static int w_evaluate_rpn(struct sip_msg *msg, char *exp, char *result) LM_DBG("Evaluating expression: %.*s\n", s.len, s.s); - return evaluate_exp(msg, &s, (pv_spec_p)result, 1); + return evaluate_rpn(msg, &s, (pv_spec_p)result); } diff --git a/modules/mathops/tinyexpr - LICENSE.md b/modules/mathops/tinyexpr - LICENSE.md new file mode 100644 index 00000000000..665ae06657a --- /dev/null +++ b/modules/mathops/tinyexpr - LICENSE.md @@ -0,0 +1,22 @@ +TINYEXPR - Tiny recursive descent parser and evaluation engine in C + +Copyright (c) 2015, 2016 Lewis Van Winkle + +http://CodePlea.com + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + diff --git a/modules/mathops/tinyexpr.c b/modules/mathops/tinyexpr.c new file mode 100644 index 00000000000..5a44685994e --- /dev/null +++ b/modules/mathops/tinyexpr.c @@ -0,0 +1,409 @@ +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015, 2016 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "tinyexpr.h" +#include +#include +#include +#include + + +enum {TOK_NULL, TOK_END, TOK_OPEN, TOK_CLOSE, TOK_NUMBER, TOK_ADD, TOK_SUB, TOK_MUL, TOK_DIV, TOK_FUNCTION1, TOK_FUNCTION2, TOK_VARIABLE, TOK_ERROR}; + + + +typedef struct { + const char *start; + const char *next; + int type; + union {double value; te_fun1 f1; te_fun2 f2; const double *var;}; + + const te_variable *lookup; + int lookup_len; +} state; + + + + +static te_expr *new_expr(te_expr *l, te_expr *r) { + te_expr *ret = malloc(sizeof(te_expr)); + ret->left = l; + ret->right = r; + ret->bound = 0; + return ret; +} + + +void te_free(te_expr *n) { + if (!n) return; + if (n->left) te_free(n->left); + if (n->right) te_free(n->right); + free(n); +} + + +typedef struct { + const char *name; + te_fun1 f1; +} builtin; + + +static const builtin functions[] = { + /* must be in alphabetical order */ + {"abs", fabs}, + {"acos", acos}, + {"asin", asin}, + {"atan", atan}, + {"ceil", ceil}, + {"cos", cos}, + {"cosh", cosh}, + {"exp", exp}, + {"floor", floor}, + {"ln", log}, + {"log", log10}, + {"sin", sin}, + {"sinh", sinh}, + {"sqrt", sqrt}, + {"tan", tan}, + {"tanh", tanh}, + {0} +}; + + +static const builtin *find_function(const char *name, int len) { + int imin = 0; + int imax = sizeof(functions) / sizeof(builtin) - 2; + + /*Binary search.*/ + while (imax >= imin) { + const int i = (imin + ((imax-imin)/2)); + int c = strncmp(name, functions[i].name, len); + if (!c) c = '\0' - functions[i].name[len]; + if (c == 0) { + return functions + i; + } else if (c > 0) { + imin = i + 1; + } else { + imax = i - 1; + } + } + + return 0; +} + + +static const double *find_var(const state *s, const char *name, int len) { + int i; + if (!s->lookup) return 0; + for (i = 0; i < s->lookup_len; ++i) { + if (strncmp(name, s->lookup[i].name, len) == 0 && s->lookup[i].name[len] == '\0') { + return s->lookup[i].value; + } + } + return 0; +} + + + +static double add(double a, double b) {return a + b;} +static double sub(double a, double b) {return a - b;} +static double mul(double a, double b) {return a * b;} +static double divide(double a, double b) {return a / b;} +static double negate(double a) {return -a;} + + +void next_token(state *s) { + s->type = TOK_NULL; + + if (!*s->next){ + s->type = TOK_END; + return; + } + + do { + + /* Try reading a number. */ + if ((s->next[0] >= '0' && s->next[0] <= '9') || s->next[0] == '.') { + s->value = strtod(s->next, (char**)&s->next); + s->type = TOK_NUMBER; + } else { + /* Look for a variable or builtin function call. */ + if (s->next[0] >= 'a' && s->next[0] <= 'z') { + const char *start; + start = s->next; + while (s->next[0] >= 'a' && s->next[0] <= 'z') s->next++; + + const double *var = find_var(s, start, s->next - start); + if (var) { + s->type = TOK_VARIABLE; + s->var = var; + } else { + if (s->next - start > 15) { + s->type = TOK_ERROR; + } else { + s->type = TOK_FUNCTION1; + const builtin *f = find_function(start, s->next - start); + if (!f) { + s->type = TOK_ERROR; + } else { + s->f1 = f->f1; + } + } + } + + } else { + /* Look for an operator or special character. */ + switch (s->next++[0]) { + case '+': s->type = TOK_FUNCTION2; s->f2 = add; break; + case '-': s->type = TOK_FUNCTION2; s->f2 = sub; break; + case '*': s->type = TOK_FUNCTION2; s->f2 = mul; break; + case '/': s->type = TOK_FUNCTION2; s->f2 = divide; break; + case '^': s->type = TOK_FUNCTION2; s->f2 = pow; break; + case '%': s->type = TOK_FUNCTION2; s->f2 = fmod; break; + case '(': s->type = TOK_OPEN; break; + case ')': s->type = TOK_CLOSE; break; + case ' ': case '\t': case '\n': case '\r': break; + default: s->type = TOK_ERROR; break; + } + } + } + } while (s->type == TOK_NULL); +} + + +static te_expr *expr(state *s); +static te_expr *power(state *s); + +static te_expr *base(state *s) { + /* = | | | "(" ")" */ + te_expr *ret; + + switch (s->type) { + case TOK_NUMBER: + ret = new_expr(0, 0); + ret->value = s->value; + next_token(s); + break; + + case TOK_VARIABLE: + ret = new_expr(0, 0); + ret->bound = s->var; + next_token(s); + break; + + case TOK_FUNCTION1: + ret = new_expr(0, 0); + ret->f1 = s->f1; + next_token(s); + ret->left = power(s); + break; + + case TOK_OPEN: + next_token(s); + ret = expr(s); + if (s->type != TOK_CLOSE) { + s->type = TOK_ERROR; + } else { + next_token(s); + } + break; + + default: + ret = new_expr(0, 0); + s->type = TOK_ERROR; + ret->value = 0.0/0.0; + break; + } + + return ret; +} + + +static te_expr *power(state *s) { + /* = {("-" | "+")} */ + int sign = 1; + while (s->type == TOK_FUNCTION2 && (s->f2 == add || s->f2 == sub)) { + if (s->f2 == sub) sign = -sign; + next_token(s); + } + + te_expr *ret; + + if (sign == 1) { + ret = base(s); + } else { + ret = new_expr(base(s), 0); + ret->f1 = negate; + } + + return ret; +} + + +static te_expr *factor(state *s) { + /* = {"^" } */ + te_expr *ret = power(s); + + while (s->type == TOK_FUNCTION2 && (s->f2 == pow)) { + te_fun2 t = s->f2; + next_token(s); + ret = new_expr(ret, power(s)); + ret->f2 = t; + } + + return ret; +} + + +static te_expr *term(state *s) { + /* = {("*" | "/" | "%") } */ + te_expr *ret = factor(s); + + while (s->type == TOK_FUNCTION2 && (s->f2 == mul || s->f2 == divide || s->f2 == fmod)) { + te_fun2 t = s->f2; + next_token(s); + ret = new_expr(ret, factor(s)); + ret->f2 = t; + } + + return ret; +} + + +static te_expr *expr(state *s) { + /* = {("+" | "-") } */ + te_expr *ret = term(s); + + while (s->type == TOK_FUNCTION2 && (s->f2 == add || s->f2 == sub)) { + te_fun2 t = s->f2; + next_token(s); + ret = new_expr(ret, term(s)); + ret->f2 = t; + } + + return ret; +} + + +double te_eval(const te_expr *n) { + double ret; + + if (n->bound) { + ret = *n->bound; + } else if (n->left == 0 && n->right == 0) { + ret = n->value; + } else if (n->left && n->right == 0) { + ret = n->f1(te_eval(n->left)); + } else { + ret = n->f2(te_eval(n->left), te_eval(n->right)); + } + return ret; +} + + +static void optimize(te_expr *n) { + /* Evaluates as much as possible. */ + if (n->bound) return; + + if (n->left) optimize(n->left); + if (n->right) optimize(n->right); + + if (n->left && n->right) + { + if (n->left->left == 0 && n->left->right == 0 && n->right->left == 0 && n->right->right == 0 && n->right->bound == 0 && n->left->bound == 0) + { + const double r = n->f2(n->left->value, n->right->value); + free(n->left); free(n->right); + n->left = 0; n->right = 0; + n->value = r; + } + } else if (n->left && !n->right) { + if (n->left->left == 0 && n->left->right == 0 && n->left->bound == 0) { + const double r = n->f1(n->left->value); + free(n->left); + n->left = 0; + n->value = r; + } + } +} + + +te_expr *te_compile(const char *expression, const te_variable *variables, int var_count, int *error) { + state s; + s.start = s.next = expression; + s.lookup = variables; + s.lookup_len = var_count; + + next_token(&s); + te_expr *root = expr(&s); + + if (s.type != TOK_END) { + te_free(root); + if (error) { + *error = (s.next - s.start); + if (*error == 0) *error = 1; + } + return 0; + } else { + optimize(root); + if (error) *error = 0; + return root; + } +} + + +double te_interp(const char *expression, int *error) { + te_expr *n = te_compile(expression, 0, 0, error); + double ret; + if (n) { + ret = te_eval(n); + te_free(n); + } else { + ret = 0.0/0.0; + } + return ret; +} + + +static void pn (const te_expr *n, int depth) { + printf("%*s", depth, ""); + + if (n->bound) { + printf("bound %p\n", n->bound); + } else if (n->left == 0 && n->right == 0) { + printf("%f\n", n->value); + } else if (n->left && n->right == 0) { + printf("f1 %p\n", n->left); + pn(n->left, depth+1); + } else { + printf("f2 %p %p\n", n->left, n->right); + pn(n->left, depth+1); + pn(n->right, depth+1); + } +} + + +void te_print(const te_expr *n) { + pn(n, 0); +} diff --git a/modules/mathops/tinyexpr.h b/modules/mathops/tinyexpr.h new file mode 100644 index 00000000000..90ae90bac45 --- /dev/null +++ b/modules/mathops/tinyexpr.h @@ -0,0 +1,75 @@ +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015, 2016 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#ifndef __TINYEXPR_H__ +#define __TINYEXPR_H__ + + +#ifdef __cplusplus +extern "C" { +#endif + + +typedef double (*te_fun1)(double); +typedef double (*te_fun2)(double, double); + + +typedef struct te_expr { + struct te_expr *left, *right; + union {double value; te_fun1 f1; te_fun2 f2;}; + const double *bound; +} te_expr; + + +typedef struct { + const char *name; + const double *value; +} te_variable; + + + +/* Parses the input expression, evaluates it, and frees it. */ +/* Returns NaN on error. */ +double te_interp(const char *expression, int *error); + +/* Parses the input expression and binds variables. */ +/* Returns NULL on error. */ +te_expr *te_compile(const char *expression, const te_variable *variables, int var_count, int *error); + +/* Evaluates the expression. */ +double te_eval(const te_expr *n); + +/* Prints debugging information on the syntax tree. */ +void te_print(const te_expr *n); + +/* Frees the expression. */ +/* This is safe to call on NULL pointers. */ +void te_free(te_expr *n); + + +#ifdef __cplusplus +} +#endif + +#endif /*__TINYEXPR_H__*/