weechat/src/core/wee-calc.c

476 lines
12 KiB
C

/*
* wee-calc.c - calculate result of an expression
*
* Copyright (C) 2019-2020 Sébastien Helleu <flashcode@flashtux.org>
*
* This file is part of WeeChat, the extensible chat client.
*
* WeeChat 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.
*
* WeeChat 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 WeeChat. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <locale.h>
#include "weechat.h"
#include "wee-arraylist.h"
#include "wee-string.h"
enum t_calc_symbol
{
CALC_SYMBOL_NONE = 0,
CALC_SYMBOL_PARENTHESIS_OPEN,
CALC_SYMBOL_PARENTHESIS_CLOSE,
CALC_SYMBOL_VALUE,
CALC_SYMBOL_OPERATOR,
};
/*
* Callback called to free a value or op in the arraylist.
*/
void
calc_list_free_cb (void *data, struct t_arraylist *arraylist, void *pointer)
{
/* make C compiler happy */
(void) data;
(void) arraylist;
free (pointer);
}
/*
* Returns the precedence of an operator:
* - '*' and '/': 2
* - '+' and '-': 1
* - any other: 0
*/
int
calc_operator_precedence (char *operator)
{
if (!operator)
return 0;
if ((strcmp (operator, "*") == 0)
|| (strcmp (operator, "/") == 0)
|| (strcmp (operator, "//") == 0)
|| (strcmp (operator, "%") == 0)
|| (strcmp (operator, "**") == 0))
{
return 2;
}
if ((strcmp (operator, "+") == 0)
|| (strcmp (operator, "-") == 0))
{
return 1;
}
return 0;
}
/*
* Pops an integer value from the stack of values.
*/
double
calc_pop_value (struct t_arraylist *list_values)
{
int size_values;
double *ptr_value, value;
size_values = arraylist_size (list_values);
if (size_values < 1)
return 0;
ptr_value = arraylist_get (list_values, size_values - 1);
value = *ptr_value;
arraylist_remove (list_values, size_values - 1);
return value;
}
/*
* Calculates result of an operation using an operator and two values.
*/
double
calc_operation (char *operator, double value1, double value2)
{
if (strcmp (operator, "+") == 0)
return value1 + value2;
if (strcmp (operator, "-") == 0)
return value1 - value2;
if (strcmp (operator, "*") == 0)
return value1 * value2;
if (strcmp (operator, "/") == 0)
return (value2 != 0) ? value1 / value2 : 0;
if (strcmp (operator, "//") == 0)
return (value2 != 0) ? floor (value1 / value2) : 0;
if (strcmp (operator, "%") == 0)
return (value2 != 0) ? fmod (value1, value2) : 0;
if (strcmp (operator, "**") == 0)
return pow (value1, value2);
return 0;
}
/*
* Calculates result of an operation using the operator on the operators stack
* and the two values on the values stack.
*
* The result is pushed on values stack.
*/
void
calc_operation_stacks (struct t_arraylist *list_values,
struct t_arraylist *list_ops)
{
int size_ops;
double value1, value2, result, *ptr_result;
char *ptr_operator;
size_ops = arraylist_size (list_ops);
if (size_ops < 1)
return;
ptr_operator = arraylist_get (list_ops, size_ops - 1);
value2 = calc_pop_value (list_values);
value1 = calc_pop_value (list_values);
result = calc_operation (ptr_operator, value1, value2);
ptr_result = malloc (sizeof (result));
*ptr_result = result;
arraylist_add (list_values, ptr_result);
arraylist_remove (list_ops, size_ops - 1);
}
/*
* Sanitizes a decimal number: removes any thousands separator and replaces
* the decimal separator by a dot.
*
* The string is updated in-place, the result has always a length shorter or
* equal to the input string.
*
* Examples:
* 1.23 --> 1.23
* 1,23 --> 1,23
* 1.234,56 --> 1234.56
* 123.456.789 --> 123456789
* 123,456,789 --> 123456789
* 1.234.567,89 --> 1234567.89
* 1,234,567.89 --> 1234567.89
* -2.345,67 --> -2345.67
*
* Returns:
* 1: the number has decimal part
* 0: the number has no decimal part
*/
int
calc_sanitize_decimal_number (char *string)
{
int i, j, count_sep, different_sep, index_last_sep;
count_sep = 0;
different_sep = 0;
index_last_sep = -1;
i = strlen (string) - 1;
while (i >= 0)
{
if (!isdigit ((unsigned char)string[i]) && (string[i] != '-'))
{
count_sep++;
if (index_last_sep < 0)
{
/* last separator found */
index_last_sep = i;
}
else
{
/* another separator found */
if (!different_sep && (string[i] != string[index_last_sep]))
{
different_sep = 1;
break;
}
}
}
i--;
}
if ((count_sep > 1) && !different_sep)
{
/*
* case of only thousands separators, like 123.456.789
* => we strip all separators
*/
index_last_sep = -1;
}
if (index_last_sep >= 0)
string[index_last_sep] = '.';
i = 0;
j = 0;
while (1)
{
if (((index_last_sep < 0) || (i < index_last_sep))
&& string[i]
&& !isdigit ((unsigned char)string[i])
&& (string[i] != '-'))
{
/* another separator found before the last one: skip it */
i++;
}
else
{
if (j != i)
string[j] = string[i];
if (!string[i])
break;
i++;
j++;
}
}
return (index_last_sep >= 0) ? 1 : 0;
}
/*
* Formats the result as a decimal number (locale independent): remove any
* extra "0" at the and the decimal point if needed.
*/
void
calc_format_result (double value, char *result, int max_size)
{
int i, has_decimal;
snprintf (result, max_size,
"%.10f",
/* ensure result is not "-0" */
(value == -0.0) ? 0.0 : value);
has_decimal = calc_sanitize_decimal_number (result);
i = strlen (result) - 1;
while (i >= 0)
{
if (!isdigit ((unsigned char)result[i]) && (result[i] != '-'))
{
result[i] = '\0';
break;
}
if (has_decimal && (result[i] == '0'))
{
result[i] = '\0';
i--;
}
else
break;
}
}
/*
* Calculates an expression, which can contain:
* - integer and decimal numbers (ie 2 or 2.5)
* - operators:
* +: addition
* -: subtraction
* *: multiplication
* /: division
* \: division giving an integer as result
* %: remainder of division
* **: power
* - parentheses: ( )
*
* The value returned is a string representation of the result, which can be
* an integer or a double, according to the operations and numbers in input.
*
* Note: result must be freed after use (if not NULL).
*/
char *
calc_expression (const char *expr)
{
struct t_arraylist *list_values, *list_ops;
char str_result[64], *ptr_operator, *operator;
int i, i2, index_op, decimals;
enum t_calc_symbol last_symbol;
double value, factor, *ptr_value;
list_values = NULL;
list_ops = NULL;
/* return 0 by default in case of error */
snprintf (str_result, sizeof (str_result), "0");
if (!expr)
goto end;
/* stack with values */
list_values = arraylist_new (32, 0, 1,
NULL, NULL,
&calc_list_free_cb, NULL);
if (!list_values)
goto end;
/* stack with operators */
list_ops = arraylist_new (32, 0, 1,
NULL, NULL,
&calc_list_free_cb, NULL);
if (!list_ops)
goto end;
last_symbol = CALC_SYMBOL_NONE;
for (i = 0; expr[i]; i++)
{
if (expr[i] == ' ')
{
/* ignore spaces */
continue;
}
else if (expr[i] == '(')
{
ptr_operator = string_strndup (expr + i, 1);
arraylist_add (list_ops, ptr_operator);
last_symbol = CALC_SYMBOL_PARENTHESIS_OPEN;
}
else if (isdigit ((unsigned char)expr[i]) || (expr[i] == '.')
|| ((expr[i] == '-')
&& ((last_symbol == CALC_SYMBOL_NONE)
|| (last_symbol == CALC_SYMBOL_PARENTHESIS_OPEN)
|| (last_symbol == CALC_SYMBOL_OPERATOR))))
{
value = 0;
decimals = 0;
factor = 1;
if (expr[i] == '-')
{
factor = -1;
i++;
}
while (expr[i]
&& (isdigit ((unsigned char)expr[i]) || (expr[i] == '.')))
{
if (expr[i] == '.')
{
if (decimals == 0)
decimals = 10;
}
else
{
if (decimals)
{
value = value + (((double)(expr[i] - '0')) / decimals);
decimals *= 10;
}
else
{
value = (value * 10) + (expr[i] - '0');
}
}
i++;
}
i--;
value *= factor;
ptr_value = malloc (sizeof (value));
*ptr_value = value;
arraylist_add (list_values, ptr_value);
last_symbol = CALC_SYMBOL_VALUE;
}
else if (expr[i] == ')')
{
index_op = arraylist_size (list_ops) - 1;
while (index_op >= 0)
{
ptr_operator = arraylist_get (list_ops, index_op);
if (strcmp (ptr_operator, "(") == 0)
break;
calc_operation_stacks (list_values, list_ops);
index_op--;
}
/* remove "(" from operators */
index_op = arraylist_size (list_ops) - 1;
if (index_op >= 0)
arraylist_remove (list_ops, index_op);
last_symbol = CALC_SYMBOL_PARENTHESIS_CLOSE;
}
else
{
/* operator */
i2 = i + 1;
while (expr[i2] && (expr[i2] != ' ') && (expr[i2] != '(')
&& (expr[i2] != ')') && (expr[i2] != '.')
&& (expr[i2] != '-') && !isdigit ((unsigned char)expr[i2]))
{
i2++;
}
operator = string_strndup (expr + i, i2 - i);
i = i2 - 1;
if (operator)
{
index_op = arraylist_size (list_ops) - 1;
while (index_op >= 0)
{
ptr_operator = arraylist_get (list_ops, index_op);
if (calc_operator_precedence (ptr_operator) <
calc_operator_precedence (operator))
break;
calc_operation_stacks (list_values, list_ops);
index_op--;
}
arraylist_add (list_ops, operator);
}
last_symbol = CALC_SYMBOL_OPERATOR;
}
}
while (arraylist_size (list_ops) > 0)
{
calc_operation_stacks (list_values, list_ops);
}
value = calc_pop_value (list_values);
calc_format_result (value, str_result, sizeof (str_result));
end:
if (list_values)
arraylist_free (list_values);
if (list_ops)
arraylist_free (list_ops);
return strdup (str_result);
}