mirror of
https://github.com/Gnucash/gnucash.git
synced 2024-11-23 01:16:43 -06:00
650 lines
17 KiB
C
650 lines
17 KiB
C
/********************************************************************\
|
|
* gnc-exp-parser.c -- Implementation of expression parsing for *
|
|
* GnuCash using the routines in 'calculation'. *
|
|
* Copyright (C) 2000 Dave Peticolas <dave@krondo.com> *
|
|
* *
|
|
* This program 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 2 of *
|
|
* the License, or (at your option) any later version. *
|
|
* *
|
|
* This program 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 this program; if not, write to the Free Software *
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. *
|
|
\********************************************************************/
|
|
|
|
#include <config.h>
|
|
|
|
#include <glib.h>
|
|
#include <glib/gi18n.h>
|
|
#include <libguile.h>
|
|
#include <ctype.h>
|
|
#include <locale.h>
|
|
#include <string.h>
|
|
|
|
#include "gfec.h"
|
|
#include "finproto.h"
|
|
#include "fin_spl_protos.h"
|
|
#include "gnc-filepath-utils.h"
|
|
#include "gnc-gkeyfile-utils.h"
|
|
#include "gnc-hooks.h"
|
|
#include "gnc-exp-parser.h"
|
|
#include "gnc-ui-util.h"
|
|
#include "gnc-locale-utils.h"
|
|
#include "guile-mappings.h"
|
|
|
|
#define GEP_GROUP_NAME "Variables"
|
|
|
|
static QofLogModule log_module = GNC_MOD_GUI;
|
|
|
|
/** Data Types *****************************************************/
|
|
|
|
typedef struct ParserNum
|
|
{
|
|
gnc_numeric value;
|
|
} ParserNum;
|
|
|
|
|
|
/** Static Globals *************************************************/
|
|
static GHashTable *variable_bindings = NULL;
|
|
static ParseError last_error = PARSER_NO_ERROR;
|
|
static GNCParseError last_gncp_error = NO_ERR;
|
|
static gboolean parser_inited = FALSE;
|
|
|
|
|
|
/** Implementations ************************************************/
|
|
|
|
static gchar *
|
|
gnc_exp_parser_filname (void)
|
|
{
|
|
return gnc_build_userdata_path("expressions-2.0");
|
|
}
|
|
|
|
void
|
|
gnc_exp_parser_init ( void )
|
|
{
|
|
gnc_exp_parser_real_init( TRUE );
|
|
}
|
|
|
|
void
|
|
gnc_exp_parser_real_init ( gboolean addPredefined )
|
|
{
|
|
gchar *filename, **keys, **key, *str_value;
|
|
GKeyFile *key_file;
|
|
gnc_numeric value;
|
|
|
|
if (parser_inited)
|
|
gnc_exp_parser_shutdown ();
|
|
|
|
/* The parser uses fin.scm for financial functions, so load it here. */
|
|
scm_primitive_load_path(scm_from_utf8_string("gnucash/app-utils/fin"));
|
|
variable_bindings = g_hash_table_new (g_str_hash, g_str_equal);
|
|
|
|
/* This comes after the statics have been initialized. Not at the end! */
|
|
parser_inited = TRUE;
|
|
|
|
if ( addPredefined )
|
|
{
|
|
filename = gnc_exp_parser_filname();
|
|
key_file = gnc_key_file_load_from_file(filename, TRUE, FALSE, NULL);
|
|
if (key_file)
|
|
{
|
|
keys = g_key_file_get_keys(key_file, GEP_GROUP_NAME, NULL, NULL);
|
|
for (key = keys; key && *key; key++)
|
|
{
|
|
str_value = g_key_file_get_string(key_file, GEP_GROUP_NAME, *key, NULL);
|
|
if (str_value && string_to_gnc_numeric(str_value, &value))
|
|
{
|
|
gnc_exp_parser_set_value (*key, gnc_numeric_reduce (value));
|
|
}
|
|
}
|
|
g_strfreev(keys);
|
|
g_key_file_free(key_file);
|
|
}
|
|
g_free(filename);
|
|
}
|
|
|
|
gnc_hook_add_dangler(HOOK_SHUTDOWN, (GFunc)gnc_exp_parser_shutdown, NULL, NULL);
|
|
}
|
|
|
|
static gboolean
|
|
remove_binding (gpointer key, gpointer value, gpointer not_used)
|
|
{
|
|
g_free(key);
|
|
g_free(value);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
set_one_key (gpointer key, gpointer value, gpointer data)
|
|
{
|
|
char *name = key;
|
|
ParserNum *pnum = value;
|
|
char *num_str;
|
|
|
|
num_str = gnc_numeric_to_string (gnc_numeric_reduce (pnum->value));
|
|
g_key_file_set_string ((GKeyFile *)data, GEP_GROUP_NAME, name, num_str);
|
|
g_free (num_str);
|
|
}
|
|
|
|
void
|
|
gnc_exp_parser_shutdown (void)
|
|
{
|
|
GKeyFile* key_file;
|
|
gchar *filename;
|
|
|
|
if (!parser_inited)
|
|
return;
|
|
|
|
filename = gnc_exp_parser_filname();
|
|
key_file = g_key_file_new();
|
|
g_hash_table_foreach (variable_bindings, set_one_key, key_file);
|
|
g_key_file_set_comment(key_file, GEP_GROUP_NAME, NULL,
|
|
" Variables are in the form 'name=value'",
|
|
NULL);
|
|
gnc_key_file_save_to_file(filename, key_file, NULL);
|
|
g_key_file_free(key_file);
|
|
g_free(filename);
|
|
|
|
g_hash_table_foreach_remove (variable_bindings, remove_binding, NULL);
|
|
g_hash_table_destroy (variable_bindings);
|
|
variable_bindings = NULL;
|
|
|
|
last_error = PARSER_NO_ERROR;
|
|
last_gncp_error = NO_ERR;
|
|
|
|
parser_inited = FALSE;
|
|
|
|
gnc_hook_run(HOOK_SAVE_OPTIONS, NULL);
|
|
}
|
|
|
|
void
|
|
gnc_exp_parser_remove_variable (const char *variable_name)
|
|
{
|
|
gpointer key;
|
|
gpointer value;
|
|
|
|
if (!parser_inited)
|
|
return;
|
|
|
|
if (variable_name == NULL)
|
|
return;
|
|
|
|
if (g_hash_table_lookup_extended (variable_bindings, variable_name,
|
|
&key, &value))
|
|
{
|
|
g_hash_table_remove (variable_bindings, key);
|
|
g_free(key);
|
|
g_free(value);
|
|
}
|
|
}
|
|
|
|
void
|
|
gnc_exp_parser_set_value (const char * variable_name, gnc_numeric value)
|
|
{
|
|
char *key;
|
|
ParserNum *pnum;
|
|
|
|
if (variable_name == NULL)
|
|
return;
|
|
|
|
if (!parser_inited)
|
|
gnc_exp_parser_init ();
|
|
|
|
gnc_exp_parser_remove_variable (variable_name);
|
|
|
|
key = g_strdup (variable_name);
|
|
|
|
pnum = g_new0(ParserNum, 1);
|
|
pnum->value = value;
|
|
|
|
g_hash_table_insert (variable_bindings, key, pnum);
|
|
}
|
|
|
|
static void
|
|
make_predefined_vars_helper (gpointer key, gpointer value, gpointer data)
|
|
{
|
|
var_store_ptr *vars_p = data;
|
|
ParserNum *pnum_old = value;
|
|
var_store_ptr var;
|
|
ParserNum *pnum;
|
|
|
|
var = g_new0 (var_store, 1);
|
|
|
|
pnum = g_new0 (ParserNum, 1);
|
|
*pnum = *pnum_old;
|
|
|
|
var->variable_name = g_strdup(key);
|
|
var->value = pnum;
|
|
var->next_var = *vars_p;
|
|
|
|
*vars_p = var;
|
|
}
|
|
|
|
static void
|
|
make_predefined_vars_from_external_helper( gpointer key, gpointer value, gpointer data )
|
|
{
|
|
ParserNum *pnum = g_new0( ParserNum, 1 );
|
|
if ( value != NULL )
|
|
pnum->value = *(gnc_numeric*)value;
|
|
|
|
make_predefined_vars_helper( key, pnum, data );
|
|
g_free(pnum); /* make_predefined_vars_helper allocs its own copy. */
|
|
}
|
|
|
|
static var_store_ptr
|
|
make_predefined_variables (void)
|
|
{
|
|
var_store_ptr vars = NULL;
|
|
|
|
g_hash_table_foreach (variable_bindings, make_predefined_vars_helper, &vars);
|
|
|
|
return vars;
|
|
}
|
|
|
|
static void
|
|
free_predefined_variables (var_store_ptr vars)
|
|
{
|
|
var_store_ptr next;
|
|
|
|
while (vars != NULL)
|
|
{
|
|
next = vars->next_var;
|
|
|
|
g_free(vars->variable_name);
|
|
vars->variable_name = NULL;
|
|
|
|
g_free(vars->value);
|
|
vars->value = NULL;
|
|
|
|
g_free(vars);
|
|
|
|
vars = next;
|
|
}
|
|
}
|
|
|
|
static void
|
|
update_variables (var_store_ptr vars)
|
|
{
|
|
for ( ; vars ; vars = vars->next_var )
|
|
{
|
|
ParserNum *pnum = vars->value;
|
|
if (pnum != NULL)
|
|
gnc_exp_parser_set_value (vars->variable_name, pnum->value);
|
|
}
|
|
}
|
|
|
|
static char* _function_evaluation_error_msg = NULL;
|
|
static void
|
|
_exception_handler(const char *error_message)
|
|
{
|
|
_function_evaluation_error_msg = (char*)error_message;
|
|
}
|
|
|
|
static
|
|
void*
|
|
func_op(const char *fname, int argc, void **argv)
|
|
{
|
|
SCM scmFn, scmArgs, scmTmp;
|
|
int i;
|
|
var_store *vs;
|
|
gchar *str;
|
|
gnc_numeric n, *result;
|
|
GString *realFnName;
|
|
|
|
realFnName = g_string_sized_new( strlen(fname) + 5 );
|
|
g_string_printf( realFnName, "gnc:%s", fname );
|
|
scmFn = scm_internal_catch(SCM_BOOL_T,
|
|
(scm_t_catch_body)scm_c_eval_string, realFnName->str,
|
|
scm_handle_by_message_noexit, NULL);
|
|
g_string_free( realFnName, TRUE );
|
|
if (!scm_is_procedure(scmFn))
|
|
{
|
|
/* FIXME: handle errors correctly. */
|
|
printf( "gnc:\"%s\" is not a scm procedure\n", fname );
|
|
return NULL;
|
|
}
|
|
scmArgs = scm_list_n (SCM_UNDEFINED);
|
|
for ( i = 0; i < argc; i++ )
|
|
{
|
|
/* cons together back-to-front. */
|
|
vs = (var_store*)argv[argc - i - 1];
|
|
switch ( vs->type )
|
|
{
|
|
case VST_NUMERIC:
|
|
n = *(gnc_numeric*)(vs->value);
|
|
scmTmp = scm_from_double ( gnc_numeric_to_double( n ) );
|
|
break;
|
|
case VST_STRING:
|
|
str = (char*)(vs->value);
|
|
scmTmp = scm_from_utf8_string( str );
|
|
break;
|
|
default:
|
|
/* FIXME: error */
|
|
printf( "argument %d not a numeric or string [type = %d]\n",
|
|
i, vs->type );
|
|
return NULL;
|
|
break; /* notreached */
|
|
}
|
|
scmArgs = scm_cons( scmTmp, scmArgs );
|
|
}
|
|
|
|
//scmTmp = scm_apply(scmFn, scmArgs , SCM_EOL);
|
|
scmTmp = gfec_apply(scmFn, scmArgs, _exception_handler);
|
|
if (_function_evaluation_error_msg != NULL)
|
|
{
|
|
PERR("function eval error: [%s]\n", _function_evaluation_error_msg);
|
|
_function_evaluation_error_msg = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
if (!scm_is_number (scmTmp))
|
|
{
|
|
PERR("function gnc:%s does not return a number", fname);
|
|
return NULL;
|
|
}
|
|
|
|
result = g_new0( gnc_numeric, 1 );
|
|
*result = double_to_gnc_numeric( scm_to_double(scmTmp),
|
|
GNC_DENOM_AUTO,
|
|
GNC_HOW_DENOM_SIGFIGS(12) | GNC_HOW_RND_ROUND_HALF_UP );
|
|
if (gnc_numeric_check (*result) != GNC_ERROR_OK)
|
|
{
|
|
PERR("Attempt to convert %f to GncNumeric Failed: %s",
|
|
scm_to_double(scmTmp),
|
|
gnc_numeric_errorCode_to_string (gnc_numeric_check (*result)));
|
|
g_free (result);
|
|
return NULL;
|
|
}
|
|
/* FIXME: cleanup scmArgs = scm_list, cons'ed cells? */
|
|
return (void*)result;
|
|
}
|
|
|
|
static void *
|
|
trans_numeric(const char *digit_str,
|
|
gchar *radix_point,
|
|
gchar *group_char,
|
|
char **rstr)
|
|
{
|
|
ParserNum *pnum;
|
|
gnc_numeric value;
|
|
|
|
if (digit_str == NULL)
|
|
return NULL;
|
|
|
|
if (!xaccParseAmount (digit_str, TRUE, &value, rstr))
|
|
return NULL;
|
|
|
|
pnum = g_new0(ParserNum, 1);
|
|
pnum->value = value;
|
|
|
|
return pnum;
|
|
}
|
|
|
|
static void *
|
|
numeric_ops(char op_sym,
|
|
void *left_value,
|
|
void *right_value)
|
|
{
|
|
ParserNum *left = left_value;
|
|
ParserNum *right = right_value;
|
|
ParserNum *result;
|
|
|
|
if ((left == NULL) || (right == NULL))
|
|
return NULL;
|
|
|
|
result = (op_sym == ASN_OP) ? left : g_new0(ParserNum, 1);
|
|
|
|
switch (op_sym)
|
|
{
|
|
case ADD_OP:
|
|
result->value = gnc_numeric_add (left->value, right->value,
|
|
GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
|
|
break;
|
|
case SUB_OP:
|
|
result->value = gnc_numeric_sub (left->value, right->value,
|
|
GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
|
|
break;
|
|
case DIV_OP:
|
|
result->value = gnc_numeric_div (left->value, right->value,
|
|
GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
|
|
break;
|
|
case MUL_OP:
|
|
result->value = gnc_numeric_mul (left->value, right->value,
|
|
GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
|
|
break;
|
|
case ASN_OP:
|
|
result->value = right->value;
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void *
|
|
negate_numeric(void *value)
|
|
{
|
|
ParserNum *result = value;
|
|
|
|
if (value == NULL)
|
|
return NULL;
|
|
|
|
result->value = gnc_numeric_neg (result->value);
|
|
|
|
return result;
|
|
}
|
|
|
|
static
|
|
void
|
|
gnc_ep_tmpvarhash_check_vals( gpointer key, gpointer value, gpointer user_data )
|
|
{
|
|
gboolean *allVarsHaveValues = (gboolean*)user_data;
|
|
gnc_numeric *num = (gnc_numeric*)value;
|
|
*allVarsHaveValues &= ( num && gnc_numeric_check( *num ) != GNC_ERROR_ARG );
|
|
}
|
|
|
|
static
|
|
void
|
|
gnc_ep_tmpvarhash_clean( gpointer key, gpointer value, gpointer user_data )
|
|
{
|
|
if ( key )
|
|
{
|
|
g_free( (gchar*)key );
|
|
}
|
|
if ( value )
|
|
{
|
|
g_free( (gnc_numeric*)value );
|
|
}
|
|
}
|
|
|
|
gboolean
|
|
gnc_exp_parser_parse( const char * expression, gnc_numeric *value_p,
|
|
char **error_loc_p )
|
|
{
|
|
GHashTable *tmpVarHash;
|
|
gboolean ret, toRet = TRUE;
|
|
gboolean allVarsHaveValues = TRUE;
|
|
|
|
tmpVarHash = g_hash_table_new( g_str_hash, g_str_equal );
|
|
ret = gnc_exp_parser_parse_separate_vars( expression, value_p,
|
|
error_loc_p, tmpVarHash );
|
|
if ( !ret )
|
|
{
|
|
toRet = ret;
|
|
goto cleanup;
|
|
}
|
|
|
|
g_hash_table_foreach( tmpVarHash,
|
|
gnc_ep_tmpvarhash_check_vals,
|
|
&allVarsHaveValues );
|
|
if ( !allVarsHaveValues )
|
|
{
|
|
toRet = FALSE;
|
|
last_gncp_error = VARIABLE_IN_EXP;
|
|
}
|
|
|
|
cleanup:
|
|
g_hash_table_foreach( tmpVarHash, gnc_ep_tmpvarhash_clean, NULL );
|
|
g_hash_table_destroy( tmpVarHash );
|
|
|
|
return toRet;
|
|
}
|
|
|
|
gboolean
|
|
gnc_exp_parser_parse_separate_vars (const char * expression,
|
|
gnc_numeric *value_p,
|
|
char **error_loc_p,
|
|
GHashTable *varHash )
|
|
{
|
|
parser_env_ptr pe;
|
|
var_store_ptr vars;
|
|
struct lconv *lc;
|
|
var_store result;
|
|
char * error_loc;
|
|
ParserNum *pnum;
|
|
|
|
if (expression == NULL)
|
|
return FALSE;
|
|
|
|
if (!parser_inited)
|
|
gnc_exp_parser_real_init ( (varHash == NULL) );
|
|
|
|
result.variable_name = NULL;
|
|
result.value = NULL;
|
|
result.next_var = NULL;
|
|
|
|
vars = make_predefined_variables ();
|
|
|
|
if ( varHash != NULL )
|
|
{
|
|
g_hash_table_foreach( varHash, make_predefined_vars_from_external_helper, &vars);
|
|
}
|
|
|
|
lc = gnc_localeconv ();
|
|
|
|
pe = init_parser (vars, lc->mon_decimal_point, lc->mon_thousands_sep,
|
|
trans_numeric, numeric_ops, negate_numeric, g_free,
|
|
func_op);
|
|
|
|
error_loc = parse_string (&result, expression, pe);
|
|
|
|
pnum = result.value;
|
|
|
|
if (error_loc == NULL)
|
|
{
|
|
if (gnc_numeric_check (pnum->value))
|
|
{
|
|
if (error_loc_p != NULL)
|
|
*error_loc_p = (char *) expression;
|
|
|
|
last_error = NUMERIC_ERROR;
|
|
}
|
|
else
|
|
{
|
|
if (pnum)
|
|
{
|
|
if (value_p)
|
|
*value_p = gnc_numeric_reduce (pnum->value);
|
|
|
|
if (!result.variable_name)
|
|
g_free (pnum);
|
|
}
|
|
|
|
if (error_loc_p != NULL)
|
|
*error_loc_p = NULL;
|
|
|
|
last_error = PARSER_NO_ERROR;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (error_loc_p != NULL)
|
|
*error_loc_p = error_loc;
|
|
|
|
last_error = get_parse_error (pe);
|
|
}
|
|
|
|
if ( varHash != NULL )
|
|
{
|
|
var_store_ptr newVars;
|
|
gpointer maybeKey, maybeValue;
|
|
gnc_numeric *numericValue;
|
|
|
|
newVars = parser_get_vars( pe );
|
|
for ( ; newVars ; newVars = newVars->next_var )
|
|
{
|
|
if ( g_hash_table_lookup_extended( varHash, newVars->variable_name,
|
|
&maybeKey, &maybeValue ) )
|
|
{
|
|
g_hash_table_remove( varHash, maybeKey );
|
|
g_free( maybeKey );
|
|
g_free( maybeValue );
|
|
}
|
|
numericValue = g_new0( gnc_numeric, 1 );
|
|
*numericValue = ((ParserNum*)newVars->value)->value;
|
|
// WTF?
|
|
// numericValue = NULL;
|
|
g_hash_table_insert( varHash,
|
|
g_strdup(newVars->variable_name),
|
|
numericValue );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
update_variables (vars);
|
|
}
|
|
|
|
free_predefined_variables (vars);
|
|
|
|
exit_parser (pe);
|
|
|
|
return last_error == PARSER_NO_ERROR;
|
|
}
|
|
|
|
const char *
|
|
gnc_exp_parser_error_string (void)
|
|
{
|
|
if ( last_error == PARSER_NO_ERROR )
|
|
{
|
|
switch ( last_gncp_error )
|
|
{
|
|
default:
|
|
case NO_ERR:
|
|
return NULL;
|
|
break;
|
|
case VARIABLE_IN_EXP:
|
|
return _("Illegal variable in expression." );
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (last_error)
|
|
{
|
|
default:
|
|
case PARSER_NO_ERROR:
|
|
return NULL;
|
|
case UNBALANCED_PARENS:
|
|
return _("Unbalanced parenthesis");
|
|
case STACK_OVERFLOW:
|
|
return _("Stack overflow");
|
|
case STACK_UNDERFLOW:
|
|
return _("Stack underflow");
|
|
case UNDEFINED_CHARACTER:
|
|
return _("Undefined character");
|
|
case NOT_A_VARIABLE:
|
|
return _("Not a variable");
|
|
case NOT_A_FUNC:
|
|
return _("Not a defined function");
|
|
case PARSER_OUT_OF_MEMORY:
|
|
return _("Out of memory");
|
|
case NUMERIC_ERROR:
|
|
return _("Numeric error");
|
|
}
|
|
}
|