summaryrefslogtreecommitdiff
path: root/csrc/services/quick-answers
diff options
context:
space:
mode:
authorHannes Schulze <haschu0103@outlook.de>2025-06-19 23:17:58 +0200
committerHannes Schulze <haschu0103@outlook.de>2025-06-19 23:17:58 +0200
commit1af3d14f3f995645a0de08a54b244c19840709e9 (patch)
tree3c4098863d8a8fd004a2acc8912a23b6036ab4dd /csrc/services/quick-answers
parent40efcf60a5f39674b5137f11de7bcfe37fb121b5 (diff)
init hs project
Diffstat (limited to 'csrc/services/quick-answers')
-rw-r--r--csrc/services/quick-answers/math-lexer.c248
-rw-r--r--csrc/services/quick-answers/math-parser.c414
-rw-r--r--csrc/services/quick-answers/math.c851
-rw-r--r--csrc/services/quick-answers/math.h148
-rw-r--r--csrc/services/quick-answers/math.test.c153
-rw-r--r--csrc/services/quick-answers/meson.build9
6 files changed, 1823 insertions, 0 deletions
diff --git a/csrc/services/quick-answers/math-lexer.c b/csrc/services/quick-answers/math-lexer.c
new file mode 100644
index 0000000..7fbe877
--- /dev/null
+++ b/csrc/services/quick-answers/math-lexer.c
@@ -0,0 +1,248 @@
+#include "services/quick-answers/math.h"
+
+//
+// FoobarMathToken:
+//
+// A token in a mathematical expression. The token contents are left as-is and need not be null-terminated. For example,
+// "sin(pi) - 3" would be represented as:
+// - IDENTIFIER: "sin"
+// - PAREN_OPEN: "("
+// - IDENTIFIER: "pi"
+// - PAREN_CLOSE: ")"
+// - MINUS: "-"
+// - NUMBER: "3"
+//
+
+//
+// Lexer:
+//
+// The lexer context/state which is passed around.
+//
+
+typedef struct _Lexer Lexer;
+
+struct _Lexer
+{
+ GArray* result;
+ gchar const* input;
+ gsize input_length;
+ gsize position;
+ gsize token_start;
+};
+
+static gboolean char_is_whitespace( char c );
+static gboolean char_is_digit ( char c );
+static gboolean char_is_identifier( char c );
+
+static char lexer_peek ( Lexer const* ctx,
+ gsize offset );
+static char lexer_pop ( Lexer* ctx );
+static void lexer_begin_token ( Lexer* ctx );
+static void lexer_commit_token ( Lexer* ctx,
+ FoobarMathTokenType type );
+static void lexer_commit_single( Lexer* ctx,
+ FoobarMathTokenType type );
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Lexing
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Divide an input string into a list of tokens.
+//
+// On success, this will return a newly allocated array of tokens which should be freed using g_free. The number of
+// elements will be written into out_count.
+//
+// On error, this will return NULL.
+//
+FoobarMathToken* foobar_math_lex(
+ gchar const* input,
+ gsize input_length,
+ gsize* out_count )
+{
+ g_autoptr( GArray ) result = g_array_new( FALSE, TRUE, sizeof( FoobarMathToken ) );
+
+ Lexer ctx = { 0 };
+ ctx.result = result;
+ ctx.input = input;
+ ctx.input_length = input_length;
+
+ while ( ctx.position < input_length )
+ {
+ if ( char_is_whitespace( lexer_peek( &ctx, 0 ) ) )
+ {
+ // Skip whitespace.
+
+ lexer_pop( &ctx );
+ }
+ else if ( char_is_digit( lexer_peek( &ctx, 0 ) ) )
+ {
+ // Process numbers as a whole, don't allow whitespace inbetween.
+
+ lexer_begin_token( &ctx );
+ while ( char_is_digit( lexer_peek( &ctx, 0 ) ) )
+ {
+ lexer_pop( &ctx );
+ }
+
+ if ( lexer_peek( &ctx, 0 ) == '.' && char_is_digit( lexer_peek( &ctx, 1 ) ) )
+ {
+ lexer_pop( &ctx );
+ while ( char_is_digit( lexer_peek( &ctx, 0 ) ) )
+ {
+ lexer_pop( &ctx );
+ }
+ }
+
+ lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_NUMBER );
+ }
+ else if ( char_is_identifier( lexer_peek( &ctx, 0 ) ) )
+ {
+ // Don't validate identifiers, leave that to the parser.
+
+ lexer_begin_token( &ctx );
+ while ( char_is_identifier( lexer_peek( &ctx, 0 ) ) )
+ {
+ lexer_pop( &ctx );
+ }
+ lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_IDENTIFIER );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '(' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_OPEN );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == ')' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_CLOSE );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '+' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PLUS );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '-' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MINUS );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '*' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MUL );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '/' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_DIV );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '^' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_POW );
+ }
+ else
+ {
+ // Unknown character.
+
+ return NULL;
+ }
+ }
+
+ return g_array_steal( result, out_count );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Character Classification
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Check if a character is a whitespace symbol (space, tab, carriage return).
+//
+gboolean char_is_whitespace( char c )
+{
+ return c == ' ' || c == '\t' || c == '\r';
+}
+
+//
+// Check if a character is a decimal digit.
+//
+gboolean char_is_digit( char c )
+{
+ return c >= '0' && c <= '9';
+}
+
+//
+// Check if a character belongs to an identifier (like "sin", "pi", etc.).
+//
+gboolean char_is_identifier( char c )
+{
+ // Identifiers are all lowercase ASCII.
+
+ return c >= 'a' && c <= 'z';
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// State Helpers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Peek at the current character without consuming it.
+//
+// If this is the end of the input string, return 0.
+//
+char lexer_peek(
+ Lexer const* ctx,
+ gsize offset )
+{
+ gsize position = ctx->position + offset;
+ if ( position >= ctx->input_length ) { return 0; }
+
+ return ctx->input[position];
+}
+
+//
+// Consume the current character, moving forward to the next one.
+//
+// If this is the end of the input string, return 0.
+//
+char lexer_pop( Lexer* ctx )
+{
+ if ( ctx->position >= ctx->input_length ) { return 0; }
+
+ return ctx->input[ctx->position++];
+}
+
+//
+// Start recording a token by remembering the current position.
+//
+// The current character will be the first one in the token.
+//
+void lexer_begin_token( Lexer* ctx )
+{
+ ctx->token_start = ctx->position;
+}
+
+//
+// Stop recording a token, saving the string between the call to lexer_begin_token as the specified type.
+//
+// The current character will not be a part of the token.
+//
+void lexer_commit_token(
+ Lexer* ctx,
+ FoobarMathTokenType type )
+{
+ FoobarMathToken token = { 0 };
+ token.data = &ctx->input[ctx->token_start];
+ token.length = ctx->position - ctx->token_start;
+ token.type = type;
+ g_array_append_val( ctx->result, token );
+}
+
+//
+// Consume the current character and record it as a token.
+//
+// This is equivalent to calling lexer_begin_token, lexer_pop, and lexer_commit_token.
+//
+void lexer_commit_single(
+ Lexer* ctx,
+ FoobarMathTokenType type )
+{
+ lexer_begin_token( ctx );
+ lexer_pop( ctx );
+ lexer_commit_token( ctx, type );
+}
diff --git a/csrc/services/quick-answers/math-parser.c b/csrc/services/quick-answers/math-parser.c
new file mode 100644
index 0000000..17d976d
--- /dev/null
+++ b/csrc/services/quick-answers/math-parser.c
@@ -0,0 +1,414 @@
+#include "services/quick-answers/math.h"
+
+//
+// Parser:
+//
+// The parser context/state which is passed around.
+//
+
+typedef struct _Parser Parser;
+
+struct _Parser
+{
+ FoobarMathToken const* tokens;
+ gsize tokens_count;
+ gsize position;
+};
+
+//
+// InfixPrecedence:
+//
+// Value indicating how strongly infix operators should bind. The higher the value, the stronger they should bind.
+//
+
+typedef enum
+{
+ INFIX_PRECEDENCE_INVALID = -1,
+ INFIX_PRECEDENCE_NONE = 0,
+ INFIX_PRECEDENCE_ADDITIVE = 1,
+ INFIX_PRECEDENCE_MULTIPLICATIVE = 2,
+ INFIX_PRECEDENCE_EXPONENTS = 3,
+} InfixPrecedence;
+
+static FoobarMathExpression* parser_process ( Parser* ctx,
+ FoobarMathExpression* lhs,
+ InfixPrecedence min_precedence );
+static FoobarMathExpression* parser_process_single ( Parser* ctx,
+ gboolean allow_sign );
+static FoobarMathExpression* parser_process_identifier ( Parser* ctx );
+static FoobarMathExpression* parser_process_number ( Parser* ctx );
+static FoobarMathExpression* parser_process_function ( Parser* ctx,
+ FoobarMathFunction function );
+static FoobarMathExpression* parser_process_parens ( Parser* ctx );
+static FoobarMathExpression* parser_process_sign ( Parser* ctx );
+static FoobarMathToken const* parser_peek ( Parser const* ctx );
+static FoobarMathToken const* parser_pop ( Parser* ctx );
+static gboolean parser_match_identifier ( FoobarMathToken const* token,
+ gchar const* identifier );
+static InfixPrecedence parser_operator_precedence( FoobarMathToken const* token );
+static FoobarMathOperation parser_operator ( FoobarMathToken const* token );
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Parsing
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Transform a sequence of tokens into a mathematical expression.
+//
+// On success, this will return a newly allocated expression which should be freed using foobar_math_expression_free.
+//
+// On error, this will return NULL.
+//
+FoobarMathExpression* foobar_math_parse(
+ FoobarMathToken const* tokens,
+ gsize tokens_count )
+{
+ Parser ctx = { 0 };
+ ctx.tokens = tokens;
+ ctx.tokens_count = tokens_count;
+
+ FoobarMathExpression* lhs = parser_process_single( &ctx, TRUE );
+ if ( !lhs ) { return NULL; }
+
+ FoobarMathExpression* expr = parser_process( &ctx, lhs, INFIX_PRECEDENCE_NONE );
+ if ( !expr ) { return NULL; }
+
+ if ( parser_peek( &ctx ) )
+ {
+ foobar_math_expression_free( expr );
+ return NULL;
+ }
+
+ return expr;
+}
+
+//
+// Process tokens until the end of the current precedence scope (indicated by min_precedence), starting at an operator.
+//
+FoobarMathExpression* parser_process(
+ Parser* ctx,
+ FoobarMathExpression* lhs,
+ InfixPrecedence min_precedence )
+{
+ FoobarMathToken const* op = parser_peek( ctx );
+ InfixPrecedence op_precedence = parser_operator_precedence( op );
+ while ( op_precedence >= min_precedence )
+ {
+ parser_pop( ctx );
+
+ FoobarMathExpression* rhs = parser_process_single( ctx, FALSE );
+ if ( !rhs )
+ {
+ foobar_math_expression_free( lhs );
+ return NULL;
+ }
+
+ FoobarMathToken const* next_op = parser_peek( ctx );
+ while ( parser_operator_precedence( next_op ) > op_precedence )
+ {
+ rhs = parser_process( ctx, rhs, op_precedence + 1 );
+ if ( !rhs )
+ {
+ foobar_math_expression_free( lhs );
+ return NULL;
+ }
+
+ next_op = parser_peek( ctx );
+ }
+
+ lhs = foobar_math_expression_new_operation( parser_operator( op ), lhs, rhs );
+
+ op = parser_peek( ctx );
+ op_precedence = parser_operator_precedence( op );
+ }
+
+ return lhs;
+}
+
+//
+// Process a single value which may be either a constant, function, number, expression enclosed in parenthesis or
+// negation.
+//
+// If an unexpected token is encountered (i.e. an operator), this will return FALSE.
+//
+FoobarMathExpression* parser_process_single(
+ Parser* ctx,
+ gboolean allow_sign )
+{
+ FoobarMathToken const* token = parser_peek( ctx );
+ if ( !token ) { return NULL; }
+
+ switch ( token->type )
+ {
+ case FOOBAR_MATH_TOKEN_IDENTIFIER:
+ return parser_process_identifier( ctx );
+ case FOOBAR_MATH_TOKEN_NUMBER:
+ return parser_process_number( ctx );
+ case FOOBAR_MATH_TOKEN_PAREN_OPEN:
+ return parser_process_parens( ctx );
+ case FOOBAR_MATH_TOKEN_MINUS:
+ case FOOBAR_MATH_TOKEN_PLUS:
+ if ( !allow_sign ) { return NULL; }
+ return parser_process_sign( ctx );
+ case FOOBAR_MATH_TOKEN_PAREN_CLOSE:
+ case FOOBAR_MATH_TOKEN_MUL:
+ case FOOBAR_MATH_TOKEN_DIV:
+ case FOOBAR_MATH_TOKEN_POW:
+ return NULL;
+ default:
+ g_warn_if_reached( );
+ return NULL;
+ }
+}
+
+//
+// Parse an identifier.
+//
+// If the identifier is a function, this will expected parenthesis and an expression afterwards.
+//
+FoobarMathExpression* parser_process_identifier( Parser* ctx )
+{
+ FoobarMathToken const* token = parser_pop( ctx );
+ if ( parser_match_identifier( token, "pi" ) )
+ {
+ return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_PI );
+ }
+ else if ( parser_match_identifier( token, "e" ) )
+ {
+ return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_E );
+ }
+ else if ( parser_match_identifier( token, "exp" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_EXP );
+ }
+ else if ( parser_match_identifier( token, "sqrt" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SQRT );
+ }
+ else if ( parser_match_identifier( token, "sin" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SIN );
+ }
+ else if ( parser_match_identifier( token, "cos" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COS );
+ }
+ else if ( parser_match_identifier( token, "sec" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SEC );
+ }
+ else if ( parser_match_identifier( token, "csc" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_CSC );
+ }
+ else if ( parser_match_identifier( token, "tan" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_TAN );
+ }
+ else if ( parser_match_identifier( token, "cot" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COT );
+ }
+ else if ( parser_match_identifier( token, "arcsin" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSIN );
+ }
+ else if ( parser_match_identifier( token, "arccos" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOS );
+ }
+ else if ( parser_match_identifier( token, "arcsec" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSEC );
+ }
+ else if ( parser_match_identifier( token, "arccsc" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCSC );
+ }
+ else if ( parser_match_identifier( token, "arctan" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCTAN );
+ }
+ else if ( parser_match_identifier( token, "arccot" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOT );
+ }
+ else
+ {
+ return NULL;
+ }
+}
+
+//
+// Process a numeric value.
+//
+FoobarMathExpression* parser_process_number( Parser* ctx )
+{
+ FoobarMathToken const* token = parser_pop( ctx );
+ FoobarMathValue value = { 0 };
+ if ( !foobar_math_value_from_string( token->data, token->length, &value ) )
+ {
+ return NULL;
+ }
+
+ return foobar_math_expression_new_value( value );
+}
+
+//
+// Process a function's parameter which should be enclosed in parenthesis.
+//
+FoobarMathExpression* parser_process_function(
+ Parser* ctx,
+ FoobarMathFunction function )
+{
+ FoobarMathToken const* opening_paren = parser_peek( ctx );
+ if ( !opening_paren || opening_paren->type != FOOBAR_MATH_TOKEN_PAREN_OPEN )
+ {
+ return NULL;
+ }
+
+ FoobarMathExpression* arg = parser_process_parens( ctx );
+ if ( !arg ) { return NULL; }
+
+ return foobar_math_expression_new_function( function, arg );
+}
+
+//
+// Process an expression enclosed in parenthesis.
+//
+FoobarMathExpression* parser_process_parens( Parser* ctx )
+{
+ parser_pop( ctx );
+ FoobarMathExpression* lhs = parser_process_single( ctx, TRUE );
+ if ( !lhs ) { return NULL; }
+ FoobarMathExpression* result = parser_process( ctx, lhs, INFIX_PRECEDENCE_NONE );
+ if ( !result ) { return NULL; }
+ FoobarMathToken const* closing_paren = parser_pop( ctx );
+ if ( !closing_paren || closing_paren->type != FOOBAR_MATH_TOKEN_PAREN_CLOSE )
+ {
+ foobar_math_expression_free( result );
+ return NULL;
+ }
+ return result;
+}
+
+//
+// Process a sign (- or +) which may be used for negation of an expression.
+//
+FoobarMathExpression* parser_process_sign( Parser* ctx )
+{
+ FoobarMathToken const* token = parser_pop( ctx );
+ FoobarMathExpression* result = parser_process_single( ctx, FALSE );
+ if ( !result ) { return NULL; }
+
+ if ( token->type == FOOBAR_MATH_TOKEN_MINUS )
+ {
+ return foobar_math_expression_new_function( FOOBAR_MATH_FUNCTION_NEGATE, result );
+ }
+ else
+ {
+ return result;
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// State Helpers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Peek at the current token without consuming it.
+//
+// If this is the end of the token list, return NULL.
+//
+FoobarMathToken const* parser_peek( Parser const* ctx )
+{
+ if ( ctx->position >= ctx->tokens_count ) { return NULL; }
+
+ return &ctx->tokens[ctx->position];
+}
+
+//
+// Consume the current token.
+//
+// If this is the end of the token list, return NULL.
+//
+FoobarMathToken const* parser_pop( Parser* ctx )
+{
+ if ( ctx->position >= ctx->tokens_count ) { return NULL; }
+
+ return &ctx->tokens[ctx->position++];
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Match a token's string against the given identifier string.
+//
+// This is necessary because the token's content is not a null-terminated string.
+//
+gboolean parser_match_identifier(
+ FoobarMathToken const* token,
+ gchar const* identifier )
+{
+ if ( strlen( identifier ) != token->length ) { return FALSE; }
+
+ return !strncmp( token->data, identifier, token->length );
+}
+
+//
+// Get the precedence of an infix operator token or INVALID.
+//
+InfixPrecedence parser_operator_precedence( FoobarMathToken const* token )
+{
+ if ( token )
+ {
+ switch ( token->type )
+ {
+ case FOOBAR_MATH_TOKEN_PLUS:
+ case FOOBAR_MATH_TOKEN_MINUS:
+ return INFIX_PRECEDENCE_ADDITIVE;
+ case FOOBAR_MATH_TOKEN_MUL:
+ case FOOBAR_MATH_TOKEN_DIV:
+ return INFIX_PRECEDENCE_MULTIPLICATIVE;
+ case FOOBAR_MATH_TOKEN_POW:
+ return INFIX_PRECEDENCE_EXPONENTS;
+ case FOOBAR_MATH_TOKEN_IDENTIFIER:
+ case FOOBAR_MATH_TOKEN_NUMBER:
+ case FOOBAR_MATH_TOKEN_PAREN_OPEN:
+ case FOOBAR_MATH_TOKEN_PAREN_CLOSE:
+ default:
+ break;
+ }
+ }
+
+ return INFIX_PRECEDENCE_INVALID;
+}
+
+//
+// Get the operation for an infix operator token. This requires the token to be valid.
+//
+FoobarMathOperation parser_operator( FoobarMathToken const* token )
+{
+ switch ( token->type )
+ {
+ case FOOBAR_MATH_TOKEN_PLUS:
+ return FOOBAR_MATH_OPERATION_ADD;
+ case FOOBAR_MATH_TOKEN_MINUS:
+ return FOOBAR_MATH_OPERATION_SUB;
+ case FOOBAR_MATH_TOKEN_MUL:
+ return FOOBAR_MATH_OPERATION_MUL;
+ case FOOBAR_MATH_TOKEN_DIV:
+ return FOOBAR_MATH_OPERATION_DIV;
+ case FOOBAR_MATH_TOKEN_POW:
+ return FOOBAR_MATH_OPERATION_POW;
+ case FOOBAR_MATH_TOKEN_IDENTIFIER:
+ case FOOBAR_MATH_TOKEN_NUMBER:
+ case FOOBAR_MATH_TOKEN_PAREN_OPEN:
+ case FOOBAR_MATH_TOKEN_PAREN_CLOSE:
+ default:
+ g_warn_if_reached( );
+ return 0;
+ }
+}
diff --git a/csrc/services/quick-answers/math.c b/csrc/services/quick-answers/math.c
new file mode 100644
index 0000000..69219e2
--- /dev/null
+++ b/csrc/services/quick-answers/math.c
@@ -0,0 +1,851 @@
+#define _GNU_SOURCE
+
+#include "services/quick-answers/math.h"
+#include <math.h>
+
+//
+// FoobarMathExpression:
+//
+// A fully parsed mathematical expression which can be evaluated, for example "sin(pi) - 3" would be represented as:
+// - Operation: SUB
+// - Function: SIN
+// - Constant: PI
+// - Value: 3
+//
+
+struct _FoobarMathExpression
+{
+ FoobarMathExpressionType type;
+
+ union
+ {
+ struct
+ {
+ FoobarMathValue v;
+ } value;
+
+ struct
+ {
+ FoobarMathFunction f;
+ FoobarMathExpression* input;
+ } function;
+
+ struct
+ {
+ FoobarMathConstant c;
+ } constant;
+
+ struct
+ {
+ FoobarMathOperation o;
+ FoobarMathExpression* lhs;
+ FoobarMathExpression* rhs;
+ } operation;
+ };
+};
+
+//
+// FoobarMathValue:
+//
+// Representation of a value. The goal of this memory structure is to store the value as an integer for as long as
+// possible. Because of this, there is a "decimal_places" field, so the actual value of an integer is
+// $v * 10^(-decimal_places)$.
+//
+// If absolutely necessary (for example, because we use a function like sin), the value is converted to a floating-point
+// value.
+//
+// Note that we use GMP to store the integer value to allow for arbitrarily large values. Because of this, the structure
+// should always be freed using foobar_math_value_free.
+//
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Expressions
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Helper to allocate a new FoobarMathExpression representing a value.
+//
+// Ownership of "value" is transferred to the new expression.
+//
+FoobarMathExpression* foobar_math_expression_new_value( FoobarMathValue value )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_VALUE;
+ res->value.v = value;
+ return res;
+}
+
+//
+// Helper to allocate a new FoobarMathExpression representing a function to be evaluated with another expression as its
+// parameter.
+//
+// Ownership of "input" is transferred to the new expression.
+//
+FoobarMathExpression* foobar_math_expression_new_function(
+ FoobarMathFunction function,
+ FoobarMathExpression* input )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_FUNCTION;
+ res->function.f = function;
+ res->function.input = input;
+ return res;
+}
+
+//
+// Helper to allocate a new FoobarMathExpression representing a known constant.
+//
+FoobarMathExpression* foobar_math_expression_new_constant( FoobarMathConstant constant )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_CONSTANT;
+ res->constant.c = constant;
+ return res;
+}
+
+//
+// Helper to allocate a new FoobarMathExpression representing an operation to be evaluated with two expressions as its
+// parameters.
+//
+// Ownership of "lhs" and "rhs" is transferred to the new expression.
+//
+FoobarMathExpression* foobar_math_expression_new_operation(
+ FoobarMathOperation operation,
+ FoobarMathExpression* lhs,
+ FoobarMathExpression* rhs )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_OPERATION;
+ res->operation.o = operation;
+ res->operation.lhs = lhs;
+ res->operation.rhs = rhs;
+ return res;
+}
+
+//
+// Release the memory associated with a mathematical expression, along with the expressions it owns.
+//
+void foobar_math_expression_free( FoobarMathExpression* expression )
+{
+ if ( expression )
+ {
+ switch ( expression->type )
+ {
+ case FOOBAR_MATH_EXPRESSION_VALUE:
+ foobar_math_value_free( expression->value.v );
+ break;
+ case FOOBAR_MATH_EXPRESSION_FUNCTION:
+ foobar_math_expression_free( expression->function.input );
+ break;
+ case FOOBAR_MATH_EXPRESSION_CONSTANT:
+ break;
+ case FOOBAR_MATH_EXPRESSION_OPERATION:
+ foobar_math_expression_free( expression->operation.lhs );
+ foobar_math_expression_free( expression->operation.rhs );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ g_free( expression );
+ }
+}
+
+//
+// Print a tree representation of a mathematical expression.
+//
+// This is mainly useful for debugging. "indentation" describes the current indentation level for the recursive call
+// (should be 0 initially).
+//
+void foobar_math_expression_print(
+ FoobarMathExpression const* expr,
+ gint indentation )
+{
+ for (gint i = 0; i < indentation; ++i) { g_print("| "); }
+ switch ( expr->type )
+ {
+ case FOOBAR_MATH_EXPRESSION_VALUE:
+ {
+ g_autofree gchar* val = foobar_math_value_to_string( expr->value.v );
+ g_print( "%s\n", val );
+ break;
+ }
+ case FOOBAR_MATH_EXPRESSION_FUNCTION:
+ switch ( expr->function.f )
+ {
+ case FOOBAR_MATH_FUNCTION_NEGATE:
+ g_print( "-\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_SIN:
+ g_print( "sin\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_COS:
+ g_print( "cos\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_SEC:
+ g_print( "sec\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_CSC:
+ g_print( "csc\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_TAN:
+ g_print( "tan\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_COT:
+ g_print( "cot\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSIN:
+ g_print( "arcsin\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOS:
+ g_print( "arccos\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSEC:
+ g_print( "arcsec\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCSC:
+ g_print( "arccsc\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCTAN:
+ g_print( "arctan\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOT:
+ g_print( "arccot\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_EXP:
+ g_print( "exp\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_SQRT:
+ g_print( "sqrt\n" );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ foobar_math_expression_print( expr->function.input, indentation + 1 );
+ break;
+ case FOOBAR_MATH_EXPRESSION_CONSTANT:
+ switch ( expr->constant.c )
+ {
+ case FOOBAR_MATH_CONSTANT_PI:
+ g_print( "pi\n" );
+ break;
+ case FOOBAR_MATH_CONSTANT_E:
+ g_print( "e\n" );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ break;
+ case FOOBAR_MATH_EXPRESSION_OPERATION:
+ switch ( expr->operation.o )
+ {
+ case FOOBAR_MATH_OPERATION_ADD:
+ g_print( "+\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_SUB:
+ g_print( "-\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_MUL:
+ g_print( "*\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_DIV:
+ g_print( "/\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_POW:
+ g_print( "^\n" );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ foobar_math_expression_print( expr->operation.lhs, indentation + 1 );
+ foobar_math_expression_print( expr->operation.rhs, indentation + 1 );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+//
+// Evaluate an expression, producing a value.
+//
+// On success (indicated by the return value TRUE), the value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_evaluate(
+ FoobarMathExpression const* expr,
+ FoobarMathValue* out_value )
+{
+ switch ( expr->type )
+ {
+ case FOOBAR_MATH_EXPRESSION_VALUE:
+ foobar_math_value_copy( expr->value.v, out_value );
+ return TRUE;
+ case FOOBAR_MATH_EXPRESSION_CONSTANT:
+ switch ( expr->constant.c )
+ {
+ case FOOBAR_MATH_CONSTANT_PI:
+ foobar_math_value_from_float( M_PIl, out_value );
+ return TRUE;
+ case FOOBAR_MATH_CONSTANT_E:
+ foobar_math_value_from_float( M_El, out_value );
+ return TRUE;
+ default:
+ g_warn_if_reached( );
+ return FALSE;
+ }
+ case FOOBAR_MATH_EXPRESSION_FUNCTION:
+ {
+ FoobarMathValue input = { 0 };
+ if ( !foobar_math_evaluate( expr->function.input, &input ) ) { return FALSE; }
+
+ if ( expr->function.f == FOOBAR_MATH_FUNCTION_NEGATE )
+ {
+ *out_value = input;
+ foobar_math_value_negate( out_value );
+ return TRUE;
+ }
+
+ long double input_float = foobar_math_value_to_float( input );
+ foobar_math_value_free( input );
+
+ long double result = NAN;
+ switch ( expr->function.f )
+ {
+ case FOOBAR_MATH_FUNCTION_SIN:
+ result = sinl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_COS:
+ result = cosl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_SEC:
+ result = 1.L / cosl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_CSC:
+ result = 1.L / sinl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_TAN:
+ result = tanl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_COT:
+ result = 1.L / tanl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSIN:
+ result = asinl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOS:
+ result = acosl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSEC:
+ result = acosl( 1.L / input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCSC:
+ result = asinl( 1.L / input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCTAN:
+ result = atanl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOT:
+ result = atanl( 1.L / input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_EXP:
+ result = expl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_SQRT:
+ result = sqrtl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_NEGATE:
+ default:
+ g_warn_if_reached( );
+ return FALSE;
+ }
+
+ if ( isnanl( result ) || isinfl( result ) ) { return FALSE; }
+
+ foobar_math_value_from_float( result, out_value );
+ return TRUE;
+ }
+ case FOOBAR_MATH_EXPRESSION_OPERATION:
+ {
+ FoobarMathValue lhs = { 0 };
+ if ( !foobar_math_evaluate( expr->operation.lhs, &lhs ) ) { return FALSE; }
+ FoobarMathValue rhs = { 0 };
+ if ( !foobar_math_evaluate( expr->operation.rhs, &rhs ) )
+ {
+ foobar_math_value_free( lhs );
+ return FALSE;
+ }
+
+ gboolean success = FALSE;
+ switch ( expr->operation.o )
+ {
+ case FOOBAR_MATH_OPERATION_ADD:
+ success = foobar_math_value_add( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_SUB:
+ success = foobar_math_value_sub( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_MUL:
+ success = foobar_math_value_mul( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_DIV:
+ success = foobar_math_value_div( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_POW:
+ success = foobar_math_value_pow( &lhs, &rhs, out_value );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ foobar_math_value_free( lhs );
+ foobar_math_value_free( rhs );
+
+ return success;
+ }
+ default:
+ g_warn_if_reached( );
+ return FALSE;
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Values
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Initialize a new integer value to zero.
+//
+// After this, the structure should be freed using foobar_math_value_free.
+//
+void foobar_math_value_new_int( FoobarMathValue* out_value )
+{
+ out_value->type = FOOBAR_MATH_VALUE_INT;
+ mpz_init( out_value->int_value.v );
+ out_value->int_value.decimal_places = 0;
+}
+
+//
+// Initialize a new value from its floating point representation.
+//
+// Although technically not necessary, you should still call foobar_math_value_free on the returned value.
+//
+void foobar_math_value_from_float(
+ long double value,
+ FoobarMathValue* out_value )
+{
+ out_value->type = FOOBAR_MATH_VALUE_FLOAT;
+ out_value->float_value.v = value;
+}
+
+//
+// Parse a value from its string representation. This will always return an integer representation.
+//
+// On success (return value TRUE), the structure should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_from_string(
+ gchar const* input,
+ gsize input_length,
+ FoobarMathValue* out_value )
+{
+ g_autofree gchar* without_sep = g_new0( gchar, input_length + 1 );
+ gsize sep_pos = input_length - 1;
+ gsize j = 0;
+ for ( gsize i = 0; i < input_length; ++i )
+ {
+ if ( input[i] == '.' )
+ {
+ sep_pos = i;
+ }
+ else
+ {
+ without_sep[j++] = input[i];
+ }
+ }
+
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = input_length - 1 - sep_pos;
+ if ( mpz_init_set_str( out_value->int_value.v, without_sep, 10 ) == -1 )
+ {
+ mpz_clear( out_value->int_value.v );
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+//
+// Create a copy of a value, allocating necessary memory.
+//
+// After this, the structure should be freed using foobar_math_value_free.
+//
+void foobar_math_value_copy(
+ FoobarMathValue value,
+ FoobarMathValue* out_value )
+{
+ *out_value = value;
+
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ mpz_init_set( out_value->int_value.v, value.int_value.v );
+ break;
+ case FOOBAR_MATH_VALUE_FLOAT:
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+//
+// Release memory associated with a value.
+//
+// This is necessary because the integer representation is stored using GMP, which can store arbitrarily large values.
+//
+void foobar_math_value_free( FoobarMathValue value )
+{
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ mpz_clear( value.int_value.v );
+ break;
+ case FOOBAR_MATH_VALUE_FLOAT:
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Operations
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Operation equivalent to "value *= -1".
+//
+void foobar_math_value_negate( FoobarMathValue* value )
+{
+ switch ( value->type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ mpz_neg( value->int_value.v, value->int_value.v );
+ break;
+ case FOOBAR_MATH_VALUE_FLOAT:
+ value->float_value.v = -value->float_value.v;
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+//
+// Operation setting "out_value = lhs + rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_add(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( foobar_math_value_unify_integers( lhs, rhs ) )
+ {
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places;
+ mpz_add( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ foobar_math_value_from_float( lhs_float + rhs_float, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs - rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_sub(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( foobar_math_value_unify_integers( lhs, rhs ) )
+ {
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places;
+ mpz_sub( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ foobar_math_value_from_float( lhs_float - rhs_float, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs * rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_mul(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT )
+ {
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places + rhs->int_value.decimal_places;
+ mpz_mul( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ foobar_math_value_from_float( lhs_float * rhs_float, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs / rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_div(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT )
+ {
+ if ( mpz_sgn( rhs->int_value.v ) == 0 ) { return FALSE; }
+
+ if ( rhs->int_value.decimal_places > lhs->int_value.decimal_places )
+ {
+ foobar_math_value_unify_integers( lhs, rhs );
+ }
+
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places - rhs->int_value.decimal_places;
+ mpz_t rem;
+ mpz_init( rem );
+ mpz_divmod( out_value->int_value.v, rem, lhs->int_value.v, rhs->int_value.v );
+ gboolean result_is_int = mpz_sgn( rem ) == 0;
+ mpz_clear( rem );
+
+ if ( result_is_int ) { return TRUE; }
+ foobar_math_value_free( *out_value );
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ long double result = lhs_float / rhs_float;
+ if ( isnanl( result ) || isinfl( result ) ) { return FALSE; }
+ foobar_math_value_from_float( result, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs ^ rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_pow(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( lhs->type == FOOBAR_MATH_VALUE_INT &&
+ rhs->type == FOOBAR_MATH_VALUE_INT &&
+ rhs->int_value.decimal_places == 0 &&
+ mpz_fits_ulong_p( rhs->int_value.v ) )
+ {
+ unsigned long exp = mpz_get_ui( rhs->int_value.v );
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places * exp;
+ mpz_pow_ui( out_value->int_value.v, lhs->int_value.v, exp );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ long double result = powl( lhs_float, rhs_float );
+ if ( isnanl( result ) || isinfl( result ) ) { return FALSE; }
+ foobar_math_value_from_float( result, out_value );
+ return TRUE;
+}
+
+//
+// Ensure that two integer values have the same number of decimal places.
+//
+// This may change the in-memory-representation of a value, but not its actual value. It only increases the precision of
+// one value
+//
+// If either a or b is not stored as an integer, this will return FALSE.
+//
+gboolean foobar_math_value_unify_integers(
+ FoobarMathValue* a,
+ FoobarMathValue* b )
+{
+ if ( a->type != FOOBAR_MATH_VALUE_INT || b->type != FOOBAR_MATH_VALUE_INT ) { return FALSE; }
+
+ // Extend the less precise representation so both numbers have the same number of decimal places.
+
+ gsize decimal_places = MAX( a->int_value.decimal_places, b->int_value.decimal_places );
+
+ mpz_t exp;
+ mpz_init( exp );
+
+ if ( decimal_places > a->int_value.decimal_places )
+ {
+ mpz_ui_pow_ui( exp, 10, decimal_places - a->int_value.decimal_places );
+ mpz_mul( a->int_value.v, a->int_value.v, exp );
+ }
+
+ if ( decimal_places > b->int_value.decimal_places )
+ {
+ mpz_ui_pow_ui( exp, 10, decimal_places - b->int_value.decimal_places );
+ mpz_mul( b->int_value.v, b->int_value.v, exp );
+ }
+
+ mpz_clear( exp );
+
+ a->int_value.decimal_places = b->int_value.decimal_places = decimal_places;
+
+ return TRUE;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Conversions
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Format a value as a string.
+//
+// The result is a null-terminated string which should be freed using g_free.
+//
+gchar* foobar_math_value_to_string( FoobarMathValue value )
+{
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ {
+ gsize cap = mpz_sizeinbase( value.int_value.v, 10 );
+ cap = MAX( cap, value.int_value.decimal_places + 1 ); // pad with zeroes if necessary
+ gchar* result = g_new0( gchar, cap + 3 ); // length + null terminator + sign + separator
+ mpz_get_str( result, 10, value.int_value.v );
+ gsize len = strlen( result ); // actual size may be 1 char less (+ len now also includes sign)
+ gboolean has_sign = result[0] == '-';
+ if ( has_sign )
+ {
+ // add the sign back later, makes everything else easier
+ result += 1;
+ len -= 1;
+ }
+ // pad with leading zeroes if necessary
+ gsize padded_len = MAX( len, value.int_value.decimal_places + 1 );
+ memmove( &result[padded_len - len], result, len + 1 );
+ memset( result, '0', padded_len - len );
+ len = padded_len;
+ gboolean trailing_zeros = TRUE;
+ gsize decimal_places = value.int_value.decimal_places;
+ gsize actual_decimal_places = decimal_places;
+ for ( gsize i = 0; i < decimal_places; ++i )
+ {
+ if ( trailing_zeros && result[len - 1 - i] == '0' )
+ {
+ result[len - 1 - i] = '\0';
+ actual_decimal_places -= 1;
+ }
+ else
+ {
+ trailing_zeros = FALSE;
+ }
+ }
+ if ( actual_decimal_places > 0 )
+ {
+ memmove( &result[len + 1 - decimal_places], &result[len - decimal_places], decimal_places + 1 );
+ result[len - decimal_places] = '.';
+ }
+ if ( has_sign )
+ {
+ result -= 1;
+ len += 1;
+ }
+ return result;
+ }
+ case FOOBAR_MATH_VALUE_FLOAT:
+ {
+ gchar* result = g_strdup_printf( "%.30Lf", value.float_value.v );
+
+ // trim trailing zeros
+ size_t len = strlen( result );
+ for ( gsize i = 0; i < len; ++i )
+ {
+ if ( result[len - 1 - i] == '.' )
+ {
+ result[len - 1 - i] = '\0';
+ break;
+ }
+ else if ( result[len - 1 - i] == '0' )
+ {
+ result[len - 1 - i] = '\0';
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return result;
+ }
+ default:
+ g_warn_if_reached( );
+ return NULL;
+ }
+}
+
+//
+// Convert a value to its floating-point representation.
+//
+// This may be a lossy conversion and should only be done if absolutely necessary.
+//
+long double foobar_math_value_to_float( FoobarMathValue value )
+{
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ {
+ mpz_t exp, integer, frac;
+ mpz_inits( exp, integer, frac, NULL );
+ mpz_ui_pow_ui( exp, 10, value.int_value.decimal_places );
+ mpz_mod( frac, value.int_value.v, exp );
+ mpz_div( integer, value.int_value.v, exp );
+ mpf_t f_exp, f_frac;
+ mpf_inits( f_exp, f_frac, NULL );
+ mpf_set_z( f_exp, exp );
+ mpf_set_z( f_frac, frac );
+ mpf_div( f_frac, f_frac, f_exp );
+ long double result = mpz_get_d( integer ) + mpf_get_d( f_frac );
+ mpf_clears( f_exp, f_frac, NULL );
+ mpz_clears( exp, integer, frac, NULL );
+ return result;
+ }
+ case FOOBAR_MATH_VALUE_FLOAT:
+ return value.float_value.v;
+ default:
+ g_warn_if_reached( );
+ return 0;
+ }
+}
diff --git a/csrc/services/quick-answers/math.h b/csrc/services/quick-answers/math.h
new file mode 100644
index 0000000..fafa73d
--- /dev/null
+++ b/csrc/services/quick-answers/math.h
@@ -0,0 +1,148 @@
+#pragma once
+
+#include <gio/gio.h>
+#include <gmp.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+ FOOBAR_MATH_VALUE_INT,
+ FOOBAR_MATH_VALUE_FLOAT,
+} FoobarMathValueType;
+
+typedef struct _FoobarMathValue FoobarMathValue;
+
+struct _FoobarMathValue
+{
+ FoobarMathValueType type;
+
+ union
+ {
+ struct
+ {
+ mpz_t v;
+ gsize decimal_places;
+ } int_value;
+
+ struct
+ {
+ long double v;
+ } float_value;
+ };
+};
+
+typedef enum
+{
+ FOOBAR_MATH_TOKEN_IDENTIFIER,
+ FOOBAR_MATH_TOKEN_NUMBER,
+ FOOBAR_MATH_TOKEN_PAREN_OPEN,
+ FOOBAR_MATH_TOKEN_PAREN_CLOSE,
+ FOOBAR_MATH_TOKEN_PLUS,
+ FOOBAR_MATH_TOKEN_MINUS,
+ FOOBAR_MATH_TOKEN_MUL,
+ FOOBAR_MATH_TOKEN_DIV,
+ FOOBAR_MATH_TOKEN_POW,
+} FoobarMathTokenType;
+
+typedef struct _FoobarMathToken FoobarMathToken;
+
+struct _FoobarMathToken
+{
+ FoobarMathTokenType type;
+ gchar const* data;
+ gsize length;
+};
+
+typedef enum
+{
+ FOOBAR_MATH_EXPRESSION_VALUE,
+ FOOBAR_MATH_EXPRESSION_FUNCTION,
+ FOOBAR_MATH_EXPRESSION_CONSTANT,
+ FOOBAR_MATH_EXPRESSION_OPERATION,
+} FoobarMathExpressionType;
+
+typedef enum
+{
+ FOOBAR_MATH_FUNCTION_NEGATE,
+ FOOBAR_MATH_FUNCTION_SIN,
+ FOOBAR_MATH_FUNCTION_COS,
+ FOOBAR_MATH_FUNCTION_SEC,
+ FOOBAR_MATH_FUNCTION_CSC,
+ FOOBAR_MATH_FUNCTION_TAN,
+ FOOBAR_MATH_FUNCTION_COT,
+ FOOBAR_MATH_FUNCTION_ARCSIN,
+ FOOBAR_MATH_FUNCTION_ARCCOS,
+ FOOBAR_MATH_FUNCTION_ARCSEC,
+ FOOBAR_MATH_FUNCTION_ARCCSC,
+ FOOBAR_MATH_FUNCTION_ARCTAN,
+ FOOBAR_MATH_FUNCTION_ARCCOT,
+ FOOBAR_MATH_FUNCTION_EXP,
+ FOOBAR_MATH_FUNCTION_SQRT,
+} FoobarMathFunction;
+
+typedef enum
+{
+ FOOBAR_MATH_CONSTANT_PI,
+ FOOBAR_MATH_CONSTANT_E,
+} FoobarMathConstant;
+
+typedef enum
+{
+ FOOBAR_MATH_OPERATION_ADD,
+ FOOBAR_MATH_OPERATION_SUB,
+ FOOBAR_MATH_OPERATION_MUL,
+ FOOBAR_MATH_OPERATION_DIV,
+ FOOBAR_MATH_OPERATION_POW,
+} FoobarMathOperation;
+
+typedef struct _FoobarMathExpression FoobarMathExpression;
+
+FoobarMathToken* foobar_math_lex ( gchar const* input,
+ gsize input_length,
+ gsize* out_count );
+FoobarMathExpression* foobar_math_parse ( FoobarMathToken const* tokens,
+ gsize tokens_count );
+FoobarMathExpression* foobar_math_expression_new_value ( FoobarMathValue value );
+FoobarMathExpression* foobar_math_expression_new_function ( FoobarMathFunction function,
+ FoobarMathExpression* input );
+FoobarMathExpression* foobar_math_expression_new_constant ( FoobarMathConstant constant );
+FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation operation,
+ FoobarMathExpression* lhs,
+ FoobarMathExpression* rhs );
+void foobar_math_expression_print ( FoobarMathExpression const* expr,
+ gint indentation );
+gboolean foobar_math_evaluate ( FoobarMathExpression const* expr,
+ FoobarMathValue* out_value );
+void foobar_math_expression_free ( FoobarMathExpression* expression );
+void foobar_math_value_new_int ( FoobarMathValue* out_value );
+void foobar_math_value_from_float ( long double value,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_from_string ( gchar const* input,
+ gsize input_length,
+ FoobarMathValue* out_value );
+void foobar_math_value_copy ( FoobarMathValue value,
+ FoobarMathValue* out_value );
+void foobar_math_value_free ( FoobarMathValue value );
+void foobar_math_value_negate ( FoobarMathValue* value );
+gboolean foobar_math_value_add ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_sub ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_mul ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_div ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_pow ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_unify_integers ( FoobarMathValue* a,
+ FoobarMathValue* b );
+gchar* foobar_math_value_to_string ( FoobarMathValue value );
+long double foobar_math_value_to_float ( FoobarMathValue value );
+
+G_END_DECLS
diff --git a/csrc/services/quick-answers/math.test.c b/csrc/services/quick-answers/math.test.c
new file mode 100644
index 0000000..416b576
--- /dev/null
+++ b/csrc/services/quick-answers/math.test.c
@@ -0,0 +1,153 @@
+#include "services/quick-answers/math.h"
+#include <mutest.h>
+
+#define SERIALIZATION_TEST( value, identifier ) \
+ static void serialization_value_##identifier##_spec( void ) \
+ { \
+ FoobarMathValue val; \
+ \
+ gchar const input[] = #value; \
+ mutest_expect( \
+ "parse result", \
+ mutest_bool_value( foobar_math_value_from_string( input, sizeof(input) - 1, &val) ), \
+ mutest_to_be_true, \
+ NULL ); \
+ g_autofree gchar* output = foobar_math_value_to_string( val ); \
+ mutest_expect( \
+ "string representation", \
+ mutest_string_value( output ), \
+ mutest_to_be, \
+ input, \
+ NULL ); \
+ \
+ foobar_math_value_free( val ); \
+ }
+
+SERIALIZATION_TEST( 0, 0 )
+SERIALIZATION_TEST( 1, 1 )
+SERIALIZATION_TEST( -1, neg_1 )
+SERIALIZATION_TEST( 1.2, 1_2 )
+SERIALIZATION_TEST( -1.2, neg_1_2 )
+SERIALIZATION_TEST( 1.30002, 1_30002 )
+SERIALIZATION_TEST( -1.30002, neg_1_30002 )
+SERIALIZATION_TEST( 0.2, 0_2 )
+SERIALIZATION_TEST( -0.2, neg_0_2 )
+SERIALIZATION_TEST( 0.00002, 0_00002 )
+SERIALIZATION_TEST( -0.00002, neg_0_00002 )
+SERIALIZATION_TEST( 123, 123 )
+SERIALIZATION_TEST( 123.456, 123_456 )
+SERIALIZATION_TEST( -123, neg_123 )
+SERIALIZATION_TEST( -123.456, neg_123_456 )
+
+#undef SERIALIZATION_TEST
+
+static void serialization_suite( void )
+{
+ mutest_it( "value 0", serialization_value_0_spec );
+ mutest_it( "value 1", serialization_value_1_spec );
+ mutest_it( "value -1", serialization_value_neg_1_spec );
+ mutest_it( "value 1.2", serialization_value_1_2_spec );
+ mutest_it( "value -1.2", serialization_value_neg_1_2_spec );
+ mutest_it( "value 1.30002", serialization_value_1_30002_spec );
+ mutest_it( "value -1.30002", serialization_value_neg_1_30002_spec );
+ mutest_it( "value 0.2", serialization_value_0_2_spec );
+ mutest_it( "value -0.2", serialization_value_neg_0_2_spec );
+ mutest_it( "value 0.00002", serialization_value_0_00002_spec );
+ mutest_it( "value -0.00002", serialization_value_neg_0_00002_spec );
+ mutest_it( "value 123", serialization_value_123_spec );
+ mutest_it( "value 123.456", serialization_value_123_456_spec );
+ mutest_it( "value -123", serialization_value_neg_123_spec );
+ mutest_it( "value -123.456", serialization_value_neg_123_456_spec );
+}
+#define OPERATION_TEST( op, value_a, value_b, value_res, identifier ) \
+ static void operation_##identifier##_spec( void ) \
+ { \
+ FoobarMathValue a, b, res; \
+ \
+ gchar const input_a[] = #value_a; \
+ gchar const input_b[] = #value_b; \
+ mutest_expect( \
+ "input a parsed", \
+ mutest_bool_value( foobar_math_value_from_string( input_a, sizeof(input_a) - 1, &a) ), \
+ mutest_to_be_true, \
+ NULL ); \
+ mutest_expect( \
+ "input b parsed", \
+ mutest_bool_value( foobar_math_value_from_string( input_b, sizeof(input_b) - 1, &b) ), \
+ mutest_to_be_true, \
+ NULL ); \
+ foobar_math_value_##op( &a, &b, &res ); \
+ g_autofree gchar* output = foobar_math_value_to_string( res ); \
+ mutest_expect( \
+ "result", \
+ mutest_string_value( output ), \
+ mutest_to_be, \
+ #value_res, \
+ NULL ); \
+ \
+ foobar_math_value_free( a ); \
+ foobar_math_value_free( b ); \
+ foobar_math_value_free( res ); \
+ }
+
+OPERATION_TEST( add, 123, 456, 579, 123_plus_456)
+OPERATION_TEST( add, 123.456, 11, 134.456, 123_456_plus_11)
+OPERATION_TEST( add, 11, 123.456, 134.456, 11_plus_123_456)
+OPERATION_TEST( sub, 123, 456, -333, 123_minus_456)
+OPERATION_TEST( sub, 123.456, 11, 112.456, 123_456_minus_11)
+OPERATION_TEST( sub, 11, 123.456, -112.456, 11_minus_123_456)
+OPERATION_TEST( mul, 123, 3, 369, 123_times_3 )
+OPERATION_TEST( mul, 12, 0.05, 0.6, 12_times_0_05 )
+OPERATION_TEST( mul, 0.05, 12, 0.6, 0_05_times_12 )
+OPERATION_TEST( div, 369, 3, 123, 369_div_3 )
+OPERATION_TEST( div, 369, 2, 184.5, 369_div_2 )
+OPERATION_TEST( div, 12, 0.05, 240, 12_div_0_05 )
+OPERATION_TEST( div, 0.08, 4, 0.02, 0_08_div_4 )
+OPERATION_TEST( pow, 2, 3, 8, 2_pow_3 )
+OPERATION_TEST( pow, 4, 0.5, 2, 4_pow_0_5 )
+
+#undef OPERATION_TEST
+
+static void addition_suite( void )
+{
+ mutest_it( "calculate 123 + 456", operation_123_plus_456_spec );
+ mutest_it( "calculate 123.456 + 11", operation_123_456_plus_11_spec );
+ mutest_it( "calculate 11 + 123.456", operation_11_plus_123_456_spec );
+}
+
+static void subtraction_suite( void )
+{
+ mutest_it( "calculate 123 - 456", operation_123_minus_456_spec );
+ mutest_it( "calculate 123.456 - 11", operation_123_456_minus_11_spec );
+ mutest_it( "calculate 11 - 123.456", operation_11_minus_123_456_spec );
+}
+
+static void multiplication_suite( void )
+{
+ mutest_it( "calculate 123 * 3", operation_123_times_3_spec );
+ mutest_it( "calculate 12 * 0.05", operation_12_times_0_05_spec );
+ mutest_it( "calculate 0.05 * 12", operation_0_05_times_12_spec );
+}
+
+static void division_suite( void )
+{
+ mutest_it( "calculate 369 / 3", operation_369_div_3_spec );
+ mutest_it( "calculate 369 / 2", operation_369_div_2_spec );
+ mutest_it( "calculate 12 / 0.05", operation_12_div_0_05_spec );
+ mutest_it( "calculate 0.08 / 4", operation_0_08_div_4_spec );
+}
+
+static void power_suite( void )
+{
+ mutest_it( "calculate 2 ^ 3", operation_2_pow_3_spec );
+ mutest_it( "calculate 4 ^ 0.5", operation_4_pow_0_5_spec );
+}
+
+MUTEST_MAIN(
+ mutest_describe( "Serialization", serialization_suite );
+ mutest_describe( "Addition", addition_suite );
+ mutest_describe( "Subtraction", subtraction_suite );
+ mutest_describe( "Multiplication", multiplication_suite );
+ mutest_describe( "Division", division_suite );
+ mutest_describe( "Power", power_suite );
+)
diff --git a/csrc/services/quick-answers/meson.build b/csrc/services/quick-answers/meson.build
new file mode 100644
index 0000000..ae468c2
--- /dev/null
+++ b/csrc/services/quick-answers/meson.build
@@ -0,0 +1,9 @@
+foobar_sources += files(
+ 'math-lexer.c',
+ 'math-parser.c',
+ 'math.c',
+)
+
+foobar_tests += {
+ 'math': files('math.test.c'),
+}