summaryrefslogtreecommitdiffstats
path: root/src/util/expression-evaluator.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/expression-evaluator.cpp')
-rw-r--r--src/util/expression-evaluator.cpp396
1 files changed, 396 insertions, 0 deletions
diff --git a/src/util/expression-evaluator.cpp b/src/util/expression-evaluator.cpp
new file mode 100644
index 0000000..24a56b7
--- /dev/null
+++ b/src/util/expression-evaluator.cpp
@@ -0,0 +1,396 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+/** @file
+ * TODO: insert short description here
+ */
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * Original file from libgimpwidgets: gimpeevl.c
+ * Copyright (C) 2008 Fredrik Alstromer <roe@excu.se>
+ * Copyright (C) 2008 Martin Nordholts <martinn@svn.gnome.org>
+ * Modified for Inkscape by Johan Engelen
+ * Copyright (C) 2011 Johan Engelen
+ * Copyright (C) 2013 Matthew Petroff
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include "util/expression-evaluator.h"
+#include "util/units.h"
+
+#include <glib/gconvert.h>
+
+#include <cmath>
+#include <cstring>
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace Util {
+
+EvaluatorQuantity::EvaluatorQuantity(double value, unsigned int dimension) :
+ value(value),
+ dimension(dimension)
+{
+}
+
+EvaluatorToken::EvaluatorToken()
+{
+ type = 0;
+ value.fl = 0;
+}
+
+ExpressionEvaluator::ExpressionEvaluator(const char *string, Unit const *unit) :
+ string(g_locale_to_utf8(string,-1,nullptr,nullptr,nullptr)),
+ unit(unit)
+{
+ current_token.type = TOKEN_END;
+
+ // Preload symbol
+ parseNextToken();
+}
+
+/**
+ * Evaluates the given arithmetic expression, along with an optional dimension
+ * analysis, and basic unit conversions.
+ *
+ * All units conversions factors are relative to some implicit
+ * base-unit. This is also the unit of the returned value.
+ *
+ * Returns: An EvaluatorQuantity with a value given in the base unit along with
+ * the order of the dimension (e.g. if the base unit is inches, a dimension
+ * order of two means in^2).
+ *
+ * @return Result of evaluation.
+ * @throws Inkscape::Util::EvaluatorException There was a parse error.
+ **/
+EvaluatorQuantity ExpressionEvaluator::evaluate()
+{
+ if (!g_utf8_validate(string, -1, nullptr)) {
+ throw EvaluatorException("Invalid UTF8 string", nullptr);
+ }
+
+ EvaluatorQuantity result = EvaluatorQuantity();
+ EvaluatorQuantity default_unit_factor;
+
+ // Empty expression evaluates to 0
+ if (acceptToken(TOKEN_END, nullptr)) {
+ return result;
+ }
+
+ result = evaluateExpression();
+
+ // There should be nothing left to parse by now
+ isExpected(TOKEN_END, nullptr);
+
+ resolveUnit(nullptr, &default_unit_factor, unit);
+
+ // Entire expression is dimensionless, apply default unit if applicable
+ if ( result.dimension == 0 && default_unit_factor.dimension != 0 ) {
+ result.value /= default_unit_factor.value;
+ result.dimension = default_unit_factor.dimension;
+ }
+ return result;
+}
+
+EvaluatorQuantity ExpressionEvaluator::evaluateExpression()
+{
+ bool subtract;
+ EvaluatorQuantity evaluated_terms;
+
+ evaluated_terms = evaluateTerm();
+
+ // Continue evaluating terms, chained with + or -.
+ for (subtract = FALSE;
+ acceptToken('+', nullptr) || (subtract = acceptToken('-', nullptr));
+ subtract = FALSE)
+ {
+ EvaluatorQuantity new_term = evaluateTerm();
+
+ // If dimensions mismatch, attempt default unit assignment
+ if ( new_term.dimension != evaluated_terms.dimension ) {
+ EvaluatorQuantity default_unit_factor;
+
+ resolveUnit(nullptr, &default_unit_factor, unit);
+
+ if ( new_term.dimension == 0
+ && evaluated_terms.dimension == default_unit_factor.dimension )
+ {
+ new_term.value /= default_unit_factor.value;
+ new_term.dimension = default_unit_factor.dimension;
+ } else if ( evaluated_terms.dimension == 0
+ && new_term.dimension == default_unit_factor.dimension )
+ {
+ evaluated_terms.value /= default_unit_factor.value;
+ evaluated_terms.dimension = default_unit_factor.dimension;
+ } else {
+ throwError("Dimension mismatch during addition");
+ }
+ }
+
+ evaluated_terms.value += (subtract ? -new_term.value : new_term.value);
+ }
+
+ return evaluated_terms;
+}
+
+EvaluatorQuantity ExpressionEvaluator::evaluateTerm()
+{
+ bool division;
+ EvaluatorQuantity evaluated_exp_terms = evaluateExpTerm();
+
+ for ( division = false;
+ acceptToken('*', nullptr) || (division = acceptToken('/', nullptr));
+ division = false )
+ {
+ EvaluatorQuantity new_exp_term = evaluateExpTerm();
+
+ if (division) {
+ evaluated_exp_terms.value /= new_exp_term.value;
+ evaluated_exp_terms.dimension -= new_exp_term.dimension;
+ } else {
+ evaluated_exp_terms.value *= new_exp_term.value;
+ evaluated_exp_terms.dimension += new_exp_term.dimension;
+ }
+ }
+
+ return evaluated_exp_terms;
+}
+
+EvaluatorQuantity ExpressionEvaluator::evaluateExpTerm()
+{
+ EvaluatorQuantity evaluated_signed_factors = evaluateSignedFactor();
+
+ while(acceptToken('^', nullptr)) {
+ EvaluatorQuantity new_signed_factor = evaluateSignedFactor();
+
+ if (new_signed_factor.dimension == 0) {
+ evaluated_signed_factors.value = pow(evaluated_signed_factors.value,
+ new_signed_factor.value);
+ evaluated_signed_factors.dimension *= new_signed_factor.value;
+ } else {
+ throwError("Unit in exponent");
+ }
+ }
+
+ return evaluated_signed_factors;
+}
+
+EvaluatorQuantity ExpressionEvaluator::evaluateSignedFactor()
+{
+ EvaluatorQuantity result;
+ bool negate = FALSE;
+
+ if (!acceptToken('+', nullptr)) {
+ negate = acceptToken ('-', nullptr);
+ }
+
+ result = evaluateFactor();
+
+ if (negate) {
+ result.value = -result.value;
+ }
+
+ return result;
+}
+
+EvaluatorQuantity ExpressionEvaluator::evaluateFactor()
+{
+ EvaluatorQuantity evaluated_factor = EvaluatorQuantity();
+ EvaluatorToken consumed_token = EvaluatorToken();
+
+ if (acceptToken(TOKEN_END, &consumed_token)) {
+ return evaluated_factor;
+ }
+ else if (acceptToken(TOKEN_NUM, &consumed_token)) {
+ evaluated_factor.value = consumed_token.value.fl;
+ } else if (acceptToken('(', nullptr)) {
+ evaluated_factor = evaluateExpression();
+ isExpected(')', nullptr);
+ } else {
+ throwError("Expected number or '('");
+ }
+
+ if ( current_token.type == TOKEN_IDENTIFIER ) {
+ char *identifier;
+ EvaluatorQuantity result;
+
+ acceptToken(TOKEN_ANY, &consumed_token);
+
+ identifier = g_newa(char, consumed_token.value.size + 1);
+
+ strncpy(identifier, consumed_token.value.c, consumed_token.value.size);
+ identifier[consumed_token.value.size] = '\0';
+
+ if (resolveUnit(identifier, &result, unit)) {
+ evaluated_factor.value /= result.value;
+ evaluated_factor.dimension += result.dimension;
+ } else {
+ throwError("Unit was not resolved");
+ }
+ }
+
+ return evaluated_factor;
+}
+
+bool ExpressionEvaluator::acceptToken(TokenType token_type,
+ EvaluatorToken *consumed_token)
+{
+ bool existed = FALSE;
+
+ if ( token_type == current_token.type || token_type == TOKEN_ANY ) {
+ existed = TRUE;
+
+ if (consumed_token) {
+ *consumed_token = current_token;
+ }
+
+ // Parse next token
+ parseNextToken();
+ }
+
+ return existed;
+}
+
+void ExpressionEvaluator::parseNextToken()
+{
+ const char *s;
+
+ movePastWhiteSpace();
+ s = string;
+ start_of_current_token = s;
+
+ if ( !s || s[0] == '\0' ) {
+ // We're all done
+ current_token.type = TOKEN_END;
+ } else if ( s[0] == '+' || s[0] == '-' ) {
+ // Snatch these before the g_strtod() does, otherwise they might
+ // be used in a numeric conversion.
+ acceptTokenCount(1, s[0]);
+ } else {
+ // Attempt to parse a numeric value
+ char *endptr = nullptr;
+ gdouble value = g_strtod(s, &endptr);
+
+ if ( endptr && endptr != s ) {
+ // A numeric could be parsed, use it
+ current_token.value.fl = value;
+
+ current_token.type = TOKEN_NUM;
+ string = endptr;
+ } else if (isUnitIdentifierStart(s[0])) {
+ // Unit identifier
+ current_token.value.c = s;
+ current_token.value.size = getIdentifierSize(s, 0);
+
+ acceptTokenCount(current_token.value.size, TOKEN_IDENTIFIER);
+ } else {
+ // Everything else is a single character token
+ acceptTokenCount(1, s[0]);
+ }
+ }
+}
+
+void ExpressionEvaluator::acceptTokenCount (int count, TokenType token_type)
+{
+ current_token.type = token_type;
+ string += count;
+}
+
+void ExpressionEvaluator::isExpected(TokenType token_type,
+ EvaluatorToken *value)
+{
+ if (!acceptToken(token_type, value)) {
+ throwError("Unexpected token");
+ }
+}
+
+void ExpressionEvaluator::movePastWhiteSpace()
+{
+ if (!string) {
+ return;
+ }
+
+ while (g_ascii_isspace(*string)) {
+ string++;
+ }
+}
+
+bool ExpressionEvaluator::isUnitIdentifierStart(gunichar c)
+{
+ return (g_unichar_isalpha (c)
+ || c == (gunichar) '%'
+ || c == (gunichar) '\'');
+}
+
+/**
+ * getIdentifierSize:
+ * @s:
+ * @start:
+ *
+ * Returns: Size of identifier in bytes (not including NULL
+ * terminator).
+ **/
+int ExpressionEvaluator::getIdentifierSize(const char *string, int start_offset)
+{
+ const char *start = g_utf8_offset_to_pointer(string, start_offset);
+ const char *s = start;
+ gunichar c = g_utf8_get_char(s);
+ int length = 0;
+
+ if (isUnitIdentifierStart(c)) {
+ s = g_utf8_next_char (s);
+ c = g_utf8_get_char (s);
+ length++;
+
+ while ( isUnitIdentifierStart (c) || g_unichar_isdigit (c) ) {
+ s = g_utf8_next_char(s);
+ c = g_utf8_get_char(s);
+ length++;
+ }
+ }
+
+ return g_utf8_offset_to_pointer(start, length) - start;
+}
+
+bool ExpressionEvaluator::resolveUnit (const char* identifier,
+ EvaluatorQuantity *result,
+ Unit const* unit)
+{
+ if (!unit) {
+ result->value = 1;
+ result->dimension = 1;
+ return true;
+ }else if (!identifier) {
+ result->value = 1;
+ result->dimension = unit->isAbsolute() ? 1 : 0;
+ return true;
+ } else if (unit_table.hasUnit(identifier)) {
+ Unit const *identifier_unit = unit_table.getUnit(identifier);
+ result->value = Quantity::convert(1, unit, identifier_unit);
+ result->dimension = identifier_unit->isAbsolute() ? 1 : 0;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void ExpressionEvaluator::throwError(const char *msg)
+{
+ throw EvaluatorException(msg, start_of_current_token);
+}
+
+} // namespace Util
+} // namespace Inkscape