diff --git a/src/expr.cpp b/src/expr.cpp
index 745a739a..e00a1143 100644
--- a/src/expr.cpp
+++ b/src/expr.cpp
@@ -682,6 +682,7 @@ ExprParser::Token ExprParser::Lex(std::string *error) {
Token t = Token::From();
char c = PeekChar();
if(isupper(c)) {
+ std::string n = ReadWord();
t = Token::From(TokenType::OPERAND, Expr::Op::VARIABLE);
} else if(isalpha(c)) {
std::string s = ReadWord();
@@ -796,6 +797,7 @@ bool ExprParser::Reduce(std::string *error) {
switch(op.expr->op) {
case Expr::Op::NEGATE: e = e->Negate(); break;
case Expr::Op::SQRT: e = e->Sqrt(); break;
+ case Expr::Op::SQUARE: e = e->Times(e); break;
case Expr::Op::SIN: e = e->Times(Expr::From(PI/180))->Sin(); break;
case Expr::Op::COS: e = e->Times(Expr::From(PI/180))->Cos(); break;
case Expr::Op::ASIN: e = e->ASin()->Times(Expr::From(180/PI)); break;
@@ -883,6 +885,10 @@ Expr *ExprParser::Parse(const char *input, std::string *error) {
return r.expr;
}
+Expr *Expr::Parse(const char *input, std::string *error) {
+ return ExprParser::Parse(input, error);
+}
+
Expr *Expr::From(const char *input, bool popUpError) {
std::string error;
Expr *e = ExprParser::Parse(input, &error);
diff --git a/src/expr.h b/src/expr.h
index b02d903a..ea113105 100644
--- a/src/expr.h
+++ b/src/expr.h
@@ -96,6 +96,7 @@ public:
Expr *DeepCopyWithParamsAsPointers(IdList *firstTry,
IdList *thenTry) const;
+ static Expr *Parse(const char *input, std::string *error);
static Expr *From(const char *in, bool popUpError);
};
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index ca91188c..271751d9 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -11,6 +11,7 @@ endforeach()
set(testsuite_SOURCES
harness.cpp
+ core/expr/test.cpp
constraint/points_coincident/test.cpp
constraint/pt_pt_distance/test.cpp
constraint/pt_plane_distance/test.cpp
diff --git a/test/core/expr/test.cpp b/test/core/expr/test.cpp
new file mode 100644
index 00000000..ab3829b5
--- /dev/null
+++ b/test/core/expr/test.cpp
@@ -0,0 +1,106 @@
+#include "harness.h"
+
+#define CHECK_PARSE(var, expr) \
+ do { \
+ var = Expr::From(expr, false); \
+ CHECK_TRUE(var != NULL); \
+ } while(0)
+
+#define CHECK_PARSE_ERR(expr, msg) \
+ do { \
+ std::string err; \
+ Expr *e = Expr::Parse(expr, &err); \
+ CHECK_TRUE(e == NULL); \
+ CHECK_TRUE(err.find(msg) != std::string::npos); \
+ } while(0)
+
+TEST_CASE(constant) {
+ Expr *e;
+ CHECK_PARSE(e, "pi");
+ CHECK_EQ_EPS(e->Eval(), 3.1415926);
+}
+
+TEST_CASE(literal) {
+ Expr *e;
+ CHECK_PARSE(e, "42");
+ CHECK_TRUE(e->Eval() == 42);
+ CHECK_PARSE(e, "42.5");
+ CHECK_TRUE(e->Eval() == 42.5);
+}
+
+TEST_CASE(unary_ops) {
+ Expr *e;
+ CHECK_PARSE(e, "-10");
+ CHECK_TRUE(e->Eval() == -10);
+}
+
+TEST_CASE(binary_ops) {
+ Expr *e;
+ CHECK_PARSE(e, "1 + 2");
+ CHECK_TRUE(e->Eval() == 3);
+ CHECK_PARSE(e, "1 - 2");
+ CHECK_TRUE(e->Eval() == -1);
+ CHECK_PARSE(e, "3 * 4");
+ CHECK_TRUE(e->Eval() == 12);
+ CHECK_PARSE(e, "3 / 4");
+ CHECK_TRUE(e->Eval() == 0.75);
+}
+
+TEST_CASE(parentheses) {
+ Expr *e;
+ CHECK_PARSE(e, "(1 + 2) * 3");
+ CHECK_TRUE(e->Eval() == 9);
+ CHECK_PARSE(e, "1 + (2 * 3)");
+ CHECK_TRUE(e->Eval() == 7);
+}
+
+TEST_CASE(functions) {
+ Expr *e;
+ CHECK_PARSE(e, "sqrt(2)");
+ CHECK_EQ_EPS(e->Eval(), 1.414213);
+ CHECK_PARSE(e, "square(3)");
+ CHECK_EQ_EPS(e->Eval(), 9);
+ CHECK_PARSE(e, "sin(180)");
+ CHECK_EQ_EPS(e->Eval(), 0);
+ CHECK_PARSE(e, "sin(90)");
+ CHECK_EQ_EPS(e->Eval(), 1);
+ CHECK_PARSE(e, "cos(180)");
+ CHECK_EQ_EPS(e->Eval(), -1);
+ CHECK_PARSE(e, "asin(1)");
+ CHECK_EQ_EPS(e->Eval(), 90);
+ CHECK_PARSE(e, "acos(0)");
+ CHECK_EQ_EPS(e->Eval(), 90);
+}
+
+TEST_CASE(variable) {
+ Expr *e;
+ CHECK_PARSE(e, "Var");
+ CHECK_TRUE(e->op == Expr::Op::VARIABLE);
+}
+
+TEST_CASE(precedence) {
+ Expr *e;
+ CHECK_PARSE(e, "2 + 3 * 4");
+ CHECK_TRUE(e->Eval() == 14);
+ CHECK_PARSE(e, "2 - 3 / 4");
+ CHECK_TRUE(e->Eval() == 1.25);
+ CHECK_PARSE(e, "-3 + 2");
+ CHECK_TRUE(e->Eval() == -1);
+ CHECK_PARSE(e, "2 + 3 - 4");
+ CHECK_TRUE(e->Eval() == 1);
+}
+
+TEST_CASE(errors) {
+ CHECK_PARSE_ERR("\x01",
+ "Unexpected character");
+ CHECK_PARSE_ERR("notavar",
+ "'notavar' is not a valid variable, function or constant");
+ CHECK_PARSE_ERR("_",
+ "'_' is not a valid operator");
+ CHECK_PARSE_ERR("2 2",
+ "Expected an operator");
+ CHECK_PARSE_ERR("2 + +",
+ "Expected an operand");
+ CHECK_PARSE_ERR("( 2 + 2",
+ "Expected ')'");
+}
diff --git a/test/harness.cpp b/test/harness.cpp
index 7f3a8f04..213768cc 100644
--- a/test/harness.cpp
+++ b/test/harness.cpp
@@ -178,6 +178,18 @@ bool Test::Helper::CheckTrue(const char *file, int line, const char *expr, bool
}
}
+bool Test::Helper::CheckEqualEpsilon(const char *file, int line, const char *valueExpr,
+ double value, double reference) {
+ bool result = fabs(value - reference) < LENGTH_EPS;
+ if(!RecordCheck(result)) {
+ std::string msg = ssprintf("(%s) = %.4g ≉ %.4g", valueExpr, value, reference);
+ PrintFailure(file, line, msg);
+ return false;
+ } else {
+ return true;
+ }
+}
+
bool Test::Helper::CheckLoad(const char *file, int line, const char *fixture) {
std::string fixturePath = GetAssetPath(file, fixture);
diff --git a/test/harness.h b/test/harness.h
index 74798e01..9e434a21 100644
--- a/test/harness.h
+++ b/test/harness.h
@@ -22,6 +22,8 @@ public:
std::string mangle = "");
bool CheckTrue(const char *file, int line, const char *expr, bool result);
+ bool CheckEqualEpsilon(const char *file, int line, const char *valueExpr,
+ double value, double reference);
bool CheckLoad(const char *file, int line, const char *fixture);
bool CheckSave(const char *file, int line, const char *reference);
bool CheckRender(const char *file, int line, const char *fixture);
@@ -51,6 +53,9 @@ using namespace SolveSpace;
#define CHECK_TRUE(cond) \
do { if(!helper->CheckTrue(__FILE__, __LINE__, #cond, cond)) return; } while(0)
+#define CHECK_EQ_EPS(value, reference) \
+ do { if(!helper->CheckEqualEpsilon(__FILE__, __LINE__, \
+ #value, value, reference)) return; } while(0)
#define CHECK_LOAD(fixture) \
do { if(!helper->CheckLoad(__FILE__, __LINE__, fixture)) return; } while(0)
#define CHECK_SAVE(fixture) \