Browse Source

Add additional operator precedence in data parser. Add more tests. Add format() and round() transform functions to data parser.

Michael Ragazzon 5 years ago
parent
commit
3e8980938b

+ 0 - 2
Include/RmlUi/Core/DataTypes.h

@@ -31,8 +31,6 @@
 
 #include "Header.h"
 #include "Types.h"
-#include "Traits.h"
-#include "Variant.h"
 #include <functional>
 
 namespace Rml {

+ 4 - 0
Include/RmlUi/Core/Math.h

@@ -138,6 +138,10 @@ RMLUICORE_API float SquareRoot(float value);
 RMLUICORE_API float RoundFloat(float value);
 /// Rounds a floating-point value to the nearest integer.
 /// @param[in] value The value to round.
+/// @return The rounded integer as double.
+RMLUICORE_API double RoundFloat(double value);
+/// Rounds a floating-point value to the nearest integer.
+/// @param[in] value The value to round.
 /// @return The rounded integer.
 RMLUICORE_API int RoundToInteger(float value);
 /// Rounds a floating-point value up to the nearest integer.

+ 13 - 6
Source/Core/DataModel.cpp

@@ -374,8 +374,8 @@ static struct TestDataVariables {
 		handle.Bind("data", &data);
 
 		{
-			std::vector<String> test_addresses = { "data.more_fun[1].magic[3]", "data.fun.x", "data.valid" };
-			std::vector<String> expected_results = { ToString(data.more_fun[1].magic[3]), ToString(data.fun.x), ToString(data.valid) };
+			std::vector<String> test_addresses = { "data.more_fun[1].magic[3]", "data.more_fun[1].magic.size", "data.fun.x", "data.valid" };
+			std::vector<String> expected_results = { ToString(data.more_fun[1].magic[3]), ToString(int(data.more_fun[1].magic.size())), ToString(data.fun.x), ToString(data.valid) };
 
 			std::vector<String> results;
 
@@ -390,14 +390,21 @@ static struct TestDataVariables {
 
 			RMLUI_ASSERT(results == expected_results);
 
-			bool success = model.GetVariable(ParseAddress("data.more_fun[1].magic[1]")).Set(Variant(String("199")));
+			bool success = true;
+			success &= model.GetVariable(ParseAddress("data.more_fun[1].magic[1]")).Set(Variant(String("199")));
 			RMLUI_ASSERT(success && data.more_fun[1].magic[1] == 199);
 
 			data.fun.magic = { 99, 190, 55, 2000, 50, 60, 70, 80, 90 };
 
-			Variant test_get_result;
-			bool test_get_success = model.GetVariable(ParseAddress("data.fun.magic[8]")).Get(test_get_result);
-			RMLUI_ASSERT(test_get_success && test_get_result.Get<String>() == "90");
+			Variant get_result;
+
+			const int magic_size = int(data.fun.magic.size());
+			success &= model.GetVariable(ParseAddress("data.fun.magic.size")).Get(get_result);
+			RMLUI_ASSERT(success && get_result.Get<String>() == ToString(magic_size));
+			RMLUI_ASSERT(model.GetVariable(ParseAddress("data.fun.magic")).Size() == magic_size);
+
+			success &= model.GetVariable(ParseAddress("data.fun.magic[8]")).Get(get_result);
+			RMLUI_ASSERT(success && get_result.Get<String>() == "90");
 		}
 	}
 } test_data_variables;

+ 133 - 49
Source/Core/DataParser.cpp

@@ -39,7 +39,6 @@
 namespace Rml {
 namespace Core {
 
-class Interpreter;
 class DataParser;
 
 /*
@@ -263,9 +262,12 @@ namespace Parse {
 	// Forward declare all parse functions.
 	static void Assignment(DataParser& parser);
 
+	// The following in order of precedence.
 	static void Expression(DataParser& parser);
-	static void Factor(DataParser& parser);
+	static void Relational(DataParser& parser);
+	static void Additive(DataParser& parser);
 	static void Term(DataParser& parser);
+	static void Factor(DataParser& parser);
 
 	static void NumberLiteral(DataParser& parser);
 	static void StringLiteral(DataParser& parser);
@@ -381,16 +383,14 @@ namespace Parse {
 	}
 	static void Expression(DataParser& parser)
 	{
-		Term(parser);
+		Relational(parser);
 
 		bool looping = true;
 		while (looping)
 		{
 			switch (char c = parser.Look())
 			{
-			case '+': Add(parser); break;
-			case '-': Subtract(parser); break;
-			case '?': Ternary(parser); break;
+			case '&': And(parser); break;
 			case '|':
 			{
 				parser.Match('|', false);
@@ -409,19 +409,50 @@ namespace Parse {
 				}
 			}
 			break;
-			case '&': And(parser); break;
+			case '?': Ternary(parser); break;
+			default:
+				looping = false;
+			}
+		}
+	}
+
+	static void Relational(DataParser& parser)
+	{
+		Additive(parser);
+
+		bool looping = true;
+		while (looping)
+		{
+			switch (const char c = parser.Look())
+			{
 			case '=': Equal(parser); break;
 			case '!': NotEqual(parser); break;
 			case '<': Less(parser); break;
 			case '>': Greater(parser); break;
-			case '\0':
+			default:
 				looping = false;
-				break;
+			}
+		}
+	}
+	
+	static void Additive(DataParser& parser)
+	{
+		Term(parser);
+
+		bool looping = true;
+		while (looping)
+		{
+			switch (const char c = parser.Look())
+			{
+			case '+': Add(parser); break;
+			case '-': Subtract(parser); break;
 			default:
 				looping = false;
 			}
 		}
 	}
+
+
 	static void Term(DataParser& parser)
 	{
 		Factor(parser);
@@ -433,7 +464,6 @@ namespace Parse {
 			{
 			case '*': Multiply(parser); break;
 			case '/': Divide(parser); break;
-			case '\0': looping = false; break;
 			default:
 				looping = false;
 			}
@@ -591,7 +621,7 @@ namespace Parse {
 		// We already skipped the first '|' during expression
 		parser.Match('|');
 		parser.Push();
-		Term(parser);
+		Relational(parser);
 		parser.Pop(Register::L);
 		parser.Emit(Instruction::Or);
 	}
@@ -600,7 +630,7 @@ namespace Parse {
 		parser.Match('&', false);
 		parser.Match('&');
 		parser.Push();
-		Term(parser);
+		Relational(parser);
 		parser.Pop(Register::L);
 		parser.Emit(Instruction::And);
 	}
@@ -616,7 +646,7 @@ namespace Parse {
 			parser.SkipWhitespace();
 		}
 		parser.Push();
-		Term(parser);
+		Additive(parser);
 		parser.Pop(Register::L);
 		parser.Emit(instruction);
 	}
@@ -632,7 +662,7 @@ namespace Parse {
 			parser.SkipWhitespace();
 		}
 		parser.Push();
-		Term(parser);
+		Additive(parser);
 		parser.Pop(Register::L);
 		parser.Emit(instruction);
 	}
@@ -641,7 +671,7 @@ namespace Parse {
 		parser.Match('=', false);
 		parser.Match('=');
 		parser.Push();
-		Term(parser);
+		Additive(parser);
 		parser.Pop(Register::L);
 		parser.Emit(Instruction::Equal);
 	}
@@ -650,7 +680,7 @@ namespace Parse {
 		parser.Match('!', false);
 		parser.Match('=');
 		parser.Push();
-		Term(parser);
+		Additive(parser);
 		parser.Pop(Register::L);
 		parser.Emit(Instruction::NotEqual);
 	}
@@ -717,18 +747,6 @@ namespace Parse {
 
 
 
-static String DumpProgram(const Program& program)
-{
-	String str;
-	for (size_t i = 0; i < program.size(); i++)
-	{
-		String instruction_str = program[i].data.Get<String>();
-		str += CreateString(instruction_str.size(), "  %4d  '%c'  %s\n", i, char(program[i].instruction), instruction_str.c_str());
-	}
-	return str;
-}
-
-
 class DataInterpreter {
 public:
 	DataInterpreter(const Program& program, const AddressList& addresses, DataExpressionInterface expression_interface) : program(program), addresses(addresses), expression_interface(expression_interface) {}
@@ -758,7 +776,7 @@ public:
 
 		if(!success)
 		{
-			String program_str = DumpProgram(program);
+			String program_str = DumpProgram();
 			Log::Message(Log::LT_WARNING, "Failed to execute program with %d instructions:", program.size());
 			Log::Message(Log::LT_WARNING, program_str.c_str());
 		}
@@ -766,6 +784,17 @@ public:
 		return success;
 	}
 
+	String DumpProgram() const
+	{
+		String str;
+		for (size_t i = 0; i < program.size(); i++)
+		{
+			String instruction_str = program[i].data.Get<String>();
+			str += CreateString(50 + instruction_str.size(), "  %4d  '%c'  %s\n", i, char(program[i].instruction), instruction_str.c_str());
+		}
+		return str;
+	}
+
 	Variant Result() const {
 		return R;
 	}
@@ -947,30 +976,59 @@ struct TestParser {
 	TestParser() : model(type_register.GetTransformFuncRegister())
 	{
 		DataModelConstructor handle(&model, &type_register);
+		handle.Bind("radius", &radius);
 		handle.Bind("color_name", &color_name);
 		handle.BindFunc("color_value", [this](Rml::Core::Variant& variant) {
 			variant = ToString(color_value);
 		});
 
 		String result;
-		result = TestExpression("!!10 - 1 ? 'hello' : 'world' | to_upper");
-		result = TestExpression("(color_name) + (': rgba(' + color_value + ')')");
-		result = TestExpression("'hello world' | to_upper(5 + 12 == 17 ? 'yes' : 'no', 9*2)");
-		result = TestExpression("true == false");
-		result = TestExpression("true != false");
-		result = TestExpression("true");
-		result = TestExpression(R"(true || false ? (true && true ? 'Absolutely!' : 'well..') : 'no')");
-		result = TestExpression("2 * 2");
-		result = TestExpression("50000 / 1500");
-		result = TestExpression("5*1+2");
-		result = TestExpression("5*(1+2)");
-		result = TestExpression("5.2 + 19 + 'px'");
+		result = TestExpression("!!10 - 1 ? 'hello' : 'world' | to_upper",         "WORLD");
+		result = TestExpression("(color_name) + (': rgba(' + color_value + ')')",  "color: rgba(180, 100, 255, 255)");
+		result = TestExpression("'hello world' | to_upper(5 + 12 == 17 ? 'yes' : 'no', 9*2)",  "HELLO WORLD");
+		result = TestExpression("true == false",  "0");
+		result = TestExpression("true != false",  "1");
+		result = TestExpression("true",           "1");
+
+		result = TestExpression("true || false ? (true && 3==1+2 ? 'Absolutely!' : 'well..') : 'no'",  "Absolutely!");
+		result = TestExpression(R"('It\'s a fit')",  R"(It's a fit)");
+		result = TestExpression("2 * 2",           "4");
+		result = TestExpression("50000 / 1500",    "33.333");
+		result = TestExpression("5*1+2",           "7");
+		result = TestExpression("5*(1+2)",         "15");
+		result = TestExpression("2*(-2)/4",        "-1");
+		result = TestExpression("5.2 + 19 + 'px'", "24.2px");
+
+		result = TestExpression("radius + 'm'",    "8.7m");
+		result = TestExpression("radius < 10.5 ? 'smaller' : 'larger'",  "smaller");
+		TestAssignment("radius = 15");
+		result = TestExpression("radius < 10.5 ? 'smaller' : 'larger'",  "larger");
+		TestAssignment("radius = 4; color_name = 'image-color'");
+		result = TestExpression("radius == 4 && color_name == 'image-color'",  "1");
+
+		result = TestExpression("5 == 1 + 2*2 || 8 == 1 + 4  ? 'yes' : 'no'",  "yes");
+		result = TestExpression("!!('fa' + 'lse')", "0");
+		result = TestExpression("!!('tr' + 'ue')", "1");
+		result = TestExpression("'fox' + 'dog' ? 'FoxyDog' : 'hot' + 'dog' | to_upper", "HOTDOG");
+
+		result = TestExpression("3.62345 | round", "4");
+		result = TestExpression("3.62345 | format(0)", "4");
+		result = TestExpression("3.62345 | format(2)", "3.62");
+		result = TestExpression("3.62345 | format(10)", "3.6234500000");
+		result = TestExpression("3.62345 | format(10, true)", "3.62345");
+		result = TestExpression("3.62345 | round | format(2)", "4.00");
+		result = TestExpression("3.0001 | format(2, false)", "3.00");
+		result = TestExpression("3.0001 | format(2, true)"), "3";
+
+		result = TestExpression("0.2 + 3.42345 | round", "4");
+		result = TestExpression("(3.42345 | round) + 0.2", "3.2");
+		result = TestExpression("(3.42345 | format(0)) + 0.2", "30.2"); // Here, format(0) returns a string, so the + means string concatenation.
 	}
 
-	String TestExpression(String expression)
+	String TestExpression(String expression, String expected = String())
 	{
+		String result;
 		DataExpressionInterface interface(&model, nullptr);
-
 		DataParser parser(expression, interface);
 		if (parser.Parse(false))
 		{
@@ -979,14 +1037,44 @@ struct TestParser {
 
 			DataInterpreter interpreter(program, addresses, interface);
 			if (interpreter.Run())
-				return interpreter.Result().Get<String>();
+				result = interpreter.Result().Get<String>();
+
+			if (!expected.empty() && result != expected)
+			{
+				String program_str = interpreter.DumpProgram();
+				Log::Message(Log::LT_WARNING, "%s", program_str.c_str());
+				RMLUI_ERRORMSG("Got unexpected data parser result.");
+			}
+		}
+		else
+		{
+			RMLUI_ERRORMSG("Could not parse expression.");
+		}
+
+		return result;
+	};
+
+	bool TestAssignment(String expression)
+	{
+		bool result = false;
+		DataExpressionInterface interface(&model, nullptr);
+		DataParser parser(expression, interface);
+		if (parser.Parse(true))
+		{
+			Program program = parser.ReleaseProgram();
+			AddressList addresses = parser.ReleaseAddresses();
+
+			DataInterpreter interpreter(program, addresses, interface);
+			result = interpreter.Run();
 		}
-		return "<invalid expression>";
+		RMLUI_ASSERT(result);
+		return result;
 	};
 
 	DataTypeRegister type_register;
 	DataModel model;
 
+	float radius = 8.7f;
 	String color_name = "color";
 	Colourb color_value = Colourb(180, 100, 255);
 };
@@ -1005,10 +1093,6 @@ bool DataExpression::Parse(const DataExpressionInterface& expression_interface,
 	// @todo: Remove, debugging only
 	static TestParser test_parser;
 
-	// TODO:
-	//  3. Create a plug-in wrapper for use by scripting languages to replace this parser. Design wrapper as for events.
-	//  5. Add tests
-
 	DataParser parser(expression, expression_interface);
 	if (!parser.Parse(is_assignment_expression))
 		return false;

+ 43 - 0
Source/Core/DataTypeRegister.cpp

@@ -52,6 +52,49 @@ DataTypeRegister::DataTypeRegister()
 		variant = StringUtilities::ToUpper(value);
 		return true;
 	});
+
+	transform_register.Register("format", [](Variant& variant, const VariantList& arguments) -> bool {
+        // Arguments in:
+        //   0 : int[0,32]  Precision. Number of digits after the decimal point.
+        //  [1]: bool       True to remove trailing zeros (default = false).
+        if (arguments.size() < 1 || arguments.size() > 2) {
+            Log::Message(Log::LT_WARNING, "Transform function 'format' requires at least one argument, at most two arguments.");
+            return false;
+        }
+        int precision = 0;
+        if (!arguments[0].GetInto(precision) || precision < 0 || precision > 32) {
+            Log::Message(Log::LT_WARNING, "Transform function 'format': First argument must be an integer in [0, 32].");
+            return false;
+        }
+        bool remove_trailing_zeros = false;
+        if (arguments.size() >= 2) {
+            if (!arguments[1].GetInto(remove_trailing_zeros))
+                return false;
+        }
+
+		double value = 0;
+		if (!variant.GetInto(value))
+			return false;
+
+        String format_specifier = String(remove_trailing_zeros ? "%#." : "%.") + ToString(precision) + 'f';
+        String result;
+        if (FormatString(result, 64, format_specifier.c_str(), value) == 0)
+            return false;
+
+        if (remove_trailing_zeros)
+            StringUtilities::TrimTrailingDotZeros(result);
+
+        variant = result;
+		return true;
+	});
+
+	transform_register.Register("round", [](Variant& variant, const VariantList& arguments) -> bool {
+		double value = 0;
+		if (!variant.GetInto(value))
+			return false;
+        variant = Math::RoundFloat(value);
+		return true;
+	});
 }
 
 DataTypeRegister::~DataTypeRegister()

+ 6 - 0
Source/Core/Math.cpp

@@ -130,6 +130,12 @@ RMLUICORE_API float RoundFloat(float value)
 	return roundf(value);
 }
 
+// Rounds a floating-point value to the nearest integer.
+RMLUICORE_API double RoundFloat(double value)
+{
+	return round(value);
+}
+
 // Rounds a floating-point value to the nearest integer.
 RMLUICORE_API int RoundToInteger(float value)
 {