Numeric expression evaluator

This commit is contained in:
Michael Geselbracht 2017-11-23 11:36:24 +01:00 committed by Maciej Suminski
parent 579d3f478d
commit b5fc6e45cf
8 changed files with 1678 additions and 1 deletions

View File

@ -348,6 +348,8 @@ set( COMMON_SRCS
geometry/shape_collisions.cpp geometry/shape_collisions.cpp
geometry/shape_file_io.cpp geometry/shape_file_io.cpp
geometry/convex_hull.cpp geometry/convex_hull.cpp
libeval/numeric_evaluator.cpp
) )
add_library( common STATIC ${COMMON_SRCS} ) add_library( common STATIC ${COMMON_SRCS} )
add_dependencies( common lib-dependencies ) add_dependencies( common lib-dependencies )

View File

@ -39,6 +39,7 @@
#include <class_title_block.h> #include <class_title_block.h>
#include <common.h> #include <common.h>
#include <base_units.h> #include <base_units.h>
#include "libeval/numeric_evaluator.h"
#if defined( PCBNEW ) || defined( CVPCB ) || defined( EESCHEMA ) || defined( GERBVIEW ) || defined( PL_EDITOR ) #if defined( PCBNEW ) || defined( CVPCB ) || defined( EESCHEMA ) || defined( GERBVIEW ) || defined( PL_EDITOR )
@ -385,6 +386,10 @@ int ValueFromTextCtrl( const wxTextCtrl& aTextCtr )
{ {
int value; int value;
wxString msg = aTextCtr.GetValue(); wxString msg = aTextCtr.GetValue();
NumericEvaluator eval;
if( eval.process( msg.mb_str() ) )
msg = wxString::FromUTF8( eval.result() );
value = ValueFromString( g_UserUnit, msg ); value = ValueFromString( g_UserUnit, msg );

1011
common/libeval/grammar.c Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
/*
This file is part of libeval, a simple math expression evaluator
Copyright (C) 2017 Michael Geselbracht, mgeselbracht3@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
%token_type { numEval::TokenType }
%extra_argument { NumericEvaluator* pEval }
%nonassoc VAR ASSIGN UNIT SEMCOL.
%left PLUS MINUS.
%left DIVIDE MULT.
%include {
#include <assert.h>
#include <math.h>
#include <libeval/numeric_evaluator.h>
}
%syntax_error {
pEval->parseError("Syntax error");
}
%parse_accept {
pEval->parseOk();
}
main ::= in.
/* Allow multiple statements in input string: x=1; y=2 */
in ::= stmt.
in ::= in stmt.
/* A statement can be empty, an expr or an expr followed by ';' */
stmt ::= ENDS.
stmt ::= expr(A) ENDS. { pEval->parseSetResult(A.valid ? A.dValue : NAN); }
stmt ::= expr SEMCOL. { pEval->parseSetResult(NAN); }
expr(A) ::= VALUE(B). { A.dValue = B.dValue; A.valid=true; }
expr(A) ::= VALUE(B) UNIT(C). { A.dValue = B.dValue * C.dValue; A.valid=true; }
expr(A) ::= MINUS expr(B). { A.dValue = -B.dValue; A.valid=B.valid; }
expr(A) ::= VAR(B). { A.dValue = pEval->getVar(B.text); A.valid=true; }
expr(A) ::= VAR(B) ASSIGN expr(C). { pEval->setVar(B.text, C.dValue); A.dValue = C.dValue; A.valid=false; }
expr(A) ::= expr(B) PLUS expr(C). { A.dValue = B.dValue + C.dValue; A.valid=C.valid; }
expr(A) ::= expr(B) MINUS expr(C). { A.dValue = B.dValue - C.dValue; A.valid=C.valid; }
expr(A) ::= expr(B) MULT expr(C). { A.dValue = B.dValue * C.dValue; A.valid=C.valid; }
expr(A) ::= expr(B) DIVIDE expr(C). {
if (C.dValue != 0.0) {
A.dValue = B.dValue / C.dValue;
}
else pEval->parseError("Div by zero");
A.valid=C.valid;
}
expr(A) ::= PARENL expr(B) PARENR. { A.dValue = B.dValue; A.valid=B.valid; }

52
common/libeval/main.cpp Normal file
View File

@ -0,0 +1,52 @@
/*
This file is part of libeval, a simple math expression evaluator
Copyright (C) 2017 Michael Geselbracht, mgeselbracht3@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <assert.h>
#include "numeric_evaluator.h"
int main()
{
NumericEvaluator eval;
eval.process("2.54mm+50mil");
if (eval.isValid()) printf("%s\n", eval.result());
eval.process("x=1; y=5;");
if (eval.isValid()) printf("%s\n", eval.result());
eval.process("x+y");
if (eval.isValid()) printf("%s\n", eval.result());
eval.setVar("posx", -3.14152);
bool retval = eval.process("posx");
assert(retval == eval.isValid());
if (eval.isValid()) printf("%s\n", eval.result());
eval.process("x=1; y=2");
eval.setVar("z", 3);
eval.process("x+y+z");
printf("x+y+z=%s\n", eval.result());
eval.process("1\"");
printf("1\" = %s\n", eval.result());
eval.process("12.7 - 0.1\" - 50mil");
printf("12.7 - 0.1\" - 50mil = %s\n", eval.result());
}

View File

@ -0,0 +1,334 @@
/*
This file is part of libeval, a simple math expression evaluator
Copyright (C) 2017 Michael Geselbracht, mgeselbracht3@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#define TESTMODE 0
#include <libeval/numeric_evaluator.h>
#if !TESTMODE
#include <common.h>
#else
#include <unistd.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* The (generated) lemon parser is written in C.
* In order to keep its symbol from the global namespace include the parser code with
* a C++ namespace.
*/
namespace numEval
{
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wsign-compare"
#endif
#include "grammar.c"
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
} /* namespace numEval */
NumericEvaluator :: NumericEvaluator() : pClParser(0)
{
cClDecSep = '.';
bClTextInputStorage = true;
init();
}
NumericEvaluator :: ~NumericEvaluator()
{
numEval::ParseFree(pClParser, free);
// Allow explicit call to destructor
pClParser = nullptr;
clear();
}
void
NumericEvaluator :: init()
{
if (pClParser == nullptr)
pClParser = numEval::ParseAlloc(malloc);
//numEval::ParseTrace(stdout, "lib");
#if TESTMODE
eClUnitDefault = Unit::Metric;
#else
switch (g_UserUnit)
{
case INCHES : eClUnitDefault = Unit::Inch; break;
case MILLIMETRES : eClUnitDefault = Unit::Metric; break;
default: eClUnitDefault = Unit::Metric; break;
}
#endif
}
void
NumericEvaluator :: clear()
{
free(clToken.token);
clToken.token = nullptr;
clToken.input = nullptr;
bClError = true;
}
void
NumericEvaluator :: parse(int token, numEval::TokenType value)
{
numEval::Parse(pClParser, token, value, this);
}
void
NumericEvaluator :: parseError(const char* s)
{
bClError = true;
}
void
NumericEvaluator :: parseOk()
{
bClError = false;
bClParseFinished = true;
}
void
NumericEvaluator :: parseSetResult(double val)
{
snprintf(clToken.token, clToken.OutLen, "%g", val);
}
const char*
NumericEvaluator :: textInput(const void* pObj) const
{
auto it = clObjMap.find(pObj);
if (it != clObjMap.end()) return it->second.c_str();
return nullptr;
}
bool
NumericEvaluator :: process(const char* s)
{
/* Process new string.
* Feed parser token after token until end of input.
*/
newString(s);
if (pClParser == nullptr) init();
bClParseFinished = false;
Token tok;
numEval::TokenType parseTok;
do {
tok = getToken();
parse(tok.token, tok.value);
if (bClParseFinished || tok.token == ENDS) {
numEval::Parse(pClParser, 0, parseTok, this);
break;
}
//usleep(200000);
} while (tok.token);
return !bClError;
}
bool
NumericEvaluator :: process(const char* s, const void* pObj)
{
if (bClTextInputStorage) // Store input string for (text entry) pObj.
clObjMap[pObj] = s;
return process(s);
}
void
NumericEvaluator :: newString(const char* s)
{
clear();
auto len = strlen(s);
clToken.token = reinterpret_cast<decltype(clToken.token)>(malloc(TokenStat::OutLen+1));
clToken.inputLen = len;
clToken.pos = 0;
clToken.input = s;
bClParseFinished = false;
}
NumericEvaluator::Token
NumericEvaluator :: getToken()
{
Token retval;
size_t idx;
retval.token = ENDS;
retval.value.dValue = 0;
if (clToken.token == nullptr) return retval;
if (clToken.input == nullptr) return retval;
if (clToken.pos >= clToken.inputLen) return retval;
// Lambda: get value as string, store into clToken.token and update current index.
auto extractNumber = [&idx, this]() {
short sepCount = 0;
idx = 0;
auto ch = clToken.input[clToken.pos];
do {
if (ch == cClDecSep && sepCount) break;
clToken.token[idx++] = ch;
if (ch == cClDecSep) sepCount++;
ch = clToken.input[++clToken.pos];
} while (isdigit(ch) || ch == cClDecSep);
clToken.token[idx] = 0;
};
// Lamda: Get unit for current token. Returns Unit::Invalid if token is not a unit.
auto checkUnit = [this]() -> Unit {
// '"' or "mm" or "mil"
char ch = clToken.input[clToken.pos];
if (ch == '"' || ch == 'm') {
Unit convertFrom = Unit::Invalid;
if (ch == '"') {
convertFrom = Unit::Inch;
clToken.pos++;
}
else {
// Do not use strcasecmp() as it is not available on all platforms
const char* cptr = &clToken.input[clToken.pos];
const auto sizeLeft = clToken.inputLen - clToken.pos;
if (sizeLeft >= 2) {
if (tolower(cptr[1]) == 'm' && !isalnum(cptr[2])) {
convertFrom = Unit::Metric;
clToken.pos += 2;
}
else if (sizeLeft >= 3) {
if (tolower(cptr[1]) == 'i' && tolower(cptr[2]) == 'l' && !isalnum(cptr[3])) {
convertFrom = Unit::Mil;
clToken.pos += 3;
}
}
}
}
return convertFrom;
}
return Unit::Invalid;
};
// Start processing of first/next token: Remove whitespace
char ch;
for (;;) {
ch = clToken.input[clToken.pos];
if (ch == ' ') {
clToken.pos++;
}
else break;
}
Unit convertFrom;
if (ch == 0) {
/* End of input */
}
else if (isdigit(ch) || ch == cClDecSep) { // VALUE
extractNumber();
retval.token = VALUE;
retval.value.dValue = atof(clToken.token);
}
else if ((convertFrom = checkUnit()) != Unit::Invalid) { // UNIT
/* Units are appended to a VALUE.
* Determine factor to default unit if unit for value is given.
* Example: Default is mm, unit is inch: factor is 25.4
* The factor is assigned to the terminal UNIT. The actual
* conversion is done within a parser action.
*/
retval.token = UNIT;
if (eClUnitDefault == Unit::Metric)
{
switch (convertFrom)
{
case Unit::Inch : retval.value.dValue = 25.4; break;
case Unit::Mil : retval.value.dValue = 25.4/1000.0; break;
case Unit::Metric : retval.value.dValue = 1.0; break;
case Unit::Invalid : break;
}
}
else if (eClUnitDefault == Unit::Inch)
{
switch (convertFrom)
{
case Unit::Inch : retval.value.dValue = 1.0; break;
case Unit::Mil : retval.value.dValue = 1.0/1000.0; break;
case Unit::Metric : retval.value.dValue = 1.0/25.4; break;
case Unit::Invalid : break;
}
}
}
else if (isalpha(ch)) { // VAR
const char* cptr = &clToken.input[clToken.pos];
cptr++;
while (isalnum(*cptr)) cptr++;
retval.token = VAR;
size_t bytesToCopy = cptr - &clToken.input[clToken.pos];
if (bytesToCopy >= sizeof(retval.value.text)) bytesToCopy = sizeof(retval.value.text)-1;
strncpy(retval.value.text, &clToken.input[clToken.pos], bytesToCopy);
retval.value.text[bytesToCopy] = 0;
clToken.pos += cptr - &clToken.input[clToken.pos];
}
else { // Single char tokens
switch (ch) {
case '+' : retval.token = PLUS; break;
case '-' : retval.token = MINUS; break;
case '*' : retval.token = MULT; break;
case '/' : retval.token = DIVIDE; break;
case '(' : retval.token = PARENL; break;
case ')' : retval.token = PARENR; break;
case '=' : retval.token = ASSIGN; break;
case ';' : retval.token = SEMCOL; break;
}
clToken.pos++;
}
return retval;
}
void
NumericEvaluator :: setVar(const std::string& s, double value)
{
clVarMap[s] = value;
}
double
NumericEvaluator :: getVar(const std::string& s)
{
auto result = clVarMap.find(s);
if (result != clVarMap.end()) return result->second;
return 0.0;
}

12
include/libeval/grammar.h Normal file
View File

@ -0,0 +1,12 @@
#define VAR 1
#define ASSIGN 2
#define UNIT 3
#define SEMCOL 4
#define PLUS 5
#define MINUS 6
#define DIVIDE 7
#define MULT 8
#define ENDS 9
#define VALUE 10
#define PARENL 11
#define PARENR 12

View File

@ -0,0 +1,193 @@
/*
This file is part of libeval, a simple math expression evaluator
Copyright (C) 2017 Michael Geselbracht, mgeselbracht3@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
An evaluator object is used to replace an input string that represents
a mathematical expression by its result.
Example: Consider the input "3+4". The result of this expression is "7".
The NumericEvaluator can be used like this:
NumericEvaluator eval;
eval.process("3+4");
printf("3+4", eval.result());
The same example with error checking. Please note that even a valid input string may result
in an empty output string or "NaN".
NumericEvaluator eval;
bool ret = eval.process("3+4");
assert(ret == eval.isValid()); // isValid() reflects return value of process().
if (eval.isValid()) printf("3+4=%s\n", eval.result());
Using variables
Expressions can refer to variables if they were defined by previous expressions.
A variable can be defined by an expression or by the setVar() method.
Expressions that define/set variables do not have a result.
eval.process("x=1; y=2"); // Result is NaN
eval.setVar("z", 3);
eval.process("x+y+z");
printf("x+y+z=%s\n", eval.result());
Input string storage
An evaluator object can store and retrieve the original input string using a pointer
as key. This can be used to restore the input string of a text entry field.
eval.process("25.4-0.7", &eval);
printf("%s = %s\n", eval.textInput(&eval), eval.result());
Unit conversion
The evaluator uses a default unit and constants can be specified with a unit.
As long as no units are used the default unit is not relevant. The default
unit is taken from the global (Kicad) variable g_UserUnit.
Supported units are millimeters (mm), Mil (mil) and inch (")
eval.process("1\"");
printf("1\" = %s\n", eval.result());
eval.process("12.7 - 0.1\" - 50mil");
printf("12.7 - 0.1\" - 50mil = %s\n", eval.result());
*/
#ifndef NUMERIC_EVALUATOR_H_
#define NUMERIC_EVALUATOR_H_
#include "grammar.h"
#include <stddef.h>
#include <string>
#include <string>
#include <map>
// This namespace is used for the lemon parser
namespace numEval
{
struct TokenType
{
union {
double dValue;
int iValue;
};
bool valid;
char text[32];
};
} // namespace numEval
class NumericEvaluator {
enum class Unit { Invalid, Metric, Inch, Mil };
public:
NumericEvaluator();
~NumericEvaluator();
/* Initialization and destruction. init() is invoked be the constructor and should not be needed
* by the user.
* clear() should be invoked by the user if a new input string is to be processed. It will reset
* the parser. User defined variables are retained.
*/
void init();
void clear();
/* Set the decimal separator for the input string. Defaults to '.' */
void setDecimalSeparator(char sep);
/* Enable or disable support for input string storage.
* If enabled the input string is saved if process(const char*, const void*) is used.
*/
void enableTextInputStorage(bool w) { bClTextInputStorage = w; }
/* Used by the lemon parser */
void parse(int token, numEval::TokenType value);
void parseError(const char* s);
void parseOk();
void parseSetResult(double);
/* Check if previous invokation of process() was successful */
inline bool isValid() const { return !bClError; }
/* Result of string processing. Undefined if !isValid() */
inline const char* result() const { return clToken.token; }
/* Evaluate input string.
* Result can be retrieved by result().
* Returns true if input string could be evaluated, otherwise false.
*/
bool process(const char* s);
/* Like process(const char*) but also stores input string in a std:map with key pObj. */
bool process(const char* s, const void* pObj);
/* Retrieve old input string with key pObj. */
const char* textInput(const void* pObj) const;
/* Add/set variable with value */
void setVar(const std::string&, double value);
/* Get value of variable. Returns 0.0 if not defined. */
double getVar(const std::string&);
/* Remove single variable */
void removeVar(const std::string& s) { clVarMap.erase(s); }
/* Remove all variables */
void clearVar() { clVarMap.clear(); }
protected:
/* Token type used by the tokenizer */
struct Token {
int token;
numEval::TokenType value;
};
/* Begin processing of a new input string */
void newString(const char* s);
/* Tokenizer: Next token/value taken from input string. */
Token getToken();
private:
void* pClParser; // the current lemon parser state machine
/* Token state for input string. */
struct TokenStat {
enum { OutLen=32 };
TokenStat() : input(0), token(0), inputLen(0) { /* empty */ }
const char* input; // current input string ("var=4")
char* token; // output token ("var", type:VAR; "4", type:VALUE)
size_t inputLen; // strlen(input)
size_t pos; // current index
} clToken;
char cClDecSep; // decimal separator ('.')
/* Parse progress. Set by parser actions. */
bool bClError;
bool bClParseFinished;
bool bClTextInputStorage; // Enable input string storage used by process(const char*, const void*)
Unit eClUnitDefault; // Default unit for values
std::map<const void*, std::string> clObjMap; // Map pointer to text entry -> (original) input string
std::map<std::string, double> clVarMap;
};
#endif /* NUMERIC_EVALUATOR_H_ */