From 4cc2bfb9a9e84ee1aceba4ab504b2efb47a31f05 Mon Sep 17 00:00:00 2001 From: Dave Peticolas Date: Tue, 7 Aug 2001 23:36:04 +0000 Subject: [PATCH] Missed some. git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/trunk@5076 57a11ea4-9604-0410-9ed3-97b8803252fd --- src/engine/Account.c | 2163 +++++++++++++++++++++ src/engine/Account.h | 375 ++++ src/engine/AccountP.h | 207 +++ src/engine/Backend.c | 207 +++ src/engine/Backend.h | 96 + src/engine/BackendP.h | 230 +++ src/engine/DateUtils.c | 71 + src/engine/DateUtils.h | 37 + src/engine/FreqSpec.c | 878 +++++++++ src/engine/FreqSpec.h | 240 +++ src/engine/FreqSpecP.h | 112 ++ src/engine/GNCId.c | 326 ++++ src/engine/GNCId.h | 72 + src/engine/GNCIdP.h | 57 + src/engine/Group.c | 1111 +++++++++++ src/engine/Group.h | 327 ++++ src/engine/GroupP.h | 60 + src/engine/NetIO.c | 326 ++++ src/engine/NetIO.h | 32 + src/engine/Query.c | 2794 ++++++++++++++++++++++++++++ src/engine/Query.h | 348 ++++ src/engine/README | 14 + src/engine/README.query-api | 205 ++ src/engine/SchedXaction.c | 486 +++++ src/engine/SchedXaction.h | 214 +++ src/engine/Scrub.c | 506 +++++ src/engine/Scrub.h | 99 + src/engine/TransLog.c | 363 ++++ src/engine/TransLog.h | 46 + src/engine/Transaction.c | 2538 +++++++++++++++++++++++++ src/engine/Transaction.h | 476 +++++ src/engine/TransactionP.h | 236 +++ src/engine/date.c | 701 +++++++ src/engine/date.h | 206 ++ src/engine/design.txt | 397 ++++ src/engine/extensions.txt | 76 + src/engine/gnc-associate-account.c | 491 +++++ src/engine/gnc-associate-account.h | 89 + src/engine/gnc-book-p.h | 102 + src/engine/gnc-book.c | 1026 ++++++++++ src/engine/gnc-book.h | 210 +++ src/engine/gnc-commodity.c | 755 ++++++++ src/engine/gnc-commodity.h | 116 ++ src/engine/gnc-common.h | 31 + src/engine/gnc-engine-util.c | 392 ++++ src/engine/gnc-engine-util.h | 269 +++ src/engine/gnc-engine.c | 117 ++ src/engine/gnc-engine.h | 69 + src/engine/gnc-event-p.h | 45 + src/engine/gnc-event.c | 185 ++ src/engine/gnc-event.h | 84 + src/engine/gnc-numeric.c | 1268 +++++++++++++ src/engine/gnc-numeric.h | 167 ++ src/engine/gnc-pricedb-p.h | 89 + src/engine/gnc-pricedb.c | 1257 +++++++++++++ src/engine/gnc-pricedb.h | 289 +++ src/engine/guid.c | 571 ++++++ src/engine/guid.h | 112 ++ src/engine/kvp_doc.txt | 176 ++ src/engine/kvp_frame.c | 1107 +++++++++++ src/engine/kvp_frame.h | 197 ++ src/engine/md5.c | 444 +++++ src/engine/md5.h | 163 ++ 63 files changed, 26453 insertions(+) create mode 100644 src/engine/Account.c create mode 100644 src/engine/Account.h create mode 100644 src/engine/AccountP.h create mode 100644 src/engine/Backend.c create mode 100644 src/engine/Backend.h create mode 100644 src/engine/BackendP.h create mode 100644 src/engine/DateUtils.c create mode 100644 src/engine/DateUtils.h create mode 100644 src/engine/FreqSpec.c create mode 100644 src/engine/FreqSpec.h create mode 100644 src/engine/FreqSpecP.h create mode 100644 src/engine/GNCId.c create mode 100644 src/engine/GNCId.h create mode 100644 src/engine/GNCIdP.h create mode 100644 src/engine/Group.c create mode 100644 src/engine/Group.h create mode 100644 src/engine/GroupP.h create mode 100644 src/engine/NetIO.c create mode 100644 src/engine/NetIO.h create mode 100644 src/engine/Query.c create mode 100644 src/engine/Query.h create mode 100644 src/engine/README create mode 100644 src/engine/README.query-api create mode 100644 src/engine/SchedXaction.c create mode 100644 src/engine/SchedXaction.h create mode 100644 src/engine/Scrub.c create mode 100644 src/engine/Scrub.h create mode 100644 src/engine/TransLog.c create mode 100644 src/engine/TransLog.h create mode 100644 src/engine/Transaction.c create mode 100644 src/engine/Transaction.h create mode 100644 src/engine/TransactionP.h create mode 100644 src/engine/date.c create mode 100644 src/engine/date.h create mode 100644 src/engine/design.txt create mode 100644 src/engine/extensions.txt create mode 100644 src/engine/gnc-associate-account.c create mode 100644 src/engine/gnc-associate-account.h create mode 100644 src/engine/gnc-book-p.h create mode 100644 src/engine/gnc-book.c create mode 100644 src/engine/gnc-book.h create mode 100644 src/engine/gnc-commodity.c create mode 100644 src/engine/gnc-commodity.h create mode 100644 src/engine/gnc-common.h create mode 100644 src/engine/gnc-engine-util.c create mode 100644 src/engine/gnc-engine-util.h create mode 100644 src/engine/gnc-engine.c create mode 100644 src/engine/gnc-engine.h create mode 100644 src/engine/gnc-event-p.h create mode 100644 src/engine/gnc-event.c create mode 100644 src/engine/gnc-event.h create mode 100644 src/engine/gnc-numeric.c create mode 100644 src/engine/gnc-numeric.h create mode 100644 src/engine/gnc-pricedb-p.h create mode 100644 src/engine/gnc-pricedb.c create mode 100644 src/engine/gnc-pricedb.h create mode 100644 src/engine/guid.c create mode 100644 src/engine/guid.h create mode 100644 src/engine/kvp_doc.txt create mode 100644 src/engine/kvp_frame.c create mode 100644 src/engine/kvp_frame.h create mode 100644 src/engine/md5.c create mode 100644 src/engine/md5.h diff --git a/src/engine/Account.c b/src/engine/Account.c new file mode 100644 index 0000000000..97b96176f0 --- /dev/null +++ b/src/engine/Account.c @@ -0,0 +1,2163 @@ +/********************************************************************\ + * Account.c -- Account data structure implementation * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997, 1998, 1999, 2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#include "config.h" + +#include +#include +#include + +#include "Account.h" +#include "AccountP.h" +#include "BackendP.h" +#include "GNCIdP.h" +#include "Group.h" +#include "GroupP.h" +#include "Transaction.h" +#include "TransactionP.h" +#include "date.h" +#include "gnc-commodity.h" +#include "gnc-engine.h" +#include "gnc-engine-util.h" +#include "gnc-event-p.h" +#include "kvp_frame.h" +#include "messages.h" + +static short module = MOD_ENGINE; + + +/********************************************************************\ + * Because I can't use C++ for this project, doesn't mean that I * + * can't pretend to! These functions perform actions on the * + * account data structure, in order to encapsulate the knowledge * + * of the internals of the Account in one file. * +\********************************************************************/ + +static void xaccAccountBringUpToDate(Account *acc); + +/********************************************************************\ +\********************************************************************/ + +G_INLINE_FUNC void mark_account (Account *account); +G_INLINE_FUNC void +mark_account (Account *account) +{ + if (account->parent) + account->parent->saved = FALSE; + + gnc_engine_generate_event (&account->guid, GNC_EVENT_MODIFY); +} + +/********************************************************************\ +\********************************************************************/ + +static void +xaccInitAccount (Account * acc) +{ + acc->parent = NULL; + acc->children = NULL; + + acc->balance = gnc_numeric_zero(); + acc->cleared_balance = gnc_numeric_zero(); + acc->reconciled_balance = gnc_numeric_zero(); + + acc->starting_balance = gnc_numeric_zero(); + acc->starting_cleared_balance = gnc_numeric_zero(); + acc->starting_reconciled_balance = gnc_numeric_zero(); + + acc->type = NO_TYPE; + + acc->accountName = g_strdup(""); + acc->accountCode = g_strdup(""); + acc->description = g_strdup(""); + + acc->kvp_data = kvp_frame_new(); + acc->idata = 0; + + acc->commodity = NULL; + acc->commodity_scu = 100000; + + acc->splits = NULL; + + acc->version = 0; + acc->version_check = 0; + acc->editlevel = 0; + acc->balance_dirty = FALSE; + acc->sort_dirty = FALSE; + acc->core_dirty = FALSE; + acc->do_free = FALSE; + + xaccGUIDNew(&acc->guid); + xaccStoreEntity(acc, &acc->guid, GNC_ID_ACCOUNT); + LEAVE ("account=%p\n", acc); +} + +/********************************************************************\ +\********************************************************************/ + +Account * +xaccMallocAccount (void) +{ + Account *acc = g_new (Account, 1); + + xaccInitAccount (acc); + + gnc_engine_generate_event (&acc->guid, GNC_EVENT_CREATE); + + return acc; +} + +Account * +xaccCloneAccountSimple(const Account *from) +{ + Account *ret; + + ret = xaccMallocAccount(); + + ret->type = from->type; + + ret->accountName = g_strdup(from->accountName); + ret->accountCode = g_strdup(from->accountCode); + ret->description = g_strdup(from->description); + + ret->kvp_data = kvp_frame_copy(from->kvp_data); + + xaccAccountSetCommodity (ret, from->commodity); + + return ret; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccFreeAccount (Account *acc) +{ + Transaction *t; + GList *lp; + + if (NULL == acc) return; + + gnc_engine_generate_event (&acc->guid, GNC_EVENT_DESTROY); + + xaccRemoveEntity(&acc->guid); + + if (acc->children) + { + PERR (" xinstead of calling xaccFreeAccount(), please call \n" + " xaccAccountBeginEdit(); xaccAccountDestroy(); \n"); + + /* First, recursively free children */ + xaccFreeAccountGroup (acc->children); + acc->children = NULL; + } + + /* Next, clean up the splits */ + /* NB there shouldn't be any splits by now ... they should + * have been all been freed by CommitEdit(). We can remove this + * check once we know the warning isn't occurring any more. */ + if (acc->splits) + { + PERR (" instead of calling xaccFreeAccount(), please call \n" + " xaccAccountBeginEdit(); xaccAccountDestroy(); \n"); + + /* any split pointing at this account needs to be unmarked */ + for(lp = acc->splits; lp; lp = lp->next) + { + xaccSplitSetAccount((Split *) lp->data, NULL); + } + + acc->editlevel = 0; + + for(lp = acc->splits; lp; lp = lp->next) { + Split *s = (Split *) lp->data; + t = s->parent; + xaccTransBeginEdit (t); + xaccSplitDestroy (s); + xaccTransCommitEdit (t); + } + + /* free up array of split pointers */ + g_list_free(acc->splits); + acc->splits = NULL; + } + + if (acc->accountName) g_free (acc->accountName); + acc->accountName = NULL; + if (acc->accountCode) g_free (acc->accountCode); + acc->accountCode = NULL; + if (acc->description) g_free (acc->description); + acc->description = NULL; + + kvp_frame_delete (acc->kvp_data); + acc->kvp_data = NULL; + + /* zero out values, just in case stray + * pointers are pointing here. */ + + acc->commodity = NULL; + + acc->parent = NULL; + acc->children = NULL; + + acc->balance = gnc_numeric_zero(); + acc->cleared_balance = gnc_numeric_zero(); + acc->reconciled_balance = gnc_numeric_zero(); + + acc->type = NO_TYPE; + + acc->accountName = NULL; + acc->description = NULL; + acc->commodity = NULL; + + acc->version = 0; + acc->editlevel = 0; + acc->balance_dirty = FALSE; + acc->sort_dirty = FALSE; + acc->core_dirty = FALSE; + + g_free(acc); +} + +/********************************************************************\ + * transactional routines +\********************************************************************/ + +void +xaccAccountBeginEdit (Account *acc) +{ + Backend * be; + if (!acc) return; + + acc->editlevel++; + if (1 < acc->editlevel) return; + + if (0 >= acc->editlevel) + { + PERR ("unbalanced call - resetting (was %d)", acc->editlevel); + acc->editlevel = 1; + } + + acc->core_dirty = FALSE; + + /* See if there's a backend. If there is, invoke it. */ + be = xaccAccountGetBackend (acc); + if (be && be->account_begin_edit) { + (be->account_begin_edit) (be, acc); + } +} + +void +xaccAccountCommitEdit (Account *acc) +{ + Backend * be; + + if (!acc) return; + + acc->editlevel--; + if (0 < acc->editlevel) return; + + if (0 > acc->editlevel) + { + PERR ("unbalanced call - resetting (was %d)", acc->editlevel); + acc->editlevel = 0; + } + + /* If marked for deletion, get rid of subaccounts first, + * and then the splits ... */ + if (acc->do_free) + { + GList *lp; + + /* First, recursively free children */ + xaccFreeAccountGroup (acc->children); + acc->children = NULL; + + PINFO ("freeing splits for account %p (%s)\n", + acc, acc->accountName ? acc->accountName : "(null)"); + + /* any split pointing at this account needs to be unmarked */ + for(lp = acc->splits; lp; lp = lp->next) + { + xaccSplitSetAccount((Split *) lp->data, NULL); + } + + for(lp = acc->splits; lp; lp = lp->next) + { + Split *s = (Split *) lp->data; + Transaction *t = s->parent; + xaccTransBeginEdit (t); + xaccSplitDestroy (s); + xaccTransCommitEdit (t); + } + + /* free up array of split pointers */ + g_list_free(acc->splits); + acc->splits = NULL; + + acc->core_dirty = TRUE; + } + else + { + xaccAccountBringUpToDate(acc); + } + + /* See if there's a backend. If there is, invoke it. */ + be = xaccAccountGetBackend (acc); + if (be && be->account_commit_edit) + { + GNCBackendError errcode; + + /* clear errors */ + do { + errcode = xaccBackendGetError (be); + } while (ERR_BACKEND_NO_ERR != errcode); + + (be->account_commit_edit) (be, acc); + errcode = xaccBackendGetError (be); + + if (ERR_BACKEND_NO_ERR != errcode) + { + /* destroys must be rolled back as well ... ??? */ + acc->do_free = FALSE; + /* XXX hack alert FIXME implement account rollback */ + PERR (" backend asked engine to rollback, but this isn't" + " handled yet. Return code=%d", errcode); + /* push error back onto the stack */ + xaccBackendSetError (be, errcode); + } + } + acc->core_dirty = FALSE; + + /* final stages of freeing the account */ + if (acc->do_free) + { + xaccGroupRemoveAccount(acc->parent, acc); + xaccFreeAccount(acc); + } +} + +void +xaccAccountDestroy (Account *acc) +{ + if (!acc) return; + acc->do_free = TRUE; + + xaccAccountCommitEdit (acc); +} + + +void +xaccAccountSetVersion (Account *acc, gint32 vers) +{ + if (!acc) return; + acc->version = vers; +} + +gint32 +xaccAccountGetVersion (Account *acc) +{ + if (!acc) return 0; + return (acc->version); +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccAccountEqual(Account *aa, Account *ab, gboolean check_guids) { + if(!aa && !ab) return TRUE; + if(!aa) return FALSE; + if(!ab) return FALSE; + + if(aa->type != ab->type) return FALSE; + + if(safe_strcmp(aa->accountName, ab->accountName) != 0) return FALSE; + if(safe_strcmp(aa->accountCode, ab->accountCode) != 0) return FALSE; + if(safe_strcmp(aa->description, ab->description) != 0) return FALSE; + if(!gnc_commodity_equiv(aa->commodity, ab->commodity)) return FALSE; + + if(check_guids) { + if(!guid_equal(&aa->guid, &ab->guid)) return FALSE; + } + + if(kvp_frame_compare(aa->kvp_data, ab->kvp_data) != 0) return FALSE; + + /* no parent; always compare downwards. */ + + { + GList *la = aa->splits; + GList *lb = ab->splits; + + if( la && !lb) return FALSE; + if(!la && lb) return FALSE; + if(la && lb) { + /* presume that the splits are in the same order */ + while(la && lb) { + Split *sa = (Split *) la->data; + Split *sb = (Split *) lb->data; + if(!xaccSplitEqual(sa, sb, check_guids, FALSE)) return(FALSE); + la = la->next; + lb = lb->next; + } + if((la != NULL) || (lb != NULL)) return(FALSE); + } + } + + if(!xaccGroupEqual(aa->children, ab->children, check_guids)) return FALSE; + + return(TRUE); +} + +/********************************************************************\ +\********************************************************************/ + +static gint +split_sort_func(gconstpointer a, gconstpointer b) { + /* don't coerce xaccSplitDateOrder so we'll catch changes */ + Split *sa = (Split *) a; + Split *sb = (Split *) b; + return(xaccSplitDateOrder(sa, sb)); +} + +static void +xaccAccountSortSplits (Account *acc) +{ + if(!acc) return; + + if(!acc->sort_dirty) return; + if(acc->editlevel > 0) return; + acc->splits = g_list_sort(acc->splits, split_sort_func); + acc->sort_dirty = FALSE; +} + +static void +xaccAccountBringUpToDate(Account *acc) +{ + if(!acc) return; + + /* if a re-sort happens here, then everything will update, so the + cost basis and balance calls are no-ops */ + xaccAccountSortSplits(acc); + xaccAccountRecomputeBalance(acc); +} + + +/******************************************************************** + * xaccAccountGetSlots + ********************************************************************/ + +kvp_frame * +xaccAccountGetSlots(Account * account) { + if (!account) return NULL; + return(account->kvp_data); +} + +void +xaccAccountSetSlots_nc(Account *account, kvp_frame *frame) +{ + if (!account) return; + if (frame == account->kvp_data) return; + + xaccAccountBeginEdit (account); + if(account->kvp_data) + { + kvp_frame_delete (account->kvp_data); + } + account->kvp_data = frame; + xaccAccountCommitEdit (account); +} + + +/********************************************************************\ +\********************************************************************/ + +const GUID * +xaccAccountGetGUID (Account *account) +{ + if (!account) + return xaccGUIDNULL(); + + return &account->guid; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetGUID (Account *account, const GUID *guid) +{ + if (!account || !guid) return; + + PINFO("acct=%p", account); + xaccAccountBeginEdit (account); + xaccRemoveEntity(&account->guid); + + account->guid = *guid; + + xaccStoreEntity(account, &account->guid, GNC_ID_ACCOUNT); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ +\********************************************************************/ + +Account * +xaccAccountLookup (const GUID *guid) +{ + if (!guid) return NULL; + return xaccLookupEntity(guid, GNC_ID_ACCOUNT); +} + +/********************************************************************\ +\********************************************************************/ + +short +xaccAccountGetMark (Account *acc) +{ + if (!acc) return 0; + return acc->mark; +} + +void +xaccAccountSetMark (Account *acc, short m) +{ + if (!acc) return; + acc->mark = m; +} + +void +xaccClearMark (Account *acc, short val) +{ + AccountGroup *topgrp; + + if (!acc) return; + topgrp = xaccGetAccountRoot (acc); + + if (topgrp) + { + GList *list; + GList *node; + + list = xaccGroupGetAccountList (topgrp); + + for (node = list; node; node = node->next) + { + Account *account = node->data; + + xaccClearMarkDown (account, val); + } + } + else + xaccClearMarkDown (acc, val); +} + +void +xaccClearMarkDown (Account *acc, short val) +{ + AccountGroup *children; + + if (!acc) return; + acc->mark = val; + + children = acc->children; + if (children) + { + GList *list; + GList *node; + + list = xaccGroupGetAccountList (children); + + for (node = list; node; node = node->next) + { + Account *account = node->data; + + xaccClearMarkDown (account, val); + } + } +} + +void +xaccClearMarkDownGr (AccountGroup *grp, short val) +{ + GList *list; + GList *node; + + if (!grp) return; + + list = xaccGroupGetAccountList (grp); + + for (node = list; node; node = node->next) + { + Account *account = node->data; + + xaccClearMarkDown (account, val); + } +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountInsertSplit (Account *acc, Split *split) +{ + if (!acc) return; + if (!split) return; + + xaccAccountBeginEdit(acc); + + acc->balance_dirty = TRUE; + acc->sort_dirty = TRUE; + + /* convert the split to the new account's denominator */ + /* if the denominator can't be exactly converted, it's an error */ + /* FIXME : need to enforce ordering of insertion/value */ + split->amount = gnc_numeric_convert(split->amount, + xaccAccountGetCommoditySCU(acc), + GNC_RND_ROUND); + + /* if this split belongs to another account, remove it from there + * first. We don't want to ever leave the system in an inconsistent + * state. Note that it might belong to the current account if we're + * just using this call to re-order. */ + if (xaccSplitGetAccount(split) && + xaccSplitGetAccount(split) != acc) + xaccAccountRemoveSplit (xaccSplitGetAccount(split), split); + xaccSplitSetAccount(split, acc); + + if(g_list_index(acc->splits, split) == -1) + { + if (acc->editlevel == 1) + { + acc->splits = g_list_insert_sorted(acc->splits, split, + split_sort_func); + acc->sort_dirty = FALSE; + } + else + acc->splits = g_list_prepend(acc->splits, split); + + mark_account (acc); + if (split->parent) + gnc_engine_generate_event (&split->parent->guid, GNC_EVENT_MODIFY); + } + + xaccAccountCommitEdit(acc); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountRemoveSplit (Account *acc, Split *split) +{ + if (!acc) return; + if (!split) return; + + xaccAccountBeginEdit(acc); + { + GList *node; + + node = g_list_find (acc->splits, split); + if (!node) + { + PERR ("split not in account"); + } + else + { + acc->splits = g_list_remove_link (acc->splits, node); + g_list_free_1 (node); + + acc->balance_dirty = TRUE; + xaccSplitSetAccount(split, NULL); + + mark_account (acc); + if (split->parent) + gnc_engine_generate_event (&split->parent->guid, GNC_EVENT_MODIFY); + } + } + xaccAccountCommitEdit(acc); +} + + +/********************************************************************\ + * xaccAccountRecomputeBalance * + * recomputes the partial balances and the current balance for * + * this account. * + * * + * The way the computation is done depends on whether the partial * + * balances are for a monetary account (bank, cash, etc.) or a * + * certificate account (stock portfolio, mutual fund). For bank * + * accounts, the invariant amount is the dollar amount. For share * + * accounts, the invariant amount is the number of shares. For * + * share accounts, the share price fluctuates, and the current * + * value of such an account is the number of shares times the * + * current share price. * + * * + * Part of the complexity of this computation stems from the fact * + * xacc uses a double-entry system, meaning that one transaction * + * appears in two accounts: one account is debited, and the other * + * is credited. When the transaction represents a sale of shares, * + * or a purchase of shares, some care must be taken to compute * + * balances correctly. For a sale of shares, the stock account must* + * be debited in shares, but the bank account must be credited * + * in dollars. Thus, two different mechanisms must be used to * + * compute balances, depending on account type. * + * * + * Args: account -- the account for which to recompute balances * + * Return: void * +\********************************************************************/ + +void +xaccAccountRecomputeBalance (Account * acc) +{ + gnc_numeric balance; + gnc_numeric cleared_balance; + gnc_numeric reconciled_balance; + Split *last_split = NULL; + GList *lp; + + if (NULL == acc) return; + if (acc->editlevel > 0) return; + if (!acc->balance_dirty) return; + if (acc->do_free) return; + + balance = acc->starting_balance; + cleared_balance = acc->starting_cleared_balance; + reconciled_balance = acc->starting_reconciled_balance; + + for(lp = acc->splits; lp; lp = lp->next) { + Split *split = (Split *) lp->data; + + balance = gnc_numeric_add_fixed(balance, split->amount); + + if (NREC != split->reconciled) + cleared_balance = gnc_numeric_add_fixed(cleared_balance, split->amount); + + if (YREC == split->reconciled || + FREC == split->reconciled) { + reconciled_balance = + gnc_numeric_add_fixed(reconciled_balance, split->amount); + } + + split->balance = balance; + split->cleared_balance = cleared_balance; + split->reconciled_balance = reconciled_balance; + + last_split = split; + } + + acc->balance = balance; + acc->cleared_balance = cleared_balance; + acc->reconciled_balance = reconciled_balance; + + acc->balance_dirty = FALSE; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetStartingBalance(Account *acc, + const gnc_numeric start_baln, + const gnc_numeric start_cleared_baln, + const gnc_numeric start_reconciled_baln) +{ + if (!acc) return; + + acc->starting_balance = start_baln; + acc->starting_cleared_balance = start_cleared_baln; + acc->starting_reconciled_balance = start_reconciled_baln; + + acc->balance_dirty = TRUE; +} + +/********************************************************************\ + * xaccAccountFixSplitDateOrder * + * check this split to see if the date is in correct order * + * If it is not, reorder the transactions ... * + * * + * Args: acc -- the account to check * + * split -- the split to check * + * * + * Return: int -- non-zero if out of order * +\********************************************************************/ + +void +xaccAccountFixSplitDateOrder (Account * acc, Split *split) +{ + if (NULL == acc) return; + if (NULL == split) return; + + if (acc->do_free) return; + + acc->sort_dirty = TRUE; + acc->balance_dirty = TRUE; + + if (acc->editlevel > 0) return; + + xaccAccountBringUpToDate (acc); +} + +/********************************************************************\ + * xaccCheckTransDateOrder * + * check this transaction to see if the date is in correct order * + * If it is not, reorder the transactions. * + * This routine perfroms the check for both of the double-entry * + * transaction entries. * + * * + * Args: trans -- the transaction to check * + * Return: int -- non-zero if out of order * +\********************************************************************/ + +void +xaccTransFixSplitDateOrder (Transaction *trans) +{ + GList *node; + + if (trans == NULL) return; + + for (node = trans->splits; node; node = node->next) + { + Split *s = node->data; + xaccAccountFixSplitDateOrder (xaccSplitGetAccount(s), s); + } +} + +/********************************************************************\ +\********************************************************************/ + +/* The sort order is used to implicitly define an + * order for report generation */ + +static int typeorder[NUM_ACCOUNT_TYPES] = { + BANK, STOCK, MUTUAL, CURRENCY, CASH, ASSET, + CREDIT, LIABILITY, INCOME, EXPENSE, EQUITY }; + +static int revorder[NUM_ACCOUNT_TYPES] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; + + +int +xaccAccountOrder (Account **aa, Account **ab) { + char *da, *db; + char *endptr = NULL; + int ta, tb; + long la, lb; + + if ( (*aa) && !(*ab) ) return -1; + if ( !(*aa) && (*ab) ) return +1; + if ( !(*aa) && !(*ab) ) return 0; + + /* sort on accountCode strings */ + da = (*aa)->accountCode; + db = (*ab)->accountCode; + + /* If accountCodes are both base 36 integers do an integer sort */ + la = strtoul (da, &endptr, 36); + if((*da != '\0') && (*endptr == '\0')) { + lb = strtoul (db, &endptr, 36); + if((*db != '\0') && (*endptr == '\0')) { + if (la < lb) return -1; + if (la > lb) return +1; + } + } + + /* Otherwise do a string sort */ + SAFE_STRCMP (da, db); + + /* if acccount-type-order array not initialized, initialize it */ + /* this will happen at most once during program invocation */ + if (-1 == revorder[0]) { + int i; + for (i=0; itype; + tb = (*ab)->type; + ta = revorder[ta]; + tb = revorder[tb]; + if (ta < tb) return -1; + if (ta > tb) return +1; + + /* otherwise, sort on accountName strings */ + da = (*aa)->accountName; + db = (*ab)->accountName; + SAFE_STRCMP (da, db); + + return 0; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetType (Account *acc, int tip) { + + if (!acc) return; + + xaccAccountBeginEdit(acc); + { + /* refuse invalid account types, and don't bother if not new type. */ + if((NUM_ACCOUNT_TYPES > tip) && (acc->type != tip)) { + acc->type = tip; + acc->balance_dirty = TRUE; /* new type may affect balance computation */ + } + + mark_account (acc); + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +void +xaccAccountSetName (Account *acc, const char *str) { + char * tmp; + + if ((!acc) || (!str)) return; + + xaccAccountBeginEdit(acc); + { + /* make strdup before freeing (just in case str==accountName !!) */ + tmp = g_strdup (str); + g_free (acc->accountName); + acc->accountName = tmp; + + mark_account (acc); + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +void +xaccAccountSetCode (Account *acc, const char *str) { + char * tmp; + if ((!acc) || (!str)) return; + + xaccAccountBeginEdit(acc); + { + /* make strdup before freeing */ + tmp = g_strdup (str); + g_free (acc->accountCode); + acc->accountCode = tmp; + + mark_account (acc); + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +void +xaccAccountSetDescription (Account *acc, const char *str) { + char * tmp; + if ((!acc) || (!str)) return; + + xaccAccountBeginEdit(acc); + { + /* make strdup before freeing (just in case str==description !!) */ + tmp = g_strdup (str); + g_free (acc->description); + acc->description = tmp; + + mark_account (acc); + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +void +xaccAccountSetNotes (Account *acc, const char *str) +{ + if ((!acc) || (!str)) return; + + xaccAccountBeginEdit(acc); + kvp_frame_set_slot_nc(acc->kvp_data, "notes", + kvp_value_new_string(str)); + mark_account (acc); + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +/* FIXME : is this the right way to do this? */ +static void +update_split_commodity(Account * acc) +{ + GList *lp; + + if(!acc) return; + + xaccAccountBeginEdit(acc); + /* iterate over splits */ + for(lp = acc->splits; lp; lp = lp->next) { + Split *s = (Split *) lp->data; + s->amount = gnc_numeric_convert(s->amount, + xaccAccountGetCommoditySCU(acc), + GNC_RND_ROUND); + } + xaccAccountCommitEdit(acc); +} + +/********************************************************************\ +\********************************************************************/ +/* This is an experimental implementation of set commodity. In the + * long haul, it will need to set the one and only commodity field. + * But in the interim phase, we try to guess right ... + */ + +void +xaccAccountSetCommodity (Account * acc, gnc_commodity * com) +{ + if ((!acc) || (!com)) return; + + xaccAccountBeginEdit(acc); + { + acc->commodity = com; + acc->commodity_scu = gnc_commodity_get_fraction(com); + update_split_commodity(acc); + + acc->sort_dirty = TRUE; + acc->balance_dirty = TRUE; + + mark_account (acc); + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +void +xaccAccountSetCommoditySCU (Account *acc, int scu) +{ + if (!acc) return; + + xaccAccountBeginEdit(acc); + { + acc->commodity_scu = scu; + mark_account (acc); + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +int +xaccAccountGetCommoditySCU (Account * acc) { + if (!acc) return 0; + + return acc->commodity_scu; +} + +/********************************************************************\ +\********************************************************************/ +/* below follow the old, deprecated currency/security routines. */ + +void +DxaccAccountSetCurrency (Account * acc, gnc_commodity * currency) { + const char *string; + gnc_commodity *commodity; + + if ((!acc) || (!currency)) return; + + xaccAccountBeginEdit(acc); + string = gnc_commodity_get_unique_name (currency); + kvp_frame_set_slot_nc(acc->kvp_data, "old-currency", + kvp_value_new_string(string)); + mark_account (acc); + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); + + commodity = DxaccAccountGetCurrency (acc); + if (!commodity) + gnc_commodity_table_insert (gnc_engine_commodities (), currency); +} + +void +DxaccAccountSetSecurity (Account *acc, gnc_commodity * security) { + const char *string; + gnc_commodity *commodity; + + if ((!acc) || (!security)) return; + + xaccAccountBeginEdit(acc); + string = gnc_commodity_get_unique_name (security); + kvp_frame_set_slot_nc(acc->kvp_data, "old-security", + kvp_value_new_string(string)); + mark_account (acc); + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); + + commodity = DxaccAccountGetSecurity (acc); + if (!commodity) + gnc_commodity_table_insert (gnc_engine_commodities (), security); +} + +void +DxaccAccountSetCurrencySCU (Account * acc, int scu) { + + if (!acc) return; + + xaccAccountBeginEdit(acc); + kvp_frame_set_slot_nc(acc->kvp_data, "old-currency-scu", + kvp_value_new_gint64(scu)); + mark_account (acc); + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +int +DxaccAccountGetCurrencySCU (Account * acc) { + kvp_value *v; + + if (!acc) return 0; + + v = kvp_frame_get_slot(acc->kvp_data, "old-currency-scu"); + if (v) return kvp_value_get_gint64 (v); + + return 0; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountDeleteOldData (Account *account) +{ + if (!account) return; + + kvp_frame_set_slot_nc (account->kvp_data, "old-currency", NULL); + kvp_frame_set_slot_nc (account->kvp_data, "old-security", NULL); + kvp_frame_set_slot_nc (account->kvp_data, "old-currency-scu", NULL); + kvp_frame_set_slot_nc (account->kvp_data, "old-security-scu", NULL); +} + +/********************************************************************\ +\********************************************************************/ + +AccountGroup * +xaccAccountGetChildren (Account *acc) +{ + if (!acc) return NULL; + if (acc->children == NULL) + xaccAccountInsertSubAccount (acc, NULL); + return (acc->children); +} + +AccountGroup * +xaccAccountGetParent (Account *acc) +{ + if (!acc) return NULL; + return (acc->parent); +} + +Account * +xaccAccountGetParentAccount (Account * acc) +{ + if (!acc) return NULL; + return xaccGroupGetParentAccount(acc->parent); +} + +GNCAccountType +xaccAccountGetType (Account *acc) +{ + if (!acc) return NO_TYPE; + return (acc->type); +} + +const char * +xaccAccountGetName (Account *acc) +{ + if (!acc) return NULL; + return (acc->accountName); +} + +char * +xaccAccountGetFullName(Account *account, const char separator) +{ + Account *a; + char *fullname; + const char *name; + char *p; + int length; + + if (account == NULL) + return g_strdup(""); + + /* Figure out how much space is needed */ + length = 0; + a = account; + while (a != NULL) + { + name = xaccAccountGetName(a); + + length += strlen(name) + 1; /* plus one for the separator */ + + a = xaccAccountGetParentAccount(a); + } + + /* length has one extra separator in it, that's ok, because it will + * hold the null character at the end. */ + + /* allocate the memory */ + fullname = g_new(char, length); + + /* go to end of string */ + p = fullname + length - 1; + + /* put in the null character and move to the previous char */ + *p-- = 0; + + a = account; + while (a != NULL) + { + name = xaccAccountGetName(a); + length = strlen(name); + + /* copy the characters going backwards */ + while (length > 0) + *p-- = name[--length]; + + a = xaccAccountGetParentAccount(a); + + /* if we're not at the root, add another separator */ + if (a != NULL) + *p-- = separator; + } + + return fullname; +} + +const char * +xaccAccountGetCode (Account *acc) +{ + if (!acc) return NULL; + return (acc->accountCode); +} + +const char * +xaccAccountGetDescription (Account *acc) +{ + if (!acc) return NULL; + return (acc->description); +} + +const char * +xaccAccountGetNotes (Account *acc) +{ + kvp_value *v; + + if (!acc) return NULL; + v = kvp_frame_get_slot(acc->kvp_data, "notes"); + if(v) return(kvp_value_get_string(v)); + return(NULL); +} + +gnc_commodity * +DxaccAccountGetCurrency (Account *acc) +{ + kvp_value *v; + const char *s; + + if (!acc) return NULL; + + v = kvp_frame_get_slot(acc->kvp_data, "old-currency"); + if (!v) return NULL; + + s = kvp_value_get_string (v); + if (!s) return NULL; + + return gnc_commodity_table_lookup_unique (gnc_engine_commodities (), s); +} + +gnc_commodity * +xaccAccountGetCommodity (Account *acc) +{ + if (!acc) return NULL; + + return (acc->commodity); +} + +gnc_commodity * +DxaccAccountGetSecurity (Account *acc) +{ + kvp_value *v; + const char *s; + + if (!acc) return NULL; + + v = kvp_frame_get_slot(acc->kvp_data, "old-security"); + if (!v) return NULL; + + s = kvp_value_get_string (v); + if (!s) return NULL; + + return gnc_commodity_table_lookup_unique (gnc_engine_commodities (), s); +} + +gnc_numeric +xaccAccountGetBalance (Account *acc) { + if (!acc) return gnc_numeric_zero(); + return acc->balance; +} + +gnc_numeric +xaccAccountGetClearedBalance (Account *acc) +{ + if (!acc) return gnc_numeric_zero(); + return acc->cleared_balance; +} + +gnc_numeric +xaccAccountGetReconciledBalance (Account *acc) +{ + if (!acc) return gnc_numeric_zero(); + return acc->reconciled_balance; +} + +/********************************************************************\ +\********************************************************************/ + +gnc_numeric +xaccAccountGetBalanceAsOfDate (Account *acc, time_t date) +{ + /* Ideally this could use xaccAccountForEachSplit, but + * it doesn't exist yet and I'm uncertain of exactly how + * it would work at this time, since it differs from + * xaccAccountForEachTransaction by using gpointer return + * values rather than gbooleans. + */ + GList *lp; + Timespec ts, trans_ts; + gboolean found = FALSE; + gnc_numeric balance; + + balance = xaccAccountGetBalance( acc ); + + xaccAccountSortSplits( acc ); /* just in case, normally a nop */ + + /* Since transaction post times are stored as a Timespec, + * convert date into a Timespec as well rather than converting + * each transaction's Timespec into a time_t. + */ + + ts.tv_sec = date; + ts.tv_nsec = 0; + + /* Do checks from xaccAccountRecomputeBalance. balance_dirty isn't + * checked because it shouldn't be necessary. + */ + + if( NULL == acc || + acc->editlevel > 0 || + acc->do_free ) + { + return ( balance ); + } + + lp = xaccAccountGetSplitList( acc ); + while( lp && !found ) + { + xaccTransGetDatePostedTS( xaccSplitGetParent( (Split *)lp->data ), + &trans_ts ); + if( timespec_cmp( &trans_ts, &ts ) > 0 ) + found = TRUE; + else + lp = lp->next; + } + + if( lp && lp->prev ) + { + /* Since lp is now pointing to a split which was past the reconcile + * date, get the running balance of the previous split. + */ + balance = xaccSplitGetBalance( (Split *)lp->prev->data ); + } + + /* Otherwise there were no splits posted after the given date, + * so the latest account balance should be good enough. + */ + + return( balance ); +} + +/********************************************************************\ +\********************************************************************/ + +GList * +xaccAccountGetSplitList (Account *acc) { + if (!acc) return NULL; + return (acc->splits); +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccAccountGetTaxRelated (Account *account) +{ + kvp_value *kvp; + + if (!account) + return FALSE; + + kvp = kvp_frame_get_slot (account->kvp_data, "tax-related"); + if (!kvp) + return FALSE; + + return kvp_value_get_gint64 (kvp); +} + +void +xaccAccountSetTaxRelated (Account *account, gboolean tax_related) +{ + kvp_value *new_value; + + if (!account) + return; + + if (tax_related) + new_value = kvp_value_new_gint64 (tax_related); + else + new_value = NULL; + + xaccAccountBeginEdit (account); + kvp_frame_set_slot_nc(account->kvp_data, "tax-related", new_value); + + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +const char * +xaccAccountGetTaxUSCode (Account *account) +{ + kvp_value *value; + + if (!account) + return FALSE; + + value = kvp_frame_get_slot_path (account->kvp_data, "tax-US", "code", NULL); + if (!value) + return NULL; + + return kvp_value_get_string (value); +} + +void +xaccAccountSetTaxUSCode (Account *account, const char *code) +{ + kvp_frame *frame; + + if (!account) + return; + + xaccAccountBeginEdit (account); + + frame = kvp_frame_get_frame (account->kvp_data, "tax-US", NULL); + + kvp_frame_set_slot_nc (frame, "code", + code ? kvp_value_new_string (code) : NULL); + + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +const char * +xaccAccountGetTaxUSPayerNameSource (Account *account) +{ + kvp_value *value; + + if (!account) + return FALSE; + + value = kvp_frame_get_slot_path (account->kvp_data, + "tax-US", "payer-name-source", NULL); + if (!value) + return NULL; + + return kvp_value_get_string (value); +} + +void +xaccAccountSetTaxUSPayerNameSource (Account *account, const char *source) +{ + kvp_frame *frame; + + if (!account) + return; + + xaccAccountBeginEdit (account); + + frame = kvp_frame_get_frame (account->kvp_data, "tax-US", NULL); + + kvp_frame_set_slot_nc (frame, "payer-name-source", + source ? kvp_value_new_string (source) : NULL); + + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccAccountHasAncestor (Account *account, Account * ancestor) +{ + Account *parent; + + if ((account == NULL) || (ancestor == NULL)) + return FALSE; + + parent = xaccAccountGetParentAccount(account); + while (parent != NULL) + { + if (parent == ancestor) + return TRUE; + + parent = xaccAccountGetParentAccount(parent); + } + + return FALSE; +} + +/********************************************************************\ +\********************************************************************/ + +/* You must edit the functions in this block in tandem. KEEP THEM IN + SYNC! */ + +#define GNC_RETURN_ENUM_AS_STRING(x) case (x): return #x; + +char * +xaccAccountTypeEnumAsString(GNCAccountType type) { + switch(type) { + GNC_RETURN_ENUM_AS_STRING(NO_TYPE); + GNC_RETURN_ENUM_AS_STRING(BANK); + GNC_RETURN_ENUM_AS_STRING(CASH); + GNC_RETURN_ENUM_AS_STRING(CREDIT); + GNC_RETURN_ENUM_AS_STRING(ASSET); + GNC_RETURN_ENUM_AS_STRING(LIABILITY); + GNC_RETURN_ENUM_AS_STRING(STOCK); + GNC_RETURN_ENUM_AS_STRING(MUTUAL); + GNC_RETURN_ENUM_AS_STRING(CURRENCY); + GNC_RETURN_ENUM_AS_STRING(INCOME); + GNC_RETURN_ENUM_AS_STRING(EXPENSE); + GNC_RETURN_ENUM_AS_STRING(EQUITY); + GNC_RETURN_ENUM_AS_STRING(CHECKING); + GNC_RETURN_ENUM_AS_STRING(SAVINGS); + GNC_RETURN_ENUM_AS_STRING(MONEYMRKT); + GNC_RETURN_ENUM_AS_STRING(CREDITLINE); + default: + PERR ("asked to translate unknown account type %d.\n", type); + break; + } + return(NULL); +} + +#undef GNC_RETURN_ENUM_AS_STRING + +#define GNC_RETURN_ON_MATCH(x) \ + if(safe_strcmp(#x, (str)) == 0) { *type = x; return(TRUE); } + +gboolean +xaccAccountStringToType(const char* str, GNCAccountType *type) { + + GNC_RETURN_ON_MATCH(NO_TYPE); + GNC_RETURN_ON_MATCH(BANK); + GNC_RETURN_ON_MATCH(CASH); + GNC_RETURN_ON_MATCH(CREDIT); + GNC_RETURN_ON_MATCH(ASSET); + GNC_RETURN_ON_MATCH(LIABILITY); + GNC_RETURN_ON_MATCH(STOCK); + GNC_RETURN_ON_MATCH(MUTUAL); + GNC_RETURN_ON_MATCH(CURRENCY); + GNC_RETURN_ON_MATCH(INCOME); + GNC_RETURN_ON_MATCH(EXPENSE); + GNC_RETURN_ON_MATCH(EQUITY); + GNC_RETURN_ON_MATCH(CHECKING); + GNC_RETURN_ON_MATCH(SAVINGS); + GNC_RETURN_ON_MATCH(MONEYMRKT); + GNC_RETURN_ON_MATCH(CREDITLINE); + + PERR("asked to translate unknown account type string %s.\n", + str ? str : "(null)"); + + return(FALSE); +} + +#undef GNC_RETURN_ON_MATCH + +/* impedance mismatch is a source of loss */ +GNCAccountType +xaccAccountStringToEnum(const char* str) +{ + GNCAccountType type; + gboolean rc; + rc = xaccAccountStringToType(str, &type); + if (FALSE == rc) return BAD_TYPE; + return type; +} + +/********************************************************************\ +\********************************************************************/ + +static char * +account_type_name[NUM_ACCOUNT_TYPES] = { + N_("Bank"), + N_("Cash"), + N_("Asset"), + N_("Credit Card"), + N_("Liability"), + N_("Stock"), + N_("Mutual Fund"), + N_("Currency"), + N_("Income"), + N_("Expense"), + N_("Equity") + /* + N_("Checking"), + N_("Savings"), + N_("Money Market"), + N_("Credit Line") + */ +}; + +const char * +xaccAccountGetTypeStr(GNCAccountType type) { + if (0 > type) return ""; + if (NUM_ACCOUNT_TYPES <= type) return ""; + return _(account_type_name [type]); +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccAccountTypesCompatible (GNCAccountType parent_type, + GNCAccountType child_type) +{ + gboolean compatible = FALSE; + + switch(parent_type) + { + case BANK: + case CASH: + case ASSET: + case STOCK: + case MUTUAL: + case CURRENCY: + case CREDIT: + case LIABILITY: + compatible = ((child_type == BANK) || + (child_type == CASH) || + (child_type == ASSET) || + (child_type == STOCK) || + (child_type == MUTUAL) || + (child_type == CURRENCY) || + (child_type == CREDIT) || + (child_type == LIABILITY)); + break; + case INCOME: + case EXPENSE: + compatible = ((child_type == INCOME) || + (child_type == EXPENSE)); + break; + case EQUITY: + compatible = (child_type == EQUITY); + break; + default: + PERR("bad account type: %d", parent_type); + break; + } + + return compatible; +} + +/********************************************************************\ +\********************************************************************/ +gboolean +xaccAccountGetReconcileLastDate (Account *account, time_t *last_date) +{ + kvp_value *value; + + if (!account) + return FALSE; + + value = kvp_frame_get_slot_path (account->kvp_data, + "reconcile-info", "last-date", NULL); + if (!value) + return FALSE; + + if (kvp_value_get_type (value) == KVP_TYPE_GINT64) + { + if (last_date) + *last_date = kvp_value_get_gint64 (value); + + return TRUE; + } + + return FALSE; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetReconcileLastDate (Account *account, time_t last_date) +{ + kvp_frame *frame; + if (!account) + return; + + xaccAccountBeginEdit (account); + frame = kvp_frame_get_frame (account->kvp_data, "reconcile-info", NULL); + kvp_frame_set_slot_nc (frame, "last-date", + kvp_value_new_gint64 (last_date)); + + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccAccountGetReconcilePostponeDate (Account *account, + time_t *postpone_date) +{ + kvp_value *value; + + if (!account) + return FALSE; + + value = kvp_frame_get_slot_path (account->kvp_data, + "reconcile-info", "postpone", "date", NULL); + if (!value) + return FALSE; + + if (kvp_value_get_type (value) == KVP_TYPE_GINT64) + { + if (postpone_date) + *postpone_date = kvp_value_get_gint64 (value); + + return TRUE; + } + + return FALSE; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetReconcilePostponeDate (Account *account, + time_t postpone_date) +{ + kvp_frame *frame; + if (!account) + return; + + xaccAccountBeginEdit (account); + frame = kvp_frame_get_frame (account->kvp_data, + "reconcile-info", "postpone", NULL); + + kvp_frame_set_slot_nc (frame, "date", + kvp_value_new_gint64 (postpone_date)); + + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccAccountGetReconcilePostponeBalance (Account *account, + gnc_numeric *balance) +{ + kvp_value *value; + + if (!account) + return FALSE; + + value = kvp_frame_get_slot_path (account->kvp_data, + "reconcile-info", "postpone", "balance", + NULL); + if (!value) + return FALSE; + + if (kvp_value_get_type (value) == KVP_TYPE_NUMERIC) + { + if (balance) + *balance = kvp_value_get_numeric (value); + + return TRUE; + } + + return FALSE; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetReconcilePostponeBalance (Account *account, + gnc_numeric balance) +{ + kvp_frame *frame; + if (!account) + return; + + xaccAccountBeginEdit (account); + frame = kvp_frame_get_frame (account->kvp_data, + "reconcile-info", "postpone", NULL); + + kvp_frame_set_slot_nc (frame, "balance", + kvp_value_new_gnc_numeric (balance)); + + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ + +\********************************************************************/ + +void +xaccAccountClearReconcilePostpone (Account *account) +{ + if (!account) + return; + + xaccAccountBeginEdit (account); + { + kvp_frame_set_slot_path (account->kvp_data, NULL, + "reconcile-info", "postpone", NULL); + + mark_account (account); + } + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ +\********************************************************************/ + +/* xaccAccountGetAutoInterestXfer: determine whether the auto interest + * xfer option is enabled for this account, and return that value. + * If it is not defined for the account, return the default value. + */ +gboolean +xaccAccountGetAutoInterestXfer (Account *account, gboolean default_value) +{ + kvp_value *value = NULL; + char *setting = NULL; + gboolean result = default_value; + + if ( ( account ) && + ( value = kvp_frame_get_slot_path (account->kvp_data, + "reconcile-info", + "auto-interest-transfer", + NULL) ) && + ( kvp_value_get_type (value) == KVP_TYPE_STRING ) && + ( setting = kvp_value_get_string(value) ) ) + { + if( !strcmp( setting, "true" ) ) + result = TRUE; + else if( !strcmp( setting, "false" ) ) + result = FALSE; + } + + return (result); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetAutoInterestXfer (Account *account, gboolean option) +{ + kvp_frame *frame; + if (!account) + return; + + xaccAccountBeginEdit (account); + frame = kvp_frame_get_frame (account->kvp_data, + "reconcile-info", NULL); + + /* FIXME: need KVP_TYPE_BOOLEAN for this someday */ + + kvp_frame_set_slot_nc (frame, "auto-interest-transfer", + kvp_value_new_string (option ? "true" : "false")); + + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ +\********************************************************************/ + +const char * +xaccAccountGetLastNum (Account *account) +{ + kvp_value *value; + + if (!account) + return FALSE; + + value = kvp_frame_get_slot (account->kvp_data, "last-num"); + if (!value) + return FALSE; + + return kvp_value_get_string (value); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetLastNum (Account *account, const char *num) +{ + if (!account) + return; + + xaccAccountBeginEdit (account); + kvp_frame_set_slot_nc (account->kvp_data, "last-num", + kvp_value_new_string (num)); + mark_account (account); + account->core_dirty = TRUE; + xaccAccountCommitEdit (account); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetPriceSrc(Account *acc, const char *src) +{ + if(!acc) return; + + xaccAccountBeginEdit(acc); + { + GNCAccountType t = xaccAccountGetType(acc); + + if((t == STOCK) || (t == MUTUAL) || (t == CURRENCY)) { + kvp_frame_set_slot_nc(acc->kvp_data, + "old-price-source", + src ? kvp_value_new_string(src) : NULL); + mark_account (acc); + } + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +/********************************************************************\ +\********************************************************************/ + +const char* +xaccAccountGetPriceSrc(Account *acc) +{ + GNCAccountType t; + if(!acc) return NULL; + + t = xaccAccountGetType(acc); + if((t == STOCK) || (t == MUTUAL) || (t == CURRENCY)) + { + kvp_value *value = kvp_frame_get_slot(acc->kvp_data, "old-price-source"); + if(value) return (kvp_value_get_string(value)); + } + return NULL; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountSetQuoteTZ(Account *acc, const char *tz) +{ + if(!acc) return; + if(!tz) return; + + xaccAccountBeginEdit(acc); + { + GNCAccountType t = xaccAccountGetType(acc); + + if((t == STOCK) || (t == MUTUAL) || (t == CURRENCY)) { + kvp_frame_set_slot_nc(acc->kvp_data, + "old-quote-tz", + kvp_value_new_string(tz)); + mark_account (acc); + } + } + acc->core_dirty = TRUE; + xaccAccountCommitEdit(acc); +} + +/********************************************************************\ +\********************************************************************/ + +const char* +xaccAccountGetQuoteTZ(Account *acc) +{ + GNCAccountType t; + if(!acc) return NULL; + + t = xaccAccountGetType(acc); + if((t == STOCK) || (t == MUTUAL) || (t == CURRENCY)) + { + kvp_value *value = kvp_frame_get_slot(acc->kvp_data, "old-quote-tz"); + if(value) return (kvp_value_get_string(value)); + } + return NULL; +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccAccountVisitUnvisitedTransactions(Account *acc, + gboolean (*proc)(Transaction *t, + void *data), + void *data, + GHashTable *visited_txns) { + gboolean keep_going = TRUE; + GList *lp; + + if(!acc) return(FALSE); + if(!proc) return(FALSE); + if(!visited_txns) return(FALSE); + + for(lp = xaccAccountGetSplitList(acc); lp && keep_going; lp = lp->next) { + Split *s = (Split *) lp->data; + Transaction *t = xaccSplitGetParent(s); + + if(t) { + const GUID *guid = xaccTransGetGUID(t); + gpointer been_here = g_hash_table_lookup(visited_txns, guid); + + if(!GPOINTER_TO_INT(been_here)) { + g_hash_table_insert(visited_txns, (gpointer) guid, + GINT_TO_POINTER(TRUE)); + if(!proc(t, data)) { + keep_going = FALSE; + } + } + } + } + return(keep_going); +} + +gboolean +xaccAccountForEachTransaction(Account *acc, + gboolean (*proc)(Transaction *t, void *data), + void *data) { + GHashTable *visited_txns = NULL; + gboolean result = FALSE; + + if(!acc) return(FALSE); + if(!proc) return(FALSE); + + visited_txns = guid_hash_table_new(); + if(visited_txns) { + result = + xaccAccountVisitUnvisitedTransactions(acc, proc, data, visited_txns); + } + + /* cleanup */ + if(visited_txns) g_hash_table_destroy(visited_txns); + return(result); +} + +/********************************************************************\ +\********************************************************************/ + +/* The caller of this function can get back one or both of the + * matching split and transaction pointers, depending on whether + * a valid pointer to the location to store those pointers is + * passed. + */ +static void +finder_help_function(Account *account, + const char *description, + Split **split, + Transaction **trans ) +{ + GList *slp; + + if (account == NULL) return; + + for (slp = g_list_last (xaccAccountGetSplitList (account)); + slp; + slp = slp->prev) + { + Split *lsplit = slp->data; + Transaction *ltrans = xaccSplitGetParent(lsplit); + + if (safe_strcmp (description, xaccTransGetDescription (ltrans)) == 0) + { + if( split ) *split = lsplit; + if( trans ) *trans = ltrans; + return; + } + } + + if( split ) *split = NULL; + if( trans ) *trans = NULL; +} + +Split * +xaccAccountFindSplitByDesc(Account *account, const char *description) +{ + Split *split; + + /* Get the split which has a transaction matching the description. */ + finder_help_function(account, description, &split, NULL ); + + return( split ); +} + +/* This routine is for finding a matching transaction in an account by + * matching on the description field. This routine is used for auto-filling + * in registers with a default leading account. The dest_trans is a + * transaction used for currency checking. */ +Transaction * +xaccAccountFindTransByDesc(Account *account, const char *description) +{ + Transaction *trans; + + /* Get the transation matching the description. */ + finder_help_function(account, description, NULL, &trans ); + + return( trans ); +} + +/********************************************************************\ +\********************************************************************/ diff --git a/src/engine/Account.h b/src/engine/Account.h new file mode 100644 index 0000000000..382c468b0c --- /dev/null +++ b/src/engine/Account.h @@ -0,0 +1,375 @@ +/********************************************************************\ + * Account.h -- Account handling public routines * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997-2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef XACC_ACCOUNT_H +#define XACC_ACCOUNT_H + +#include "config.h" + +#include "GNCId.h" +#include "Transaction.h" +#include "kvp_frame.h" + + +/** PROTOTYPES ******************************************************/ + +/* + * The account types are used to determine how the transaction data + * in the account is displayed. These values can be safely changed + * from one release to the next. Note that if values are added, + * the file IO translation routines need to be updated. Note + * also that GUI code depends on these numbers. + * + * ***IMPORTANT***: If you do change the enumeration names (not the + * numbers), you need to update xaccAccountTypeEnumAsString --- used + * for text file exports */ + +typedef enum +{ + BAD_TYPE = -1, + NO_TYPE = -1, + /* Not a type */ + + BANK = 0, + /* The bank account type denotes a savings or checking account + * held at a bank. Often interest bearing. + */ + + CASH = 1, + /* The cash account type is used to denote a shoe-box or pillowcase + * stuffed with cash. + */ + + CREDIT = 3, + /* The Credit card account is used to denote credit (e.g. amex) and + * debit (e.g. visa, mastercard) card accounts + */ + + ASSET = 2, + LIABILITY = 4, + /* asset and liability accounts indicate generic, generalized accounts + * that are none of the above. + */ + + STOCK = 5, + MUTUAL= 6, + /* Stock and Mutual Fund accounts will typically be shown in registers + * which show three columns: price, number of shares, and value. + */ + + CURRENCY = 7, + /* The currency account type indicates that the account is a + * currency trading account. In many ways, a currency trading + * account is like a stock trading account, where both values + * and share quantities are set. + */ + + INCOME = 8, + EXPENSE = 9, + /* Income and expense accounts are used to denote income and expenses. */ + + EQUITY = 10, + /* Equity account is used to balance the balance sheet. */ + + NUM_ACCOUNT_TYPES = 11, + /* stop here; the following types just aren't ready for prime time */ + + /* bank account types */ + CHECKING = 11, + SAVINGS = 12, + MONEYMRKT = 13, + CREDITLINE = 14, /* line of credit */ +} GNCAccountType; + +const char * xaccAccountGetTypeStr (GNCAccountType type); /* GUI names */ + +/* Conversion routines for the account types to/from strings. + * Critical for the text communication mechanisms. i.e. INCOME -> + * "INCOME". */ +char * xaccAccountTypeEnumAsString (GNCAccountType type); +gboolean xaccAccountStringToType (const char* str, GNCAccountType *type); +GNCAccountType xaccAccountStringToEnum (const char* str); + +/* Return TRUE if accounts of type parent_type can have accounts + * of type child_type as children. */ +gboolean xaccAccountTypesCompatible (GNCAccountType parent_type, + GNCAccountType child_type); + +/* Compare two accounts for equality - this is a deep compare. */ +gboolean xaccAccountEqual(Account *a, Account* b, gboolean check_guids); + +/* + * The xaccAccountBeginEdit() and xaccAccountCommitEdit() subroutines + * provide a two-phase-commit wrapper for account updates. + * They are incompletely implemented. + * + * The xaccAccountDestroy() routine can be used to get rid of an + * account. The account should have been opened for editing + * (by calling xaccAccountBeginEdit()) before calling this routine. + */ +Account * xaccMallocAccount (void); +Account * xaccCloneAccountSimple(const Account *from); +void xaccAccountBeginEdit (Account *account); +void xaccAccountCommitEdit (Account *account); +void xaccAccountDestroy (Account *account); + +kvp_frame * xaccAccountGetSlots (Account *account); +void xaccAccountSetSlots_nc(Account *account, kvp_frame *frame); + +/* + * The xaccAccountGetGUID() subroutine will return the + * globally unique id associated with that account. + * + * The xaccAccountLookup() subroutine will return the + * account associated with the given id, or NULL + * if there is no such account. + */ +const GUID * xaccAccountGetGUID (Account *account); +Account * xaccAccountLookup (const GUID *guid); + +/* + * The xaccAccountInsertSplit() method will insert the indicated + * split into the indicated account. If the split already + * belongs to another account, it will be removed from that + * account first. + */ +void xaccAccountInsertSplit (Account *account, Split *split); + +/* The xaccAccountFixSplitDateOrder() subroutine checks to see if + * a split is in proper sorted date order with respect + * to the other splits in this account. + * + * The xaccTransFixSplitDateOrder() checks to see if + * all of the splits in this transaction are in + * proper date order. + */ +void xaccAccountFixSplitDateOrder (Account *account, Split *split); +void xaccTransFixSplitDateOrder (Transaction *trans); + +/* The xaccAccountOrder() subroutine defines a sorting order + * on accounts. It takes pointers to two accounts, and + * returns -1 if the first account is "less than" the second, + * returns +1 if the first is "greater than" the second, and + * 0 if they are equal. To determine the sort order, first + * the account codes are compared, and if these are equal, then + * account types, and, if these are equal, the account names. + */ +int xaccAccountOrder (Account **account_1, Account **account_2); + +void xaccAccountSetType (Account *account, int); +void xaccAccountSetName (Account *account, const char *name); +void xaccAccountSetCode (Account *account, const char *code); +void xaccAccountSetDescription (Account *account, const char *desc); +void xaccAccountSetNotes (Account *account, const char *notes); + +GNCAccountType xaccAccountGetType (Account *account); +const char * xaccAccountGetName (Account *account); +const char * xaccAccountGetCode (Account *account); +const char * xaccAccountGetDescription (Account *account); +const char * xaccAccountGetNotes (Account *account); + +/* New commodity access routines. + * + * The account structure no longer stores two commodities ('currency' + * and 'security'). Instead it stores only one commodity, that is the + * one formerly known as 'security'. Use xaccAccountSetCommodity() + * and xaccAccountGetCommodity() to set and fetch it. + * + * Basically, the engine eliminates the 'currency' field of the + * Account structure. Instead, the common currency is stored with the + * transaction. The 'value' of a split is a translation of the + * Split's 'amount' (which is the amount of the Account's commodity + * involved) into the Transaction's balancing currency. */ +void xaccAccountSetCommodity (Account *account, gnc_commodity *comm); +gnc_commodity * xaccAccountGetCommodity (Account *account); +int xaccAccountGetCommoditySCU (Account *account); +void xaccAccountSetCommoditySCU (Account *account, int frac); + +/* Deprecated currency/security access routines. + * The current API associates only one thing with an account: + * the 'commodity'. Use xaccAccountGetCommodity() to fetch it. + */ +/* these two funcs take control of their gnc_commodity args. Don't free */ +void DxaccAccountSetCurrency (Account *account, gnc_commodity *currency); +void DxaccAccountSetSecurity (Account *account, gnc_commodity *security); +gnc_commodity * DxaccAccountGetCurrency (Account *account); +gnc_commodity * DxaccAccountGetSecurity (Account *account); +void DxaccAccountSetCurrencySCU (Account *account, int frac); +int DxaccAccountGetCurrencySCU (Account *account); + +/* Delete any old data in the account's kvp data. + * This includes the old currency and security fields. */ +void xaccAccountDeleteOldData (Account *account); + +AccountGroup * xaccAccountGetChildren (Account *account); +AccountGroup * xaccAccountGetParent (Account *account); +Account * xaccAccountGetParentAccount (Account *account); + +gnc_numeric xaccAccountGetBalance (Account *account); +gnc_numeric xaccAccountGetClearedBalance (Account *account); +gnc_numeric xaccAccountGetReconciledBalance (Account *account); + +gnc_numeric xaccAccountGetBalanceAsOfDate (Account *account, time_t date); + +GList* xaccAccountGetSplitList (Account *account); + +gboolean xaccAccountGetTaxRelated (Account *account); +void xaccAccountSetTaxRelated (Account *account, + gboolean tax_related); + +const char * xaccAccountGetTaxUSCode (Account *account); +void xaccAccountSetTaxUSCode (Account *account, const char *code); +const char * xaccAccountGetTaxUSPayerNameSource (Account *account); +void xaccAccountSetTaxUSPayerNameSource (Account *account, + const char *source); + +/* The xaccAccountGetFullName routine returns the fully qualified name + * of the account using the given separator char. The name must be freed + * after use. The fully qualified name of an account is the concatenation + * of the names of the account and all its ancestor accounts starting with + * the topmost account and ending with the given account. Each name is + * separated by the given character. + * + * WAKE UP! + * Unlike all other gets, the string returned by xaccAccountGetFullName() + * must be freed by you the user !!! + * hack alert -- since it breaks the rule of string allocation, maybe this + * routine should not be in this library, but some utility library? + */ +char * xaccAccountGetFullName (Account *account, const char separator); + +/* Returns true if the account has 'ancestor' as an ancestor. + * Returns false if either is NULL. */ +gboolean xaccAccountHasAncestor (Account *account, Account *ancestor); + +/* Get and Set a mark on the account. The meaning of this mark is + * completely undefined. Its presented here as a utility for the + * programmer, to use as desired. Handy for performing customer traversals + * over the account tree. The mark is *not* stored in the database/file + * format. When accounts are newly created, the mark is set to zero. + * + * The xaccClearMark will find the topmost group, and clear the mark in + * the entire group tree. + * The xaccClearMarkDown will clear the mark only in this and in + * sub-accounts. + */ +short xaccAccountGetMark (Account *account); +void xaccAccountSetMark (Account *account, short mark); +void xaccClearMark (Account *account, short val); +void xaccClearMarkDown (Account *account, short val); +void xaccClearMarkDownGr (AccountGroup *group, short val); + +/* The following functions get and set reconciliation information */ +gboolean xaccAccountGetReconcileLastDate (Account *account, + time_t *last_date); +void xaccAccountSetReconcileLastDate (Account *account, + time_t last_date); + +gboolean xaccAccountGetReconcilePostponeDate (Account *account, + time_t *postpone_date); +void xaccAccountSetReconcilePostponeDate (Account *account, + time_t postpone_date); + +gboolean xaccAccountGetReconcilePostponeBalance (Account *account, + gnc_numeric *balance); +void xaccAccountSetReconcilePostponeBalance (Account *account, + gnc_numeric balance); + +void xaccAccountClearReconcilePostpone (Account *account); + +gboolean xaccAccountGetAutoInterestXfer (Account *account, gboolean default_value); +void xaccAccountSetAutoInterestXfer (Account *account, gboolean option); + +/* Get and set the last num field of an Account */ +const char * xaccAccountGetLastNum (Account *account); +void xaccAccountSetLastNum (Account *account, const char *num); + +/* The xaccAccountSetPriceSrc() and xaccAccountGetPriceSrc() routines + are used to get and set a string that identifies the Finance::Quote + backend that should be used to retrieve online prices. See + price-quotes.scm for more information. + + xaccAccountGetQuoteTZ() and xaccAccountSetQuoteTZ() set the + timezone to be used when interpreting the results from a given + Finance::Quote backend. Unfortunately, the upstream sources don't + label their output, so the user has to specify this bit. + + Since prices are not going to be stored in the accounts in the + future, and since the whole commodities infrastructure is changing + radically as we speak, this interface is not long for this + world. */ + +void xaccAccountSetPriceSrc (Account *account, const char *src); +const char * xaccAccountGetPriceSrc (Account *account); + +void xaccAccountSetQuoteTZ (Account *account, const char *tz); +const char * xaccAccountGetQuoteTZ (Account *account); + + +typedef gpointer (*SplitCallback)(Split *s, gpointer data); +gpointer xaccAccountForEachSplit(Account *account, + SplitCallback, + gpointer data); + +/* Traverse all of the transactions in the given account. Continue + processing IFF proc does not return FALSE. This function does not + descend recursively to traverse transactions in child accounts. + + Proc will be called exactly once for each transaction that is + pointed to by at least one split in the given account. + + Note too, that if you call this function on two separate accounts + and those accounts share transactions, proc will be called once per + account for the shared transactions. + + The result of this function will not be FALSE IFF every relevant + transaction was traversed exactly once. */ +typedef gboolean (*TransactionCallback)(Transaction *t, void *data); +gboolean +xaccAccountForEachTransaction(Account *account, + TransactionCallback, + void *data); + +/* Visit every transaction in the account that hasn't already been + visited exactly once. visited_txns must be a hash table created + via guid_hash_table_new() and is the authority about which + transactions have already been visited. Further, when this + procedure returns visited_txns will have been modified to reflect + all the newly visited transactions. + + The result of this function will not be FALSE IFF every relevant + transaction was traversed exactly once. */ +gboolean +xaccAccountVisitUnvisitedTransactions(Account *account, + TransactionCallback, + void *data, + GHashTable *visited_txns); + +/* Returns a pointer to the transaction, not a copy. */ +Transaction * +xaccAccountFindTransByDesc(Account *account, const char *description); +Split * +xaccAccountFindSplitByDesc(Account *account, const char *description); + +#endif /* XACC_ACCOUNT_H */ diff --git a/src/engine/AccountP.h b/src/engine/AccountP.h new file mode 100644 index 0000000000..66d0501460 --- /dev/null +++ b/src/engine/AccountP.h @@ -0,0 +1,207 @@ +/********************************************************************\ + * AccountP.h -- Account engine-private data structure * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997-2000, Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * AccountP.h + * + * FUNCTION: + * This is the *private* header for the account structure. + * No one outside of the engine should ever include this file. + * + * This header includes prototypes for "dangerous" functions. + * Invoking any of these functions potentially leave the account + * in an inconsistent state. If they are not used in the proper + * setting, they can leave the account structures in an inconsistent + * state. Thus, these methods should never be used outside of + * the engine, which is why they are "hidden" here. + * + */ + +#ifndef XACC_ACCOUNT_P_H +#define XACC_ACCOUNT_P_H + +#include "config.h" + +#include "Account.h" +#include "GNCId.h" +#include "Transaction.h" +#include "gnc-commodity.h" +#include "gnc-numeric.h" +#include "kvp_frame.h" + + +/** STRUCTS *********************************************************/ +struct _account { + /* public data, describes account */ + GUID guid; /* globally unique account id */ + + /* The accountName is an arbitrary string assigned by the user. + * It is intended to a short, 5 to 30 character long string that + * is displayed by the GUI as the account mnemonic. + */ + char *accountName; + + /* The accountCode is an arbitrary string assigned by the user. + * It is intended to be reporting code that is a synonym for the + * accountName. Typically, it will be a numeric value that follows + * the numbering assignments commonly used by accountants, such + * as 100, 200 or 600 for top-level * accounts, and 101, 102.. etc. + * for detail accounts. + */ + char *accountCode; + + /* The description is an arbitrary string assigned by the user. + * It is intended to be a longer, 1-5 sentence description of what + * this account is all about. + */ + char *description; + + /* kvp_data is a key-value pair database for storing simple "extra" + * information in splits, transactions, and accounts. it's NULL + * until accessed. See ??? for a list and description of the + * important keys. */ + kvp_frame * kvp_data; + + /* The type field is the account type, picked from the enumerated + * list that includes BANK, STOCK, CREDIT, INCOME, etc. Its + * intended use is to be a hint to the GUI as to how to display + * and format the transaction data. + */ + GNCAccountType type; + + /* Old semantics: The currency field denotes the default currency in + * which all splits in this account are denominated. Currency + * trading accounts allow splits between accounts when the currency + * string matches the security string. + * + * The gnc_commodity type represents the namespace, full name, and + * symbol for the currency. + * + * New semantics: The account structure will no longer store a + * 'currency' and a 'security'. Instead it will store only one + * commodity (i.e. currency), that is the one formerly known as + * 'security'. The 'amount' of each split represents the + * transferred amount in the account's commodity (formerly known as + * security). + */ + gnc_commodity * commodity; + int commodity_scu; + + /* The parent and children pointers are used to implement an account + * hierarchy, of accounts that have sub-accounts ("detail accounts"). + */ + AccountGroup *parent; /* back-pointer to parent */ + AccountGroup *children; /* pointer to sub-accounts */ + + /* protected data, cached parameters */ + gnc_numeric starting_balance; + gnc_numeric starting_cleared_balance; + gnc_numeric starting_reconciled_balance; + + gnc_numeric balance; + gnc_numeric cleared_balance; + gnc_numeric reconciled_balance; + + /* version number, used for tracking multiuser updates */ + gint32 version; + guint32 version_check; /* data aging timestamp */ + + GList *splits; /* list of split pointers */ + + /* keep track of nesting level of begin/end edit calls */ + gint32 editlevel; + + gboolean balance_dirty; /* balances in splits incorrect */ + gboolean sort_dirty; /* sort order of splits is bad */ + gboolean core_dirty; /* fields in this struct have changed */ + gboolean do_free; /* in process of being destroyed */ + + /* The "mark" flag can be used by the user to mark this account + * in any way desired. Handy for specialty traversals of the + * account tree. */ + short mark; + + /* -------------------------------------------------------------- */ + /* Backend private expansion data */ + guint32 idata; /* used by the sql backend for kvp management */ +}; + + +/* The xaccAccountRemoveSplit() routine will remove the indicated split + * from the indicated account. Note that this will leave the split + * "dangling", i.e. unassigned to any account, and therefore will put + * the engine into an inconsistent state. After removing a split, + * it should be immediately destroyed, or it should be inserted into + * an account. + */ +void xaccAccountRemoveSplit (Account *, Split *); + +/* the following recompute the partial balances (stored with the + * transaction) and the total balance, for this account */ +void xaccAccountRecomputeBalance (Account *); + +/* Set the account's GUID. This should only be done when reading + * an account from a datafile, or some other external source. Never + * call this on an existing account! */ +void xaccAccountSetGUID (Account *account, const GUID *guid); + +/* The xaccAccountSetStartingBalance() routine will set the starting + * commodity balance for this account. This routine is intended for + * use with backends that do not return the complete list of splits + * for an account, but rather return a partial list. In such a case, + * the backend will typically return all of the splits after some + * certain date, and the 'starting balance' will represent the summation + * of the splits up to that date. + * + * Design Note: this routine assumes that there is only one commodity + * associated with this account, and that the reporting currency will + * no longer be stored with the account. + * + * This routine is in the private .h file because only backends are + * allowed to set the starting balance. This is *not* a user interface + * function. + */ +void xaccAccountSetStartingBalance(Account *account, + const gnc_numeric start_baln, + const gnc_numeric start_cleared_baln, + const gnc_numeric start_reconciled_baln); + +/* The xaccFreeAccount() routine releases memory associated with the + * account. It should never be called directly from user code; + * instead, the xaccAccountDestroy() routine should be used + * (because xaccAccountDestroy() has the correct commit semantics). + */ + +void xaccFreeAccount (Account *account); + +/* The xaccAccountSet/GetVersion() routines set & get the version + * numbers on this account. The version number is used to manage + * multi-user updates. These routines are private because we don't + * want anyone except the backend to mess with them. + */ +void xaccAccountSetVersion (Account*, gint32); +gint32 xaccAccountGetVersion (Account*); + +#endif /* XACC_ACCOUNT_P_H */ diff --git a/src/engine/Backend.c b/src/engine/Backend.c new file mode 100644 index 0000000000..3e5e45a212 --- /dev/null +++ b/src/engine/Backend.c @@ -0,0 +1,207 @@ +/********************************************************************\ + * Backend.c -- utility routines for dealing with the data backend * + * Copyright (C) 2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#include + +#include "Account.h" +#include "AccountP.h" +#include "BackendP.h" +#include "Group.h" +#include "GroupP.h" +#include "gnc-engine-util.h" +#include "gnc-pricedb.h" +#include "gnc-pricedb-p.h" +#include "TransactionP.h" + +/* static short module = MOD_ENGINE; */ + +/********************************************************************\ + * error handling * +\********************************************************************/ + +void +xaccBackendSetError (Backend *be, GNCBackendError err) +{ + if (!be) return; + + /* use stack-push semantics. Only the earliest error counts */ + if (ERR_BACKEND_NO_ERR != be->last_err) return; + be->last_err = err; +} + +GNCBackendError +xaccBackendGetError (Backend *be) +{ + GNCBackendError err; + if (!be) return ERR_BACKEND_NO_BACKEND; + + /* use 'stack-pop' semantics */ + err = be->last_err; + be->last_err = ERR_BACKEND_NO_ERR; + return err; +} + + +/********************************************************************\ + * Fetch the backend * +\********************************************************************/ + +Backend * +xaccAccountGetBackend (Account * acc) +{ + Account *parent_acc; + AccountGroup * grp; + + if (!acc) return NULL; + + /* find the first account group that has a backend */ + grp = acc->parent; + while (grp) { + if (grp->backend) return (grp->backend); + parent_acc = grp -> parent; + grp = NULL; + if (parent_acc) { + grp = parent_acc->parent; + } + } + return NULL; +} + +/********************************************************************\ + * Fetch the backend * +\********************************************************************/ + +Backend * +xaccTransactionGetBackend (Transaction *trans) +{ + GList *snode, *node; + Split *s=NULL; + + if (!trans) return NULL; + + /* find an account */ + snode = xaccTransGetSplitList(trans); + for (node = snode; node; node=node->next) + { + s = node->data; + if (xaccSplitGetAccount(s)) break; + s = NULL; + } + + /* if transaction is being deleted, it won't have any splits + * so lets take a look at the 'original' transaction */ + if (!s) + { + snode = xaccTransGetSplitList(trans->orig); + for (node = snode; node; node=node->next) + { + s = node->data; + if (xaccSplitGetAccount(s)) break; + s = NULL; + } + } + if (!s) return NULL; + + /* I suppose it would be more 'technically correct' to make sure that + * all splits share the same backend, and flag an error if they + * don't. However, at this point, it seems quite unlikely, so we'll + * just use the first backend we find. + */ + return xaccAccountGetBackend (xaccSplitGetAccount(s)); +} + +/********************************************************************\ + * Set the backend * +\********************************************************************/ + +void +xaccGroupSetBackend (AccountGroup *grp, Backend *be) +{ + if (!grp) return; + grp->backend = be; +} + +Backend * +xaccGroupGetBackend (AccountGroup *grp) +{ + while (grp) + { + Account *parent; + if (grp->backend) return (grp->backend); + parent = grp->parent; + if (!parent) return NULL; + grp = parent->parent; + } + return NULL; +} + +/********************************************************************\ + * Set the backend * +\********************************************************************/ + +void +xaccPriceDBSetBackend (GNCPriceDB *prdb, Backend *be) +{ + if (!prdb) return; + prdb->backend = be; +} + +Backend * +xaccPriceDBGetBackend (GNCPriceDB *prdb) +{ + if (!prdb) return NULL; + return prdb->backend; +} + +/***********************************************************************/ +/* Get a clean backend */ +void +xaccInitBackend(Backend *be) +{ + be->book_begin = NULL; + be->book_load = NULL; + be->price_load = NULL; + be->book_end = NULL; + be->destroy_backend = NULL; + + be->account_begin_edit = NULL; + be->account_commit_edit = NULL; + be->trans_begin_edit = NULL; + be->trans_commit_edit = NULL; + be->trans_rollback_edit = NULL; + be->price_begin_edit = NULL; + be->price_commit_edit = NULL; + + be->run_query = NULL; + be->price_lookup = NULL; + be->all_sync = NULL; + be->sync = NULL; + be->sync_price = NULL; + + be->events_pending = NULL; + be->process_events = NULL; + + be->last_err = ERR_BACKEND_NO_ERR; +} + +/************************* END OF FILE ********************************/ diff --git a/src/engine/Backend.h b/src/engine/Backend.h new file mode 100644 index 0000000000..88f95a80be --- /dev/null +++ b/src/engine/Backend.h @@ -0,0 +1,96 @@ +/********************************************************************\ + * Backend.h -- api for engine Backend * + * * + * Copyright (c) 2000, 2001 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * Backend.h + * + * FUNCTION: + * The 'backend' is a pseudo-object providing an interface between the + * engine and a persistant data store (e.g. a server, a database, or + * a file). There are no backend functions that are 'public' to + * users of the engine. The backend can, however, report errors to + * the GUI & other front-end users. This file defines these errors. + */ + +#ifndef XACC_BACKEND_H +#define XACC_BACKEND_H + +#include "config.h" + +/* NOTE: if you modify GNCBackendError, please update src/scm/gnc.gwp */ +typedef enum { + ERR_BACKEND_NO_ERR = 0, + ERR_BACKEND_NO_BACKEND, /* Backend * pointer was null the err routine */ + /* or no backend handler (ENOSYS) */ + ERR_BACKEND_BAD_URL, /* Can't parse url */ + ERR_BACKEND_NO_SUCH_DB, /* the named database doesn't exist */ + ERR_BACKEND_CANT_CONNECT, /* bad dbname/login/passwd or network failure */ + ERR_BACKEND_CONN_LOST, /* Lost connection to server */ + ERR_BACKEND_LOCKED, /* in use by another user (ETXTBSY) */ + ERR_BACKEND_TOO_NEW, /* file/db version newer than what we can read */ + ERR_BACKEND_DATA_CORRUPT, /* data in db is corrupt */ + ERR_BACKEND_SERVER_ERR, /* error in response from server */ + ERR_BACKEND_ALLOC, /* internal memory allocation failure */ + ERR_BACKEND_PERM, /* user login successful, but no permissions + * to access the desired object */ + ERR_BACKEND_MODIFIED, /* commit of object update failed because + * another user has modified the object */ + ERR_BACKEND_MOD_DESTROY, /* commit of object update failed because + * another user has deleted the object */ + ERR_BACKEND_MISC, /* undetermined error */ + + /* fileio errors */ + ERR_FILEIO_FILE_BAD_READ = 1000, /* read failed or file prematurely truncated */ + ERR_FILEIO_FILE_EMPTY, /* file exists, is readable, but is empty */ + ERR_FILEIO_FILE_LOCKERR, /* mangled locks (unspecified error) */ + ERR_FILEIO_FILE_NOT_FOUND, /* not found / no such file */ + ERR_FILEIO_FILE_TOO_OLD, /* file version so old we can't read it */ + ERR_FILEIO_UNKNOWN_FILE_TYPE, + + /* network errors */ + ERR_NETIO_SHORT_READ = 2000, /* not enough bytes received */ + ERR_NETIO_WRONG_CONTENT_TYPE, /* wrong kind of server, wrong data served */ + ERR_NETIO_NOT_GNCXML, /* whatever it is, we can't parse it. */ + + /* database errors */ + ERR_SQL_MISSING_DATA = 3000, /* database doesn't contain expected data */ + ERR_SQL_DB_TOO_OLD, /* database is old and needs upgrading */ + ERR_SQL_DB_BUSY, /* database is busy, cannot upgrade version */ + + /* RPC errors */ + ERR_RPC_HOST_UNK = 4000, /* Host unknown */ + ERR_RPC_CANT_BIND, /* can't bind to address */ + ERR_RPC_CANT_ACCEPT, /* can't accept connection */ + ERR_RPC_NO_CONNECTION, /* no connection to server */ + ERR_RPC_BAD_VERSION, /* RPC Version Mismatch */ + ERR_RPC_FAILED, /* Operation failed */ + ERR_RPC_NOT_ADDED, /* object not added */ + +} GNCBackendError; +/* NOTE: if you modify GNCBackendError, please update src/scm/gnc.gwp */ + +typedef struct _backend Backend; + +#endif /* XACC_BACKEND_H */ diff --git a/src/engine/BackendP.h b/src/engine/BackendP.h new file mode 100644 index 0000000000..6669cd190b --- /dev/null +++ b/src/engine/BackendP.h @@ -0,0 +1,230 @@ +/********************************************************************\ + * Backend.h -- private api for engine Backend * + * * + * Copyright (c) 2000, 2001 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * BackendP.h + * + * FUNCTION: + * Pseudo-object defining how the engine can interact with different + * back-ends (which may be SQL databases, or network interfaces to + * remote GnuCash servers. In theory, file-io should be a type of + * backend). + * + * The callbacks will be called at the appropriate times during + * a book session to allow the backend to store the data as needed. + * + */ + +#ifndef XACC_BACKEND_P_H +#define XACC_BACKEND_P_H + +#include "config.h" + +#include "Account.h" +#include "Backend.h" +#include "Group.h" +#include "Query.h" +#include "Transaction.h" +#include "gnc-book.h" +#include "gnc-pricedb.h" + +/* + * The book_begin() routine gives the backend a second initialization + * opportunity. It is suggested that the backend check that + * the URL is syntactically correct, and that it is actually + * reachable. This is probably(?) a good time to initialize + * the actual network connection. + * + * The 'ignore_lock' argument indicates whether the single-user + * lock on the backend should be cleared. The typical GUI sequence + * leading to this is: (1) GUI attempts to open the backend + * by calling this routine with FALSE==ignore_lock. (2) If backend + * error'ed BACKEND_LOCK, then GUI asks user what to do. (3) if user + * answers 'break & enter' then this routine is called again with + * TRUE==ignore_lock. + * + * The 'create_if_nonexistent' argument indicates whether this + * routine should create a new 'database', if it doesn't already + * exist. For example, for a file-backend, this would create the + * file, if it didn't already exist. For an SQL backend, this + * would create the database (the schema) if it didn't already + * exist. This flag is used to implement the 'SaveAs' GUI, where + * the user requests to save data to a new backend. + * + * The book_load() routine should return at least an account tree, + * and all currencies. It does not have to return any transactions + * whatsoever, as these are obtained at a later stage when a user + * opens a register, resulting in a query being sent to the backend. + * + * (Its OK to send over transactions at this point, but one should + * be careful of the network load; also, its possible that whatever + * is sent is not what the user wanted anyway, which is why its + * better to wait for the query). + * + * The trans_commit_edit() routine takes two transaction arguments: + * the first is the proposed new transaction; the second is the + * 'original' transaction. The second argument is here for + * convenience; it had better be substantially equivalent to + * the argument for the trans_begin_edit() callback. (It doesn't + * have to be identical, it can be a clone). + * + * The trans_rollback_edit() routine is invoked in one of two different + * ways. In one case, the user may hit 'undo' in the GUI, resulting + * in xaccTransRollback() being called, which in turn calls this + * routine. In this manner, xaccTransRollback() implements a + * single-level undo convenience routine for the GUI. The other + * way in which this routine gets invoked involves conflicting + * edits by two users to the same transaction. The second user + * to make an edit will typically fail in trans_commit_edit(), + * with trans_commit_edit() returning an error code. This + * causes xaccTransCommitEdit() to call xaccTransRollback() + * which in turn calls this routine. Thus, this routine + * gives the backend a chance to clean up failed commits. + * + * If the second user tries to modify a transaction that + * the first user deleted, then the backend should set the error + * to ERR_BACKEND_MOD_DESTROY from this routine, so that the + * engine can properly clean up. + * + * The run_query() callback takes a GnuCash query object. + * For an SQL backend, the contents of the query object need to + * be turned into a corresponding SQL query statement, and sent + * to the database for evaluation. The database will return a + * set of splits and transactions, and this callback needs + * to poke these into the account-group hierarchy held by the + * query object. + * + * For a network-communications backend, essentially the same is + * done, except that this routine would convert the query to wire + * protocol, get an answer from the remote server, and push that + * into the account-group object. + * + * Note a peculiar design decision we've used here. The query + * callback has returned a list of splits; these could be returned + * directly to the caller. They are not. By poking them into the + * existing account hierarchy, we are essentially building a local + * cache of the split data. This will allow the GnuCash client to + * continue functioning even when disconnected from the server: + * this is because it will have its local cache of data to work from. + * + * The sync() routine synchronizes the engine contents to the backend. + * This is done by using version numbers (hack alert -- the engine + * does not currently contain version numbers). + * If the engine contents are newer than what's in the backend, the + * data is stored to the backend. If the engine contents are older, + * then the engine contents are updated. + * + * Note that this sync operation is only meant to apply to the + * current contents of the engine. This routine is not intended + * to be used to fetch account/transaction data from the backend. + * (It might pull new splits from the backend, if this is what is + * needed to update an existing transaction. It might pull new + * currencies (??)) + * + * The events_pending() routines should return true if there are + * external events which need to be processed to bring the + * engine up to date with the backend. + * + * The process_events() routine should process any events indicated + * by the events_pending() routine. It should return TRUE if + * the engine was changed while engine events were suspended. + * + * The last_err member indicates the last error that occurred. + * It should probably be implemented as an array (actually, + * a stack) of all the errors that have occurred. + */ + +struct _backend +{ + void (*book_begin) (Backend *be, GNCBook *book, const char *book_id, + gboolean ignore_lock, gboolean create_if_nonexistent); + AccountGroup * (*book_load) (Backend *); + GNCPriceDB * (*price_load) (Backend *); + void (*book_end) (Backend *); + void (*destroy_backend) (Backend *); + + void (*account_begin_edit) (Backend *, Account *); + void (*account_commit_edit) (Backend *, Account *); + void (*trans_begin_edit) (Backend *, Transaction *); + void (*trans_commit_edit) (Backend *, Transaction *new, Transaction *orig); + void (*trans_rollback_edit) (Backend *, Transaction *); + + void (*price_begin_edit) (Backend *, GNCPrice *); + void (*price_commit_edit) (Backend *, GNCPrice *); + + void (*run_query) (Backend *, Query *); + void (*price_lookup) (Backend *, GNCPriceLookup *); + void (*all_sync) (Backend *, AccountGroup *, GNCPriceDB *); + void (*sync) (Backend *, AccountGroup *); + void (*sync_price) (Backend *, GNCPriceDB *); + + gboolean (*events_pending) (Backend *be); + gboolean (*process_events) (Backend *be); + + GNCBackendError last_err; +}; + +/* + * The xaccBackendSetError() routine pushes an error code onto the error + * stack. (FIXME: the stack is 1 deep in current implementation). + * + * The xaccBackendGetError() routine pops an error code off the error + * stack. + */ + +void xaccBackendSetError (Backend *be, GNCBackendError err); +GNCBackendError xaccBackendGetError (Backend *be); + +/* + * The xaccGetAccountBackend() subroutine will find the + * persistent-data storage backend associated with this account. + * This routine traverses up the account hierarchy until it + * finds and account-group node that has a backend associated with + * it. The assumption is that all accounts in that account-group + * share a common back-end. + * + * The xaccGetTransactionBackend() subroutine does the same, for a given + * transaction. + */ + +Backend * xaccAccountGetBackend (Account *account); +Backend * xaccTransactionGetBackend (Transaction *trans); + +/* + * The xaccGroupSetBackend() associates a backend to a group + */ +void xaccGroupSetBackend (AccountGroup *group, Backend *be); +Backend * xaccGroupGetBackend (AccountGroup *group); +Backend * xaccGNCBookGetBackend (GNCBook *book); + +/* + * Put a link to the backend that handles this pricedb + */ +void xaccPriceDBSetBackend (GNCPriceDB *prdb, Backend *be); +Backend * xaccPriceDBGetBackend (GNCPriceDB *prdb); + +void xaccInitBackend(Backend *be); + +#endif /* XACC_BACKEND_P_H */ diff --git a/src/engine/DateUtils.c b/src/engine/DateUtils.c new file mode 100644 index 0000000000..99f3a4c16b --- /dev/null +++ b/src/engine/DateUtils.c @@ -0,0 +1,71 @@ +/********************************************************************\ + * DateUtils.c -- Date Handling Utilities * + * Copyright (C) 1998 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "DateUtils.h" + +#define BUFFSIZE 100 + + +/* ======================================================== */ +char * +xaccDateUtilGetStamp (time_t thyme) +{ + struct tm *stm; + char buf[BUFFSIZE]; + char * retval; + + stm = localtime (&thyme); + + sprintf (buf, "%04d%02d%02d%02d%02d%02d", + (stm->tm_year + 1900), + (stm->tm_mon +1), + stm->tm_mday, + stm->tm_hour, + stm->tm_min, + stm->tm_sec + ); + + retval = g_strdup (buf); + return retval; +} + +/* ======================================================== */ + +char * +xaccDateUtilGetStampNow (void) +{ + time_t now; + time (&now); + return xaccDateUtilGetStamp (now); +} + +/************************ END OF ************************************\ +\************************* FILE *************************************/ diff --git a/src/engine/DateUtils.h b/src/engine/DateUtils.h new file mode 100644 index 0000000000..a70de67c93 --- /dev/null +++ b/src/engine/DateUtils.h @@ -0,0 +1,37 @@ +/********************************************************************\ + * DateUtils.h -- Date Handling Utilities * + * Copyright (C) 1998 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef XACC_DATE_UTILS_H +#define XACC_DATE_UTILS_H + +#include + +#include "config.h" + +char * xaccDateUtilGetStamp (time_t thyme); +char * xaccDateUtilGetStampNow (void); + +#endif /* XACC_DATE_UTILS_H */ + +/************************ END OF ************************************\ +\************************* FILE *************************************/ diff --git a/src/engine/FreqSpec.c b/src/engine/FreqSpec.c new file mode 100644 index 0000000000..14af38b494 --- /dev/null +++ b/src/engine/FreqSpec.c @@ -0,0 +1,878 @@ +/********************************************************************\ + * FreqSpec.c -- Frequency specifier implementation. * + * Copyright (C) 2001 Joshua Sled * + * Copyright (C) 2001 Ben Stanley * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/********************************************************************\ + Current status + All kinds of repeats work, including composites. This is tested - + although composites need some more test cases to be put into the test + suite - ../test/test-freq-spec.c + FreqSpec objects are currently 'set' to give them the information + they need. Separate methods for modifying currently existing FreqSpec + objects are not provided. In the case of composites, you may add FreqSpec + objects to a composite, and you may access a list of the FreqSpec objects + which form the composite. This interface allows you to do damage... + + TODO list + Ben Stanley 2001-04-02 + * Write xaccFreqSpecGetFreqStr (I wonder how this will be + internationalised... I suspect that this code will need to be + re-written for each language, because the code will have to + generate grammar... It's more than just translating strings.) + However, the first priority is to write one that works for + English. + * Write a function to allow you to query whether a given + date forms part of the recurrence. + * Write a method to get the previous recurrence + * provide XML Load/Save functionality for this object. + * Figure out xaccFreqSpecIsValidDate - I suspect that this is the + 'query whether a given date forms part of the recurrence' + above. + * FIGURE OUT WHAT'S GOING ON WITH xaccFreqSpecGetUIType AND + xaccFreqSpecSetUIType. + * Try to reduce the size of the data structure. There are quite a few + 32 bit fields which could be stored in about 8 bits. + * Add public methods to allow for recurrences with an interval + of 1 to be set without reference to an initial 'date' - monthly + things in particular. Try to reduce the dependence on an initial + date for the input to set up the recurrence. + + Questions: + Is it best that the public interface stay as GDate, or should it + really be a timespec? I have no problem with converting GDates to + timespecs for load/save if that makes life easier. + + However, I chose to use GDate internally because I have used a *lot* + of the date calculating ability of GDate in the internal implementation. + GDate has simplified this work enormously compared to using struct tm + and time_t. The engine's timespec object doesn't appear to have the + required functionality either, so I would need to write the required + functions for timespec (perhaps by implementing in terms of GDate?). + + Hopefully it's not too painful to leave GDate in the public interface + and change other code to use it. +\********************************************************************/ + +#include "config.h" + +/* #include // should be config'd */ + +#include +#include + +#include "FreqSpecP.h" +#include "GNCIdP.h" +/*#include "Transaction.h"*/ +/*#include "TransactionP.h"*/ +#include "date.h" +#include "gnc-engine-util.h" +#include "gnc-event-p.h" +#include "messages.h" + +/* I have done this to prevent compiler warnings... + * This is used to convert a const GDate* to a GDate* for passing + * to the glib g_date_xxx functions which don't use const... + * Strangely, most of the rest of glib does use const, so + * perhaps this will change? When it does, just define this macro to + * nothing and the compiler will check the constness of each pointer.... + */ +#define CONST_HACK (GDate*) + +static short module = MOD_SX; + +/** PROTOTYPES ******************************************************/ + +/** + * Destroys all sub-FreqSpecs in a composite FreqSpec. + * Assertion error if it's not a COMPOSITE FreqSpec. + **/ +void xaccFreqSpecCompositesClear( FreqSpec *fs ); + +void subSpecsListMapDelete( gpointer data, gpointer user_data ); + +/** Local data defs *****/ + +/** + * The number of days in each month. + **/ +struct monthDesc { + char *dshort; + char *dlong; + gint numDays; +}; + +/* This stuff is going to need i18n. + * wouldn't it be simpler to use the system + * date conversion functions? + * glib already knows about this. + * *libc* already knows about this + * and the month names below. Both + * of these should go away! */ +static char *weekDayNames[] = { + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" +}; + +/* hmmm... glib already knows all about this. */ +struct monthDesc monthInfo[] = { +#define M_JAN 0 + { "Jan", "January", 31 }, +#define M_FEB 1 + { "Feb", "February", 28 }, +#define M_MAR 2 + { "Mar", "March", 31 }, +#define M_APR 3 + { "Apr", "April", 30 }, +#define M_MAY 4 + { "May", "May", 31 }, +#define M_JUN 5 + { "Jun", "June", 30 }, +#define M_JUL 6 + { "Jul", "July", 31 }, +#define M_AUG 7 + { "Aug", "August", 31 }, +#define M_SEP 8 + { "Sep", "September", 30 }, +#define M_OCT 9 + { "Oct", "October", 31 }, +#define M_NOV 10 + { "Nov", "November", 30 }, +#define M_DEC 11 + { "Dec", "December", 31 } +}; + +/** Local Prototypes *****/ +static void xaccFreqSpecInit( FreqSpec *fs ); + +/** + * Initializes a FreqSpec by setting it's to type INVALID. + * Use this to initialise a stack object. + * FreqSpec objects must be initalised before being used by + * any other method. + **/ + +static void +xaccFreqSpecInit( FreqSpec *fs ) +{ + g_return_if_fail( fs ); + xaccGUIDNew( &fs->guid ); + xaccStoreEntity( fs, &fs->guid, GNC_ID_FREQSPEC ); + fs->type = INVALID; + fs->uift = UIFREQ_ONCE; + memset( &(fs->s), 0, sizeof(fs->s) ); +} + +FreqSpec* +xaccFreqSpecMalloc(void) +{ + FreqSpec *fs = g_new0(FreqSpec, 1); + xaccFreqSpecInit( fs ); + /* FIXME:event */ + gnc_engine_generate_event( &fs->guid, GNC_EVENT_CREATE ); + return fs; +} + + +void +xaccFreqSpecCleanUp( FreqSpec *fs ) +{ + g_return_if_fail( fs ); + switch ( fs->type ) { + case INVALID: + case ONCE: + case DAILY: + case WEEKLY: + case MONTHLY: + case MONTH_RELATIVE: + break; + case COMPOSITE: + xaccFreqSpecCompositesClear( fs ); + g_list_free( fs->s.composites.subSpecs ); + break; + default: + g_return_if_fail(FALSE); + } + fs->type = INVALID; +} + +void +xaccFreqSpecFree( FreqSpec *fs ) +{ + if ( fs == NULL ) return; + gnc_engine_generate_event( &fs->guid, GNC_EVENT_DESTROY ); + xaccRemoveEntity( &fs->guid ); + + xaccFreqSpecCleanUp( fs ); + + g_free( fs ); +} + +FreqType +xaccFreqSpecGetType( FreqSpec *fs ) +{ + g_return_val_if_fail( fs, INVALID ); + /* Is this really a fail? */ + g_return_val_if_fail( fs->type != INVALID, INVALID ); + return fs->type; +} + + +UIFreqType +xaccFreqSpecGetUIType( FreqSpec *fs ) +{ + g_return_val_if_fail( fs, INVALID ); + return fs->uift; +} + +void +xaccFreqSpecSetUIType( FreqSpec *fs, UIFreqType newUIFreqType ) +{ + g_return_if_fail( fs ); + fs->uift = newUIFreqType; +} + +/* +void +xaccFreqSpecSetTypes( FreqSpec *fs, FreqType newFT, UIFreqType newUIFT ) +{ + g_return_if_fail( fs ); + xaccFreqSpecSetType( fs, newFT ); + xaccFreqSpecSetUIType( fs, newUIFT ); +} +*/ + +static inline guint32 min( guint32 a, guint32 b ) +{ + return a > b ? b : a; +} + +void +xaccFreqSpecGetNextInstance( + FreqSpec *fs, + const GDate* in_date, + GDate* out_date ) +{ + GList *list, *blist; + int mon; + + g_return_if_fail( fs ); + switch( fs->type ) { + case INVALID: + g_return_if_fail(FALSE); + + case ONCE: + if ( g_date_compare( &(fs->s.once.date), CONST_HACK in_date ) > 0 ) { + *out_date = fs->s.once.date; + } else { + /* Date is past due. Return an invalid date. */ + g_date_clear( out_date, 1 ); + } + break; + + case DAILY: { + guint32 julian_in_date, julian_next_repeat, complete_intervals; + + julian_in_date = g_date_julian( CONST_HACK in_date ); + complete_intervals = + (julian_in_date - fs->s.daily.offset_from_epoch) / + fs->s.daily.interval_days; + julian_next_repeat = + fs->s.daily.offset_from_epoch + + (complete_intervals + 1) * fs->s.daily.interval_days; + g_date_set_julian( out_date, julian_next_repeat ); + } break; + + case WEEKLY: { + /* This implementation stores the offset from epoch as the number + * of days, not week epoch offset and day in week offset. + * It is very similar to the daily repeat representation. */ + guint32 julian_in_date, julian_next_repeat, complete_intervals; + + julian_in_date = g_date_julian( CONST_HACK in_date ); + complete_intervals = + (julian_in_date - fs->s.weekly.offset_from_epoch) / + (fs->s.weekly.interval_weeks * 7); + julian_next_repeat = + fs->s.weekly.offset_from_epoch + + (complete_intervals + 1) * fs->s.weekly.interval_weeks * 7; + g_date_set_julian( out_date, julian_next_repeat ); + + /* This code passes the test, but it seems large and complicated... + * it uses a separate week offset from epoch and day in week offset. */ +/* guint32 julian_in_date, julian_next_repeat, complete_intervals, + in_weeks_from_epoch, after_repeat_in_week_interval; + julian_in_date = g_date_julian( CONST_HACK in_date ); + in_weeks_from_epoch = (julian_in_date-1) / 7; + complete_intervals = + (in_weeks_from_epoch - + fs->s.weekly.offset_from_epoch) / + fs->s.weekly.interval_weeks; + after_repeat_in_week_interval = + ((julian_in_date-1) % 7 >= fs->s.weekly.day_of_week || + (in_weeks_from_epoch - fs->s.weekly.offset_from_epoch) % + fs->s.weekly.interval_weeks > 0 ) ? 1 : 0; + julian_next_repeat = + (fs->s.weekly.offset_from_epoch + + (complete_intervals + after_repeat_in_week_interval) * + fs->s.weekly.interval_weeks) * 7 + + fs->s.weekly.day_of_week + 1; + g_date_set_julian( out_date, julian_next_repeat ); +*/ + } break; + + case MONTHLY: { + guint32 in_months_from_epoch, after_repeat_in_month_interval, + complete_intervals, next_repeat_months_from_epoch, month, year; + + in_months_from_epoch = (g_date_year( CONST_HACK in_date )-1) * 12 + + g_date_month( CONST_HACK in_date ) - 1; + complete_intervals = + (in_months_from_epoch - fs->s.monthly.offset_from_epoch) / + fs->s.monthly.interval_months; + after_repeat_in_month_interval = + (g_date_day( CONST_HACK in_date ) >= fs->s.monthly.day_of_month || + (in_months_from_epoch - fs->s.monthly.offset_from_epoch) % + fs->s.monthly.interval_months > 0 || + g_date_day( CONST_HACK in_date ) >= + g_date_days_in_month( g_date_month( CONST_HACK in_date ), + g_date_year( CONST_HACK in_date ) ) ) ? 1 : 0; + next_repeat_months_from_epoch = + fs->s.monthly.offset_from_epoch + + (complete_intervals + after_repeat_in_month_interval) * + fs->s.monthly.interval_months; + /* Hmmm... what happens if the day of the month is greater than the + * number of days in this month? + * Here I have constrained the day of the month by the number + * of days in the month. This is compensated for above by checking if + * the input day is the last day of that month, in which case it will + * move to the next month interval. + */ + month = next_repeat_months_from_epoch % 12 + 1; + year = next_repeat_months_from_epoch / 12 + 1; + g_date_set_dmy( out_date, + min( fs->s.monthly.day_of_month, + g_date_days_in_month( month, year ) ), + month, + year ); + } break; + + case MONTH_RELATIVE: { + guint32 in_months_from_epoch, after_repeat_in_month_interval, + complete_intervals, next_repeat_months_from_epoch, month, year, + wday_of_1st, day_of_repeat; + + GDate date1; + in_months_from_epoch = (g_date_year( CONST_HACK in_date )-1) * 12 + + g_date_month( CONST_HACK in_date ) - 1; + complete_intervals = + (in_months_from_epoch - fs->s.month_relative.offset_from_epoch) / + fs->s.month_relative.interval_months; + month = g_date_month( CONST_HACK in_date ); + year = g_date_year( CONST_HACK in_date ); + g_date_set_dmy( &date1, 1, month, year ); + wday_of_1st = g_date_weekday( &date1 ); + day_of_repeat = (fs->s.month_relative.occurrence-1)*7 + + ((fs->s.month_relative.weekday + 7 - wday_of_1st)%7 + 1); + after_repeat_in_month_interval = + (g_date_day( CONST_HACK in_date ) >= day_of_repeat || + day_of_repeat > g_date_days_in_month( month, year ) || + (in_months_from_epoch - fs->s.month_relative.offset_from_epoch) % + fs->s.month_relative.interval_months > 0 ) ? 1 : 0; + next_repeat_months_from_epoch = + fs->s.month_relative.offset_from_epoch + + (complete_intervals + after_repeat_in_month_interval) * + fs->s.month_relative.interval_months; + month = next_repeat_months_from_epoch % 12 + 1; + year = next_repeat_months_from_epoch / 12 + 1; + g_date_set_dmy( &date1, 1, month, year ); + wday_of_1st = g_date_weekday( &date1 ); + /* This calculates the day of the month in the month which forms + * the next month in the cycle after the given input date. + * However, this day may be larger than the number of days in that month... */ + day_of_repeat = (fs->s.month_relative.occurrence-1)*7 + + ((fs->s.month_relative.weekday + 7 - wday_of_1st)%7 + 1); + while( day_of_repeat > g_date_days_in_month( month, year ) ) { + /* If the repeat occurs after the end of the month, then + * find the next month containing a day which satisfies the request. + * Each candiate month separated by interval_months is considered + * by this loop.*/ + ++complete_intervals; + next_repeat_months_from_epoch = + fs->s.month_relative.offset_from_epoch + + complete_intervals * fs->s.month_relative.interval_months; + month = next_repeat_months_from_epoch % 12 + 1; + year = next_repeat_months_from_epoch / 12 + 1; + g_date_set_dmy( &date1, 1, month, year ); + wday_of_1st = g_date_weekday( &date1 ); + day_of_repeat = (fs->s.month_relative.occurrence-1)*7 + + ((fs->s.month_relative.weekday + 7 - wday_of_1st)%7 + 1); + /* Hmmm... It would be nice to know that this loop is + * guaranteed to terminate... CHECK ME! */ + } + g_date_set_dmy( out_date, day_of_repeat, month, year ); + } break; + + case COMPOSITE: + list = fs->s.composites.subSpecs; + if ( !list ) { + /* sets date to be invalid */ + g_date_clear( out_date, 1 ); + break; + } + { + /* This implements || composites. */ + guint32 min_julian = 0xFFFFFFFF; /* the biggest unsigned 32 bit number */ + guint32 this_julian; + do { + GDate next_repeat; + xaccFreqSpecGetNextInstance( + (FreqSpec*) list->data, + in_date, + &next_repeat ); + this_julian = g_date_julian( &next_repeat ); + + min_julian = min( min_julian, this_julian ); + + } while ( (list = g_list_next(list)) ); + g_date_set_julian( out_date, min_julian ); + } + break; + + default: + g_date_clear( out_date, 1 ); + g_return_if_fail(FALSE); + } +} + +/* +char* +xaccFreqSpecIsValidDateRelaxed( FreqSpec *fs, time_t query ) +{ + return "FIXME: not implemented yet!"; +} +*/ + +void +xaccFreqSpecSetOnceDate( FreqSpec *fs, const GDate* when ) +{ + g_return_if_fail( fs ); + g_return_if_fail( when ); + xaccFreqSpecCleanUp( fs ); + fs->type = ONCE; + fs->s.once.date = *when; +} + +void +xaccFreqSpecSetDaily( FreqSpec *fs, + const GDate* initial_date, + guint interval_days ) +{ + guint32 julian_days_since_epoch; + + g_return_if_fail( fs ); + g_return_if_fail( interval_days > 0 ); + xaccFreqSpecCleanUp( fs ); + fs->type = DAILY; + fs->s.daily.interval_days = interval_days; + + julian_days_since_epoch = g_date_julian( CONST_HACK initial_date ); + fs->s.daily.offset_from_epoch = julian_days_since_epoch % interval_days; +} + +void +xaccFreqSpecSetWeekly( FreqSpec *fs, + const GDate* initial_date, + guint interval_weeks ) +{ +/* pick one... make sure that the code in next matches this, + * and that the fields in the + * weekly struct match too. + */ +#if 0 +/* * + * This implements weekly by using the fact that 1 week = 7 days. + * Weeks start at epoch in this representation, not necesarily Monday, + * so there is not really any difference... + * The weekly tests pass. + */ + guint32 julian_days_since_epoch; + + g_return_if_fail( fs ); + g_return_if_fail( interval_weeks > 0 ); + xaccFreqSpecCleanUp( fs ); + + fs->type = DAILY; + fs->s.daily.interval_days = 7 * interval_weeks; + + julian_days_since_epoch = g_date_julian( CONST_HACK initial_date ); + fs->s.daily.offset_from_epoch = julian_days_since_epoch % (7*interval_weeks); +#endif +#if 1 + /* simplest solution */ + guint32 julian_days_since_epoch; + + g_return_if_fail( fs ); + g_return_if_fail( interval_weeks > 0 ); + xaccFreqSpecCleanUp( fs ); + + fs->type = WEEKLY; + fs->s.weekly.interval_weeks = interval_weeks; + + julian_days_since_epoch = g_date_julian( CONST_HACK initial_date ); + fs->s.weekly.offset_from_epoch = julian_days_since_epoch % (7*interval_weeks); +#endif +#if 0 +/** + * Use the weekly implementation, which seems to be more complicated... + * uses separate weekly and day in week offsets. + * works. + */ + guint32 julian_day_initial, weeks_since_epoch; + + g_return_if_fail( fs ); + g_return_if_fail( interval_weeks > 0 ); + xaccFreqSpecCleanUp( fs ); + + fs->type = WEEKLY; + fs->s.weekly.interval_weeks = interval_weeks; + + julian_day_initial = g_date_julian( CONST_HACK initial_date ); + weeks_since_epoch = (julian_day_initial-1) / 7; + fs->s.weekly.day_of_week = (julian_day_initial-1) % 7; + fs->s.weekly.offset_from_epoch = weeks_since_epoch % interval_weeks; + + g_return_if_fail( 0 <= fs->s.weekly.day_of_week ); + g_return_if_fail( fs->s.weekly.day_of_week < 7 ); + g_return_if_fail( fs->s.weekly.offset_from_epoch < interval_weeks ); + g_return_if_fail( 0 <= fs->s.weekly.offset_from_epoch ); +#endif +} + +void +xaccFreqSpecSetMonthly( FreqSpec *fs, + const GDate* initial_date, + guint interval_months ) +{ + guint months_since_epoch; + g_return_if_fail( fs ); + g_return_if_fail( interval_months > 0 ); + xaccFreqSpecCleanUp( fs ); + fs->type = MONTHLY; + fs->s.monthly.interval_months = interval_months; + + months_since_epoch = (g_date_year( CONST_HACK initial_date )-1) * 12 + + g_date_month( CONST_HACK initial_date ) - 1; + fs->s.monthly.offset_from_epoch = months_since_epoch % interval_months; + fs->s.monthly.day_of_month = g_date_day( CONST_HACK initial_date ); + + g_return_if_fail( fs->s.monthly.offset_from_epoch < + fs->s.monthly.interval_months ); +} + +void +xaccFreqSpecSetMonthRelative( FreqSpec *fs, + const GDate* initial_date, + guint interval_months ) +{ + guint months_since_epoch; + g_return_if_fail( fs ); + g_return_if_fail( interval_months > 0 ); + xaccFreqSpecCleanUp( fs ); + fs->type = MONTH_RELATIVE; + fs->s.month_relative.interval_months = interval_months; + + months_since_epoch = (g_date_year( CONST_HACK initial_date )-1) * 12 + + g_date_month( CONST_HACK initial_date ) - 1; + fs->s.month_relative.offset_from_epoch = months_since_epoch % interval_months; + + fs->s.month_relative.weekday = g_date_weekday( CONST_HACK initial_date ); + fs->s.month_relative.occurrence = (g_date_day( CONST_HACK initial_date )-1) / 7 + 1; + + g_return_if_fail( fs->s.month_relative.weekday > 0 ); + g_return_if_fail( fs->s.month_relative.weekday <= 7 ); + g_return_if_fail( fs->s.month_relative.occurrence > 0 ); + g_return_if_fail( fs->s.month_relative.occurrence <= 5 ); + g_return_if_fail( fs->s.month_relative.offset_from_epoch < + fs->s.month_relative.interval_months ); +} + +void +xaccFreqSpecSetComposite( FreqSpec *fs ) +{ + g_return_if_fail( fs ); + xaccFreqSpecCleanUp( fs ); + fs->type = COMPOSITE; + fs->s.composites.subSpecs = NULL; +} + +int +xaccFreqSpecGetOnce( FreqSpec *fs, GDate *outGD ) +{ + if ( fs->type != ONCE ) + return -1; + *outGD = fs->s.once.date; + return 0; +} + +int +xaccFreqSpecGetDaily( FreqSpec *fs, int *outRepeat ) +{ + if ( fs->type != DAILY ) + return -1; + *outRepeat = fs->s.daily.interval_days; + return 0; +} + +int +xaccFreqSpecGetWeekly( FreqSpec *fs, int *outRepeat, int *outDayOfWeek ) +{ + if ( fs->type != WEEKLY ) + return -1; + *outRepeat = fs->s.weekly.interval_weeks; + *outDayOfWeek = fs->s.weekly.offset_from_epoch; + return 0; +} + +int +xaccFreqSpecGetMonthly( FreqSpec *fs, int *outRepeat, int *outDayOfMonth, int *outMonthOffset ) +{ + if ( fs->type != MONTHLY ) + return -1; + *outRepeat = fs->s.monthly.interval_months; + *outDayOfMonth = fs->s.monthly.day_of_month; + *outMonthOffset = fs->s.monthly.offset_from_epoch; + return 0; +} + +// FIXME: add month-relative getter + +GList* +xaccFreqSpecCompositeGet( FreqSpec *fs ) +{ + g_return_val_if_fail( fs, NULL ); + g_return_val_if_fail( fs->type == COMPOSITE, NULL ); + return fs->s.composites.subSpecs; +} + +void +xaccFreqSpecCompositeAdd( FreqSpec *fs, FreqSpec *fsToAdd ) +{ + g_return_if_fail( fs ); + g_return_if_fail( fs->type == COMPOSITE ); + fs->s.composites.subSpecs = + g_list_append( fs->s.composites.subSpecs, fsToAdd ); +} + +void +subSpecsListMapDelete( gpointer data, gpointer user_data ) +{ + xaccFreqSpecFree( (FreqSpec*)data ); +} + +void +xaccFreqSpecCompositesClear( FreqSpec *fs ) +{ + g_return_if_fail( fs->type == COMPOSITE ); + g_list_foreach( fs->s.composites.subSpecs, + subSpecsListMapDelete, NULL ); +} + +void +xaccFreqSpecGetFreqStr( FreqSpec *fs, GString *str ) +{ + GList *list; + FreqSpec *tmpFS; + int tmpInt; + char *tmpStr; + int i; + + /* FIXME: fill in. */ + switch( xaccFreqSpecGetUIType( fs ) ) { + case UIFREQ_ONCE: + tmpStr = g_new0( char, 25 ); + /* this is now a GDate. */ + g_date_strftime( tmpStr, 25, + "%a, %b %e, %Y", + &fs->s.once.date ); + g_string_sprintf( str, "Once: %s", tmpStr ); + g_free( tmpStr ); + break; + + case UIFREQ_DAILY: + g_string_sprintf( str, "Daily" ); + if ( fs->s.daily.interval_days > 1 ) { + g_string_sprintfa( str, " (x%u)", + fs->s.daily.interval_days ); + } + break; + + case UIFREQ_DAILY_MF: + { + FreqSpec *subFS; + if ( g_list_length( fs->s.composites.subSpecs ) != 5 ) { + PERR( "Invalid Daily[M-F] structure." ); + g_string_sprintf( str, "Daily[M-F]: error" ); + return; + } + /* We assume that all of the weekly FreqSpecs that make up + the Daily[M-F] FreqSpec have the same interval. */ + subFS = (FreqSpec*)fs->s.composites.subSpecs->data; + g_string_sprintf( str, "Daily [M-F]" ); + if ( subFS->s.weekly.interval_weeks > 1 ) { + g_string_sprintfa( str, " (x%u)", + subFS->s.weekly.interval_weeks ); + } + } + break; + + case UIFREQ_WEEKLY: + g_string_sprintf( str, "Weekly" ); + tmpInt = -1; + tmpStr = g_new0( char, 8 ); + for ( i=0; i<7; i++ ) { + tmpStr[i] = '-'; + } + + list = xaccFreqSpecCompositeGet( fs ); + do { + int dowIdx; + + tmpFS = (FreqSpec*)list->data; + if ( xaccFreqSpecGetType(tmpFS) != WEEKLY ) { + g_string_sprintf( str, + "error: UIFREQ_WEEKLY doesn't contain weekly children" ); + return; + } + if ( tmpInt == -1 ) { + tmpInt = tmpFS->s.weekly.interval_weeks; + } + /* put the first letter of the weekday name in + the appropriate position. */ + dowIdx = tmpFS->s.weekly.offset_from_epoch; + tmpStr[dowIdx] = weekDayNames[dowIdx][0]; + } while ( (list = g_list_next(list)) ); + + if ( tmpInt > 1 ) { + g_string_sprintfa( str, " (x%d)", tmpInt ); + } + g_string_sprintfa( str, ": %s", tmpStr ); + g_free( tmpStr ); + break; + + case UIFREQ_BI_WEEKLY: + g_string_sprintf( str, "Bi-Weekly, %ss", weekDayNames[fs->s.weekly.offset_from_epoch % 7] ); + break; + + case UIFREQ_SEMI_MONTHLY: + g_string_sprintf( str, "Semi-Monthly" ); + list = xaccFreqSpecCompositeGet( fs ); + tmpFS = (FreqSpec*)(g_list_nth( list, 0 )->data); + if ( tmpFS->s.monthly.interval_months > 1 ) { + g_string_sprintfa( str, " (x%u)", tmpFS->s.monthly.interval_months ); + } + g_string_sprintfa( str, ": " ); + if ( tmpFS->s.monthly.day_of_month > 31 ) { + g_string_sprintfa( str, "%s", "last day" ); + } else { + g_string_sprintfa( str, "%u", tmpFS->s.monthly.day_of_month ); + } + g_string_sprintfa( str, ", " ); + tmpFS = (FreqSpec*)(g_list_nth( list, 1 )->data); + if ( tmpFS->s.monthly.day_of_month > 31 ) { + g_string_sprintfa( str, "%s", "last day" ); + } else { + g_string_sprintfa( str, "%u", tmpFS->s.monthly.day_of_month ); + } + break; + + case UIFREQ_MONTHLY: + g_string_sprintf( str, "Monthly" ); + if ( fs->s.monthly.interval_months > 1 ) { + g_string_sprintfa( str, " (x%u)", + fs->s.monthly.interval_months ); + } + g_string_sprintfa( str, ": %u", + fs->s.monthly.day_of_month ); + break; + + case UIFREQ_QUARTERLY: + g_string_sprintf( str, "Quarterly" ); + if ( fs->s.monthly.interval_months != 3 ) { + g_string_sprintfa( str, " (x%u)", + fs->s.monthly.interval_months/3 ); + } + g_string_sprintfa( str, ": %u", + fs->s.monthly.day_of_month ); + break; + + case UIFREQ_TRI_ANUALLY: + g_string_sprintf( str, "Tri-Anually" ); + if ( fs->s.monthly.interval_months != 4 ) { + g_string_sprintfa( str, " (x%u)", + fs->s.monthly.interval_months/4 ); + } + g_string_sprintfa( str, ": %u", + fs->s.monthly.day_of_month ); + break; + + case UIFREQ_SEMI_YEARLY: + g_string_sprintf( str, "Semi-Yearly" ); + if ( fs->s.monthly.interval_months != 6 ) { + if ( (fs->s.monthly.interval_months % 6) != 0 ) { + PERR( "ERROR: FreqSpec Semi-Yearly month-interval " + "is not a multiple of 6 [%d]", + fs->s.monthly.interval_months ); + } + g_string_sprintfa( str, " (x%u)", + fs->s.monthly.interval_months/6 ); + } + g_string_sprintfa( str, ": %u", + fs->s.monthly.day_of_month ); + break; + + case UIFREQ_YEARLY: + g_string_sprintf( str, "Yearly" ); + if ( fs->s.monthly.interval_months != 12 ) { + if ( (fs->s.monthly.interval_months % 12) != 0 ) { + PERR( "ERROR: \"Yearly\" FreqSpec month-interval " + "is not a multiple of 12 [%d]", + fs->s.monthly.interval_months ); + } + g_string_sprintfa( str, " (x%u)", + fs->s.monthly.interval_months/12 ); + } + g_string_sprintfa( str, ": %s/%u", + monthInfo[fs->s.monthly.offset_from_epoch].dshort, + fs->s.monthly.day_of_month ); + break; + + default: + g_string_sprintf( str, "Unknown" ); + break; + } +} diff --git a/src/engine/FreqSpec.h b/src/engine/FreqSpec.h new file mode 100644 index 0000000000..ba5fe9de94 --- /dev/null +++ b/src/engine/FreqSpec.h @@ -0,0 +1,240 @@ +/********************************************************************\ + * FreqSpec.h -- Frequency Specification * + * Copyright (C) 2001 Joshua Sled * + * Copyright (C) 2001 Ben Stanley * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef XACC_FREQSPEC_H +#define XACC_FREQSPEC_H + +#include "config.h" + +#include + +#include "GNCId.h" + +/** + * Frequency specification. + * + **/ +typedef enum gncp_FreqType { + INVALID, + ONCE, + DAILY, + WEEKLY, /* Hmmm... This is sort of DAILY[7]... */ + /* BI_WEEKLY: weekly[2] */ + /* SEMI_MONTHLY: use composite */ + MONTHLY, + MONTH_RELATIVE, + /* YEARLY: monthly[12] */ + COMPOSITE, +} FreqType; + +/** + * The user's conception of the frequency. It is expected that this + * list will grow, while the former [FreqType] will not. + * + * Ideally this is not here, but what can you do? + **/ +typedef enum gncp_UIFreqType { + UIFREQ_NONE, + UIFREQ_ONCE, + UIFREQ_DAILY, + UIFREQ_DAILY_MF, + UIFREQ_WEEKLY, + UIFREQ_BI_WEEKLY, + UIFREQ_SEMI_MONTHLY, + UIFREQ_MONTHLY, + UIFREQ_QUARTERLY, + UIFREQ_TRI_ANUALLY, + UIFREQ_SEMI_YEARLY, + UIFREQ_YEARLY, + UIFREQ_NUM_UI_FREQSPECS +} UIFreqType; + +/** + * Forward declaration of FreqSpec type for storing + * date repetition information. This is an opaque type. + */ + +struct gncp_freq_spec; +typedef struct gncp_freq_spec FreqSpec; + + +/** PROTOTYPES ******************************************************/ + +/** + * Allocates memory for a FreqSpec and initializes it. + **/ +FreqSpec* xaccFreqSpecMalloc(void); + +/** + * destroys any private data belonging to the FreqSpec. + * Use this for a stack object. + */ +void xaccFreqSpecCleanUp( FreqSpec *fs ); + +/** + * Frees a heap allocated FreqSpec. + * This is the opposite of xaccFreqSpecMalloc(). + **/ +void xaccFreqSpecFree( FreqSpec *fs ); + +/** + * Gets the type of a FreqSpec. + **/ +FreqType xaccFreqSpecGetType( FreqSpec *fs ); + +/** + * Sets the type of a FreqSpec. + * Setting the type re-initializes any spec-data; this means + * destroying any sub-types in the case of COMPOSITE. + * THESE FUNCTIONS HAVE NOT BEEN MAINTAINED THROUGH BEN'S CHANGES. + * They need to be checked. + **/ +/* void xaccFreqSpecSetType( FreqSpec *fs, FreqType newType ); */ + +UIFreqType xaccFreqSpecGetUIType( FreqSpec *fs ); +void xaccFreqSpecSetUIType( FreqSpec *fs, UIFreqType newUIFreqType ); + +/* Convenience function; calls the two above. */ +/* void xaccFreqSpecSetTypes( FreqSpec *fs, FreqType newType, UIFreqType newUIType ); */ + +/** + * Sets the type to once-off, and initialises the + * date it occurs on. + * Disposes of any previous data. + */ +void xaccFreqSpecSetOnceDate( FreqSpec *fs, + const GDate* when ); + +/** + * Sets the type to DAILY. Disposes of any previous data. + * Uses the start date to figure + * out how many days after epoch (1/1/1900) this repeat would + * have first occurred on if it had been running back then. + * This is used later to figure out absolute repeat dates. + */ +void xaccFreqSpecSetDaily( FreqSpec *fs, + const GDate* initial_date, + guint interval_days ); + +/** + * Sets the type to WEEKLY. Disposes of any previous data. + * Uses the inital date to figure out the day of the week to use. + */ +void xaccFreqSpecSetWeekly( FreqSpec *fs, + const GDate* inital_date, + guint interval_weeks ); + +/** + * Sets the type to MONTHLY. Disposes of any previous data. + * Uses the inital date to figure out the day of the month to use. + */ +void xaccFreqSpecSetMonthly( FreqSpec *fs, + const GDate* inital_date, + guint interval_months ); + +/** + * Sets the type to MONTH_RELATIVE. Disposes of any previous data. + * Uses the inital date to figure out the day of the month to use. + */ +void xaccFreqSpecSetMonthRelative( FreqSpec *fs, + const GDate* inital_date, + guint interval_months ); + +/** + * Sets the type to COMPOSITE. Disposes of any previous data. + * You must Add some repeats to the composite before using + * it for repeating. + */ +void xaccFreqSpecSetComposite( FreqSpec *fs ); + +/** + * Returns a human-readable string of the Frequency. This uses + * UIFreqType to unroll the internal structure. It is an assertion + * failure if the FreqSpec data doesn't match the UIFreqType. + * Caller allocates the GString [natch]. + **/ +void xaccFreqSpecGetFreqStr( FreqSpec *fs, GString *str ); + +int xaccFreqSpecGetOnce( FreqSpec *fs, GDate *outGD ); +int xaccFreqSpecGetDaily( FreqSpec *fs, int *outRepeat ); +int xaccFreqSpecGetWeekly( FreqSpec *fs, int *outRepeat, int *outDayOfWeek ); +int xaccFreqSpecGetMonthly( FreqSpec *fs, int *outRepeat, + int *outDayOfMonth, int *outMonthOffset ); +// FIXME: add month-relative + +/** + * Returns the list of FreqSpecs in a COMPOSITE FreqSpec. + * It is an error to use this if the type is not COMPOSITE. + * The caller should not modify this list; + * only add/remove composites and use this fn to get + * the perhaps-changed list head. + **/ +GList* xaccFreqSpecCompositeGet( FreqSpec *fs ); + +/** + * Adds a FreqSpec to the list in a COMPOSITE FreqSpec; if the + * FreqSpec is not COMPOSITE, this is an assertion failure. + **/ +void xaccFreqSpecCompositeAdd( FreqSpec *fs, FreqSpec *fsToAdd ); + +/** + * Returns the next date which the FreqSpec specifies given the + * previous occurance. Like the relaxed validity check, this doesn't + * take multipliers into account; it just returns the next possible + * occurance after the given day. + * bstanley I think this should be private. + **/ +/* +time_t xaccFreqSpecGetInstanceAfter( FreqSpec *fs, time_t after ); +*/ + +/** + * Returns the next instance of the FreqSpec after a given input date. + * Note that if the given date happens to be a repeat date, + * then the next repeat date will be returned. + **/ +void xaccFreqSpecGetNextInstance( FreqSpec *fs, + const GDate* in_date, + GDate* out_date ); + +/** + * Returns either NULL [valid] or a descriptive string [reason not + * valid] for the given query date. This is "relaxed", in that it + * doesn't care about the frequency multiplier [e.g., For + * WEEKLY[2]:Wed, all Wednesdays are valid; for MONTHLY[12]:16, the + * 16th day of every month is valid. + **/ +/* +char* xaccFreqSpecIsValidDateRelaxed( FreqSpec *fs, time_t query ); +*/ + +/** + * A strict validity check. Given a previous and query date, returns + * NULL if the query date is valid, or a text reason if not. + **/ +/* +char* xaccFreqSpecIsValidDate( FreqSpec *fs, time_t previous, time_t query ); +*/ + +#endif /* XACC_FREQSPEC_H */ diff --git a/src/engine/FreqSpecP.h b/src/engine/FreqSpecP.h new file mode 100644 index 0000000000..6b1c871d12 --- /dev/null +++ b/src/engine/FreqSpecP.h @@ -0,0 +1,112 @@ +/********************************************************************\ + * FreqSpec.h -- Frequency Specification * + * Copyright (C) 2001 Joshua Sled * + * Copyright (C) 2001 Ben Stanley * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/********************************************************************\ +This file contains private definitions and should not be used by +other parts of the engine. This is private data and is subject to +change. +Currently the only files which include this file are: + FreqSpec.c + gnc-freqspec-xml-v2.c +\********************************************************************/ +#ifndef XACC_FREQSPECP_H +#define XACC_FREQSPECP_H + +#include "FreqSpec.h" + +/** + * Scheduled transactions have a frequency defined by a frequency + * specifier. This specifier, given a start date, end date [present + * in the scheduled transaction] and last occurance date [possibly not + * present] can be used to determine that a s.transaction should be + * instantiated on a given date [the given query date]. + * + * There is a split between the UIFreqType and the 'internal' FreqType + * to reduce the complexity of some of the code involved. + * + * Hmmm... having this in the private header file actually prevents + * client code from allocating this 'class' on the stack. + * Is that a problem? + * + * This still needs to deal with: + * . exceptions + * . yearly 360/365? + * . re-based frequencies [based around a non-standard [read: + * not-Jan-1-based/fiscal] year] + * . "business days" -- m-f sans holidays [per-user list thereof] + **/ +struct gncp_freq_spec { + FreqType type; + UIFreqType uift; + union u { + struct { + /** The date on which the single event occurs. */ + GDate date; + } once; + struct { + /** number of days from one repeat to the next. */ + guint interval_days; + /** epoch is defined by glib to be 1/1/1. Offset + measured in days. 0 <= offset < interval */ + guint offset_from_epoch; + } daily; + struct { + /* A week here is measured as 7 days. The first week starts at epoch. + * 1/1/1 was a ?. */ + + /** number of weeks from one repeat to the next. */ + guint interval_weeks; + /* offset measured in days. This combines the week + * offset and the day of the week offset. */ + guint offset_from_epoch; +/* guint offset_from_epoch;*/ /* offset measured in weeks, 0 <= offset < interval */ +/* guint day_of_week;*/ /* I'm not sure what days each value represents, but it's not important. */ + } weekly; + struct { + /** number of months from one repeat to the next. */ + guint interval_months; + /* offset measured in months */ + guint offset_from_epoch; + /* Which day of the month it occurs on. */ + guint day_of_month; + } monthly; + struct { + /** Number of months from one repeat to the next. */ + guint interval_months; + /* offset measured in months */ + guint offset_from_epoch; + /* stores a value equivalent to a GDateWeekday. */ + guint weekday; + /* the 1st occurrence to the 5th occurrence. */ + guint occurrence; + } month_relative; + struct { + /** A list of specs for a composite freq. */ + GList *subSpecs; + } composites; + } s; + GUID guid; +}; + +#endif /* XACC_FREQSPECP_H */ diff --git a/src/engine/GNCId.c b/src/engine/GNCId.c new file mode 100644 index 0000000000..64c5484536 --- /dev/null +++ b/src/engine/GNCId.c @@ -0,0 +1,326 @@ +/********************************************************************\ + * GNCId.c -- Gnucash entity identifier implementation * + * Copyright (C) 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#include "config.h" + +#include +#include + +#include "GNCIdP.h" +#include "gnc-engine-util.h" + + +/** #defines ********************************************************/ +#define GNCID_DEBUG 0 + + +/** Type definitions ************************************************/ +typedef struct entity_node +{ + GNCIdType entity_type; + gpointer entity; +} EntityNode; + + +/** Static global variables *****************************************/ +static GHashTable * entity_table = NULL; +static GMemChunk *guid_memchunk = NULL; +static short module = MOD_ENGINE; + + +/** Function implementations ****************************************/ + +void +xaccGUIDInit (void) +{ + if (!guid_memchunk) + guid_memchunk = g_mem_chunk_create (GUID, 512, G_ALLOC_AND_FREE); +} + +void +xaccGUIDShutdown (void) +{ + if (guid_memchunk) + { + g_mem_chunk_destroy (guid_memchunk); + guid_memchunk = NULL; + } +} + +GUID * +xaccGUIDMalloc (void) +{ + return g_chunk_new (GUID, guid_memchunk); +} + +void +xaccGUIDFree (GUID *guid) +{ + if (!guid) + return; + + g_chunk_free (guid, guid_memchunk); +} + +static gboolean +entity_node_destroy(gpointer key, gpointer value, gpointer not_used) +{ + GUID *guid = key; + EntityNode *e_node = value; + + e_node->entity_type = GNC_ID_NONE; + e_node->entity = NULL; + + xaccGUIDFree(guid); + g_free(e_node); + + return TRUE; +} + +static void +entity_table_destroy (void) +{ + if (entity_table == NULL) + return; + + g_hash_table_foreach_remove(entity_table, entity_node_destroy, NULL); + g_hash_table_destroy(entity_table); + + entity_table = NULL; +} + +static guint +id_hash (gconstpointer key) +{ + const GUID *guid = key; + + if (key == NULL) + return 0; + + if (sizeof(guint) <= 16) + return *((guint *) guid->data); + else + { + guint hash = 0; + int i, j; + + for (i = 0, j = 0; i < sizeof(guint); i++, j++) + { + if (j == 16) + j = 0; + + hash <<= 4; + hash |= guid->data[j]; + } + + return hash; + } +} + +static gboolean +id_compare(gconstpointer key_1, gconstpointer key_2) +{ + return guid_equal (key_1, key_2); +} + +#if GNCID_DEBUG +static void +print_node(gpointer key, gpointer value, gpointer not_used) +{ + GUID *guid = key; + EntityNode *node = value; + + fprintf(stderr, "%s %d %p\n", + guid_to_string(guid), node->entity_type, node->entity); +} + +static void +summarize_table(void) +{ + if (entity_table == NULL) + return; + + g_hash_table_foreach(entity_table, print_node, NULL); +} +#endif + +static void +entity_table_init(void) +{ + if (entity_table != NULL) + entity_table_destroy(); + + entity_table = g_hash_table_new(id_hash, id_compare); + + xaccStoreEntity(NULL, xaccGUIDNULL(), GNC_ID_NULL); + +#if GNCID_DEBUG + atexit(summarize_table); +#endif +} + +GNCIdType +xaccGUIDType(const GUID * guid) +{ + EntityNode *e_node; + GNCIdType entity_type; + + if (guid == NULL) + return GNC_ID_NONE; + + if (entity_table == NULL) + entity_table_init(); + + e_node = g_hash_table_lookup(entity_table, guid->data); + if (e_node == NULL) + return GNC_ID_NONE; + + entity_type = e_node->entity_type; + if ((entity_type <= GNC_ID_NONE) || (entity_type > LAST_GNC_ID)) + return GNC_ID_NONE; + + return entity_type; +} + +void +xaccGUIDNew(GUID *guid) +{ + if (guid == NULL) + return; + + do + { + guid_new(guid); + + if (xaccGUIDType(guid) == GNC_ID_NONE) + break; + + PWARN("duplicate id created, trying again"); + } while(1); +} + +const GUID * +xaccGUIDNULL(void) +{ + static int null_inited = (0 == 1); + static GUID null_guid; + + if (!null_inited) + { + int i; + + for (i = 0; i < 16; i++) + null_guid.data[i] = 0; + + null_inited = (0 == 0); + } + + return &null_guid; +} + +void * +xaccLookupEntity(const GUID * guid, GNCIdType entity_type) +{ + EntityNode *e_node; + + if (guid == NULL) + return NULL; + + if (entity_table == NULL) + entity_table_init(); + + e_node = g_hash_table_lookup(entity_table, guid->data); + if (e_node == NULL) + return NULL; + + if (e_node->entity_type != entity_type) + return NULL; + + return e_node->entity; +} + +void +xaccStoreEntity(void * entity, const GUID * guid, GNCIdType entity_type) +{ + EntityNode *e_node; + GUID *new_guid; + + if (guid == NULL) + return; + + if ((entity_type <= GNC_ID_NONE) || (entity_type > LAST_GNC_ID)) + return; + + if (guid_equal(guid, xaccGUIDNULL())) return; + + xaccRemoveEntity(guid); + + e_node = g_new(EntityNode, 1); + e_node->entity_type = entity_type; + e_node->entity = entity; + + new_guid = xaccGUIDMalloc(); + + if(!new_guid) return; + + *new_guid = *guid; + + g_hash_table_insert(entity_table, new_guid, e_node); +} + +void +xaccRemoveEntity(const GUID * guid) +{ + EntityNode *e_node; + gpointer old_guid; + gpointer node; + + if (guid == NULL) + return; + + if (entity_table == NULL) + entity_table_init(); + + if (g_hash_table_lookup_extended(entity_table, guid, &old_guid, &node)) + { + e_node = node; + if (e_node->entity_type == GNC_ID_NULL) + return; + + g_hash_table_remove(entity_table, old_guid); + entity_node_destroy(old_guid, node, NULL); + } +} + +GHashTable * +xaccGetAndResetEntityTable(void) { + GHashTable *result = entity_table; + entity_table = NULL; + return(result); +} + +void +xaccSetEntityTable(GHashTable *et) { + if(entity_table) entity_table_destroy(); + entity_table = et; +} diff --git a/src/engine/GNCId.h b/src/engine/GNCId.h new file mode 100644 index 0000000000..c4192e7bff --- /dev/null +++ b/src/engine/GNCId.h @@ -0,0 +1,72 @@ +/********************************************************************\ + * GNCId.h -- Gnucash entity identifier API * + * Copyright (C) 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef GNC_ID_H +#define GNC_ID_H 1 + +/* This file defines an API for using gnucash entity identifiers. + * + * Identifiers can be used to reference Accounts, Transactions, and + * Splits. These four Gnucash types are referred to as Gnucash + * entities. Identifiers are globally-unique and permanent, i.e., once + * an entity has been assigned an identifier, it retains that same + * identifier for its lifetime. + * + * Identifiers can be encoded as hex strings. */ + +#include "guid.h" + + +/* Identifiers are 'typed' with integers. The ids used in gnucash are + * defined below. An id with type GNC_ID_NONE does not refer to any + * entity, although that may change as new ids are created. An id with + * type GNC_ID_NULL does not refer to any entity, and will never refer + * to any entity. An identifier with any other type may refer to an + * actual entity, but that is not guaranteed. If an id does refer to + * an entity, the type of the entity will match the type of the + * identifier. */ +typedef enum +{ + GNC_ID_NONE = 0, + GNC_ID_NULL, + GNC_ID_ACCOUNT, + GNC_ID_TRANS, + GNC_ID_SPLIT, + GNC_ID_PRICE, + GNC_ID_SCHEDXACTION, + GNC_ID_FREQSPEC, + LAST_GNC_ID = GNC_ID_FREQSPEC +} GNCIdType; + + +/* Return the type of an identifier. */ +GNCIdType xaccGUIDType (const GUID * guid); + +/* Returns a GUID which is guaranteed to never reference any entity. */ +const GUID * xaccGUIDNULL (void); + +/* Efficiently allocate & free memory for GUIDs */ +GUID * xaccGUIDMalloc (void); +void xaccGUIDFree (GUID *guid); + +#endif diff --git a/src/engine/GNCIdP.h b/src/engine/GNCIdP.h new file mode 100644 index 0000000000..2d1dafb22b --- /dev/null +++ b/src/engine/GNCIdP.h @@ -0,0 +1,57 @@ +/********************************************************************\ + * GNCIdP.h -- Gnucash entity identifier engine-only API * + * Copyright (C) 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef GNC_ID_P_H +#define GNC_ID_P_H 1 + +#include "GNCId.h" + +/* This file defines an engine-only API for using gnucash entity + * identifiers. */ + +/* Generate a new id. This function is guaranteed to return an id that + * is unique within the scope of all GnuCash entities being managed by + * the current invocation of GnuCash. GnuCash routines should always + * use this function and not guid_new! */ +void xaccGUIDNew (GUID *guid); + +/* Lookup an entity given an id and a type. If there is no entity + * associated with the id, or if it has a different type, NULL + * is returned. */ +void * xaccLookupEntity (const GUID * guid, GNCIdType entity_type); + +/* Store the given entity under the given id with the given type. */ +void xaccStoreEntity (void * entity, const GUID * guid, GNCIdType entity_type); + +/* Remove any existing association between an entity and the given + * id. The entity is not changed in any way. */ +void xaccRemoveEntity (const GUID * guid); + +GHashTable *xaccGetAndResetEntityTable (void); +void xaccSetEntityTable (GHashTable *et); + +/* Initialize and shutdown the GNC Id system. */ +void xaccGUIDInit (void); +void xaccGUIDShutdown (void); + +#endif diff --git a/src/engine/Group.c b/src/engine/Group.c new file mode 100644 index 0000000000..289ec2e457 --- /dev/null +++ b/src/engine/Group.c @@ -0,0 +1,1111 @@ +/********************************************************************\ + * Group.c -- chart of accounts (hierarchical tree of accounts) * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997-2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#include "config.h" + +#include +#include + +#include "Account.h" +#include "AccountP.h" +#include "BackendP.h" +#include "GNCIdP.h" +#include "Group.h" +#include "GroupP.h" +#include "TransactionP.h" +#include "gnc-common.h" +#include "gnc-engine-util.h" +#include "gnc-event-p.h" +#include "gnc-numeric.h" + +static short module = MOD_ENGINE; + +/********************************************************************\ + * Because I can't use C++ for this project, doesn't mean that I * + * can't pretend to! These functions perform actions on the * + * AccountGroup data structure, in order to encapsulate the * + * knowledge of the internals of the AccountGroup in one file. * +\********************************************************************/ + +/********************************************************************\ +\********************************************************************/ + +static void +xaccInitializeAccountGroup (AccountGroup *grp) +{ + grp->saved = 1; + + grp->parent = NULL; + grp->accounts = NULL; + + grp->backend = NULL; +} + +/********************************************************************\ +\********************************************************************/ + +AccountGroup * +xaccMallocAccountGroup (void) +{ + AccountGroup *grp = g_new (AccountGroup, 1); + + xaccInitializeAccountGroup (grp); + + return grp; +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccGroupEqual(AccountGroup *ga, + AccountGroup *gb, + gboolean check_guids) +{ + GList *na; + GList *nb; + + if (!ga && !gb) return(TRUE); + if (!ga) return(FALSE); + if (!gb) return(FALSE); + + na = ga->accounts; + nb = gb->accounts; + + if (!na && nb) return(FALSE); + if (na && !nb) return(FALSE); + + while (na && nb) + { + Account *aa = na->data; + Account *ab = nb->data; + + if (!xaccAccountEqual(aa, ab, check_guids)) return(FALSE); + + na = na->next; + nb = nb->next; + } + + if (na) return(FALSE); + if (nb) return(FALSE); + + return(TRUE); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountGroupBeginEdit (AccountGroup *grp) +{ + GList *node; + + if (!grp) return; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + xaccAccountBeginEdit (account); + xaccAccountGroupBeginEdit (account->children); + } +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountGroupCommitEdit (AccountGroup *grp) +{ + GList *node; + + if (!grp) return; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + xaccAccountGroupCommitEdit (account->children); + xaccAccountCommitEdit (account); + } +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccGroupMarkDoFree (AccountGroup *grp) +{ + GList *node; + + if (!grp) return; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + account->do_free = TRUE; + + xaccGroupMarkDoFree (account->children); + } +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccFreeAccountGroup (AccountGroup *grp) +{ + gboolean root_grp; + + if (!grp) return; + + root_grp = grp->parent == NULL; + + if (grp->accounts) + { + Account *account; + /* This is a weird iterator & needs some explanation. + * xaccAccountDestroy() will rip the account out of the list, thus + * iterating while grp->accounts is non-null is enough to iterate + * the loop. But when it deletes the last account, then it will + * also delete the group, unless it's the root group, making the + * grp pointer invalid. So we have to be careful with the last + * deletion: in particular, g_free(grp) would be freeing that + * memory a second time, so don't do it. */ + while (grp->accounts->next) + { + account = grp->accounts->next->data; + xaccAccountBeginEdit (account); + xaccAccountDestroy (account); + } + account = grp->accounts->data; + xaccAccountBeginEdit (account); + xaccAccountDestroy (account); + + if (!root_grp) return; + } + + if (grp->parent) grp->parent->children = NULL; + + grp->parent = NULL; + + g_free (grp); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccGroupMarkSaved (AccountGroup *grp) +{ + GList *node; + + if (!grp) return; + + grp->saved = 1; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + xaccGroupMarkSaved (account->children); + } +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccGroupMarkNotSaved (AccountGroup *grp) +{ + if (!grp) return; + + grp->saved = 0; +} + +/********************************************************************\ +\********************************************************************/ + +gboolean +xaccGroupNotSaved (AccountGroup *grp) +{ + GList *node; + + if (!grp) return FALSE; + + if (grp->saved == 0) return TRUE; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + if (xaccGroupNotSaved (account->children)) + return TRUE; + } + + return FALSE; +} + +/********************************************************************\ + * Get the number of accounts, including subaccounts * +\********************************************************************/ + +int +xaccGroupGetNumSubAccounts (AccountGroup *grp) +{ + GList *node; + int num_acc; + + if (!grp) return 0; + + num_acc = g_list_length (grp->accounts); + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + num_acc += xaccGroupGetNumSubAccounts (account->children); + } + + return num_acc; +} + +/********************************************************************\ + * Get all of the accounts, including subaccounts * +\********************************************************************/ + +static void +xaccPrependAccounts (AccountGroup *grp, GList **accounts_p) +{ + GList *node; + + if (!grp || !accounts_p) return; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + *accounts_p = g_list_prepend (*accounts_p, account); + + xaccPrependAccounts (account->children, accounts_p); + } +} + +GList * +xaccGroupGetSubAccounts (AccountGroup *grp) +{ + GList *accounts = NULL; + + if (!grp) return NULL; + + xaccPrependAccounts (grp, &accounts); + + return g_list_reverse (accounts); +} + +GList * +xaccGroupGetAccountList (AccountGroup *grp) +{ + if (!grp) return NULL; + + return grp->accounts; +} + +/********************************************************************\ + * Fetch the root of the tree * +\********************************************************************/ + +AccountGroup * +xaccGetAccountRoot (Account * acc) +{ + AccountGroup * grp; + AccountGroup * root = NULL; + + if (!acc) return NULL; + + /* find the root of the account group structure */ + grp = acc->parent; + + while (grp) + { + Account *parent_acc; + + root = grp; + parent_acc = grp->parent; + + if (parent_acc) + grp = parent_acc->parent; + else + grp = NULL; + } + + return root; +} + +/********************************************************************\ + * Fetch an account, given its name * +\********************************************************************/ + +Account * +xaccGetAccountFromName (AccountGroup *grp, const char * name) +{ + GList *node; + + if (!grp) return NULL; + if (!name) return NULL; + + /* first, look for accounts hanging off the root */ + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + if (safe_strcmp(xaccAccountGetName (account), name) == 0) + return account; + } + + /* if we are still here, then we haven't found the account yet. + * Recursively search the subgroups next */ + /* first, look for accounts hanging off the root */ + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + Account *acc; + + acc = xaccGetAccountFromName (account->children, name); + if (acc) + return acc; + } + + return NULL; +} + +/********************************************************************\ + * Fetch an account, given its full name * +\********************************************************************/ + +Account * +xaccGetAccountFromFullName (AccountGroup *grp, + const char *name, + const char separator) +{ + GList *node; + Account *found; + char *p; + + if (!grp) return NULL; + if (!name) return NULL; + + p = (char *) name; + found = NULL; + + while (1) + { + /* Look for the first separator. */ + p = strchr(p, separator); + + /* If found, switch it to the null char. */ + if (p != NULL) + *p = 0; + + /* Now look for that name in the children. */ + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + if (safe_strcmp(xaccAccountGetName (account), name) == 0) + { + /* We found an account. + * If p == NULL, there is nothing left + * in the name, so just return the account. + * We don't need to put back the separator, + * because it was never erased (p == NULL). */ + if (p == NULL) + return account; + + /* There's stuff left to search for. + * Search recursively after the separator. */ + found = xaccGetAccountFromFullName(account->children, + p + 1, separator); + + /* If we found the account, break out. */ + if (found != NULL) + break; + } + } + + /* If found != NULL, an account was found. */ + /* If p == NULL, there are no more separators left. */ + + /* Put back the separator. */ + if (p != NULL) + *p = separator; + + /* If we found the account, return it. */ + if (found != NULL) + return found; + + /* We didn't find the account. If there + * are no more separators, return NULL. */ + if (p == NULL) + return NULL; + + /* If we are here, we didn't find anything and there + * must be more separators. So, continue looking with + * a longer name, in case there is a name with the + * separator character in it. */ + p++; + } +} + +/********************************************************************\ + * Fetch an account, given its name * +\********************************************************************/ + +Account * +xaccGetPeerAccountFromName (Account *acc, const char * name) +{ + AccountGroup * root; + Account *peer_acc; + + if (!acc) return NULL; + if (!name) return NULL; + + /* first, find the root of the account group structure */ + root = xaccGetAccountRoot (acc); + + /* now search all accounts hanging off the root */ + peer_acc = xaccGetAccountFromName (root, name); + + return peer_acc; +} + +/********************************************************************\ + * Fetch an account, given its full name * +\********************************************************************/ + +Account * +xaccGetPeerAccountFromFullName (Account *acc, const char * name, + const char separator) +{ + AccountGroup * root; + Account *peer_acc; + + if (!acc) return NULL; + if (!name) return NULL; + + /* first, find the root of the account group structure */ + root = xaccGetAccountRoot (acc); + + /* now search all acounts hanging off the root */ + peer_acc = xaccGetAccountFromFullName (root, name, separator); + + return peer_acc; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountRemoveGroup (Account *acc) +{ + AccountGroup *grp; + + /* if this group has no parent, it must be the topgroup */ + if (!acc) return; + + grp = acc->children; + + if (grp) grp->parent = NULL; + acc->children = NULL; + + /* make sure that the parent of the group is marked + * as having been modified. */ + grp = acc->parent; + if (!grp) return; + + grp->saved = 0; + + gnc_engine_generate_event (&acc->guid, GNC_EVENT_MODIFY); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccGroupRemoveAccount (AccountGroup *grp, Account *acc) +{ + if (!acc) return; + /* this routine might be called on accounts which + * are not yet parented. */ + if (!grp) return; + + if (acc->parent != grp) + { + PERR ("account not in group"); + return; + } + + acc->parent = NULL; + + grp->accounts = g_list_remove (grp->accounts, acc); + + grp->saved = 0; + + /* if this was the last account in a group, delete + * the group as well (unless its a root group) */ + if ((grp->accounts == NULL) && (grp->parent)) + { + xaccAccountRemoveGroup (grp->parent); + xaccFreeAccountGroup (grp); + } + + gnc_engine_generate_event (&acc->guid, GNC_EVENT_MODIFY); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccAccountInsertSubAccount (Account *adult, Account *child) +{ + if (!adult) return; + + /* if a container for the children doesn't yet exist, add it */ + if (adult->children == NULL) + adult->children = xaccMallocAccountGroup (); + + /* set back-pointer to parent */ + adult->children->parent = adult; + + /* allow side-effect of creating a child-less account group */ + if (!child) return; + + xaccGroupInsertAccount (adult->children, child); + + gnc_engine_generate_event (&adult->guid, GNC_EVENT_MODIFY); +} + +/********************************************************************\ +\********************************************************************/ + +static int +group_sort_helper (gconstpointer a, gconstpointer b) +{ + Account *aa = (Account *) a; + Account *bb = (Account *) b; + + /* return > 1 if aa should come after bb */ + return xaccAccountOrder (&aa, &bb); +} + +void +xaccGroupInsertAccount (AccountGroup *grp, Account *acc) +{ + if (!grp) return; + if (!acc) return; + + /* If the account is currently in another group, remove it there + * first. Basically, we can't have accounts being in two places at + * once. If old and new parents are the same, reinsertion causes + * the sort order to be checked. */ + if (acc->parent == grp) + { + grp->accounts = g_list_sort (grp->accounts, group_sort_helper); + } + else + { + if (acc->parent) + xaccGroupRemoveAccount (acc->parent, acc); + + /* set back-pointer to the account's parent */ + acc->parent = grp; + + grp->accounts = g_list_insert_sorted (grp->accounts, acc, + group_sort_helper); + } + + grp->saved = 0; + + gnc_engine_generate_event (&acc->guid, GNC_EVENT_MODIFY); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccGroupConcatGroup (AccountGroup *togrp, AccountGroup *fromgrp) +{ + if (!togrp) return; + if (!fromgrp) return; + + /* The act of inserting the account into togrp also causes it to + * automatically be deleted from fromgrp. Be careful! */ + + while (TRUE) + { + Account *account; + GList *accounts; + GList *next; + + accounts = fromgrp->accounts; + if (!accounts) + return; + + next = accounts->next; + + account = accounts->data; + + xaccGroupInsertAccount (togrp, account); + + if (!next) + return; + } +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccGroupMergeAccounts (AccountGroup *grp) +{ + GList *node_a; + GList *node_b; + + if (!grp) return; + + for (node_a = grp->accounts; node_a; node_a = node_a->next) + { + Account *acc_a = node_a->data; + + for (node_b = node_a->next; node_b; node_b = node_b->next) + { + Account *acc_b = node_b->data; + + if ((0 == safe_strcmp(xaccAccountGetName(acc_a), + xaccAccountGetName(acc_b))) && + (0 == safe_strcmp(xaccAccountGetCode(acc_a), + xaccAccountGetCode(acc_b))) && + (0 == safe_strcmp(xaccAccountGetDescription(acc_a), + xaccAccountGetDescription(acc_b))) && + (gnc_commodity_equiv(xaccAccountGetCommodity(acc_a), + xaccAccountGetCommodity(acc_b))) && + (0 == safe_strcmp(xaccAccountGetNotes(acc_a), + xaccAccountGetNotes(acc_b))) && + (xaccAccountGetType(acc_a) == xaccAccountGetType(acc_b))) + { + AccountGroup *ga, *gb; + GList *lp; + + /* consolidate children */ + ga = (AccountGroup *) acc_a->children; + gb = (AccountGroup *) acc_b->children; + + if (gb) + { + if (!ga) + { + acc_a->children = gb; + gb->parent = acc_a; + acc_b->children = NULL; + + gnc_engine_generate_event (&acc_a->guid, GNC_EVENT_MODIFY); + gnc_engine_generate_event (&acc_b->guid, GNC_EVENT_MODIFY); + } + else + { + xaccGroupConcatGroup (ga, gb); + acc_b->children = NULL; + gnc_engine_generate_event (&acc_b->guid, GNC_EVENT_MODIFY); + } + } + + /* recurse to do the children's children */ + xaccGroupMergeAccounts (ga); + + /* consolidate transactions */ + lp = acc_b->splits; + + for (lp = acc_b->splits; lp; lp = lp->next) + { + Split *split = lp->data; + + gnc_engine_generate_event (&xaccSplitGetAccount(split)->guid, + GNC_EVENT_MODIFY); + xaccSplitSetAccount(split, NULL); + xaccAccountInsertSplit (acc_a, split); + } + + g_list_free(acc_b->splits); + acc_b->splits = NULL; + + /* move back one before removal */ + node_b = node_b->prev; + + /* remove from list -- node_a is ok, it's before node_b */ + grp->accounts = g_list_remove (grp->accounts, acc_b); + + xaccAccountBeginEdit (acc_b); + xaccAccountDestroy (acc_b); + break; + } + } + } +} + +/********************************************************************\ +\********************************************************************/ + +int +xaccGroupGetNumAccounts (AccountGroup *grp) +{ + if (!grp) return 0; + + return g_list_length (grp->accounts); +} + +Account * +xaccGroupGetAccount (AccountGroup *grp, int i) +{ + if (!grp) return NULL; + + return g_list_nth_data (grp->accounts, i); +} + +Account * +xaccGroupGetParentAccount (AccountGroup * grp) +{ + if (!grp) return NULL; + + return grp->parent; +} + +/********************************************************************\ +\********************************************************************/ + +int +xaccGroupGetDepth (AccountGroup *grp) +{ + GList *node; + int depth = 0; + int maxdepth = 0; + + if (!grp) return 0; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + + depth = xaccGroupGetDepth (account->children); + + if (depth > maxdepth) + maxdepth = depth; + } + + maxdepth++; + + return maxdepth; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccSplitsBeginStagedTransactionTraversals (GList *splits) +{ + GList *lp; + + for (lp = splits; lp; lp = lp->next) + { + Split *s = lp->data; + Transaction *trans = s->parent; + + if (trans) + trans->marker = 0; + } +} + +void +xaccAccountBeginStagedTransactionTraversals (Account *account) +{ + if (account == NULL) return; + xaccSplitsBeginStagedTransactionTraversals (account->splits); +} + +gboolean +xaccTransactionTraverse (Transaction *trans, int stage) +{ + if (trans == NULL) return FALSE; + + if (trans->marker < stage) + { + trans->marker = stage; + return TRUE; + } + + return FALSE; +} + +gboolean +xaccSplitTransactionTraverse (Split *split, int stage) +{ + if (split == NULL) return FALSE; + + return xaccTransactionTraverse (split->parent, stage); +} + +void +xaccGroupBeginStagedTransactionTraversals (AccountGroup *grp) +{ + GList *node; + + if (!grp) return; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + GList *lp; + + /* recursively do sub-accounts */ + xaccGroupBeginStagedTransactionTraversals (account->children); + + for (lp = account->splits; lp; lp = lp->next) + { + Split *s = lp->data; + Transaction *trans = s->parent; + trans->marker = 0; + } + } +} + +int +xaccAccountStagedTransactionTraversal (Account *acc, + unsigned int stage, + int (*callback)(Transaction *t, + void *cb_data), + void *cb_data) +{ + if (!acc) return 0; + + if (callback) + { + GList *lp; + for(lp = acc->splits; lp; lp = lp->next) + { + Split *s = (Split *) lp->data; + Transaction *trans = s->parent; + if (trans && (trans->marker < stage)) + { + int retval; + trans->marker = stage; + retval = callback(trans, cb_data); + if (retval) return retval; + } + } + } + else + { + GList *lp; + for(lp = acc->splits; lp; lp = lp->next) + { + Split *s = (Split *) lp->data; + Transaction *trans = s->parent; + if (trans && (trans->marker < stage)) + trans->marker = stage; + } + } + + return 0; +} + +int +xaccGroupStagedTransactionTraversal (AccountGroup *grp, + unsigned int stage, + int (*callback)(Transaction *t, + void *cb_data), + void *cb_data) +{ + GList *node; + + if (!grp) return 0; + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + int retval; + + /* recursively do sub-accounts */ + retval = xaccGroupStagedTransactionTraversal (account->children, stage, + callback, cb_data); + if (retval) return retval; + + retval = xaccAccountStagedTransactionTraversal (account, stage, + callback, cb_data); + if (retval) return retval; + } + + return 0; +} + +/********************************************************************\ +\********************************************************************/ + +struct group_visit_data +{ + gboolean (*proc)(Transaction *t, void *data); + void *up_data; + GHashTable *visit_table; +}; + +static gboolean +xaccGroupVisitUnvisitedTransactions_thunk(Transaction *trn, + void *data) +{ + gpointer test_trn; + struct group_visit_data *grdata = (struct group_visit_data*)data; + + test_trn = g_hash_table_lookup(grdata->visit_table, trn); + + if(!test_trn) + { + g_hash_table_insert(grdata->visit_table, trn, ""); + + grdata->proc(trn, grdata->up_data); + } + + return TRUE; +} + +gboolean +xaccGroupVisitUnvisitedTransactions (AccountGroup *g, + gboolean (*proc)(Transaction *t, + void *data), + void *data, + GHashTable *visited_txns) +{ + gboolean keep_going = TRUE; + GList *list; + GList *node; + struct group_visit_data grdata; + + if (!g) return(FALSE); + if (!proc) return(FALSE); + if (!visited_txns) return(FALSE); + + list = xaccGroupGetSubAccounts (g); + + grdata.proc = proc; + grdata.up_data = data; + grdata.visit_table = visited_txns; + + for (node = list; node && keep_going; node = node->next) + { + Account *account = node->data; + + keep_going = xaccAccountForEachTransaction( + account, xaccGroupVisitUnvisitedTransactions_thunk, (void*)&grdata); + } + + g_list_free (list); + + return(keep_going); +} + +gboolean +xaccGroupForEachTransaction (AccountGroup *g, + gboolean (*proc)(Transaction *t, void *data), + void *data) +{ + GHashTable *visited_txns = NULL; + gboolean result = FALSE; + + if (!g) return(FALSE); + if (!proc) return(FALSE); + + visited_txns = guid_hash_table_new(); + if (visited_txns) + result = xaccGroupVisitUnvisitedTransactions(g, proc, data, visited_txns); + + /* cleanup */ + if (visited_txns) + g_hash_table_destroy(visited_txns); + + return(result); +} + +/********************************************************************\ +\********************************************************************/ + +GSList * +xaccGroupMapAccounts (AccountGroup *grp, + gpointer (*thunk)(Account *a, gpointer data), + gpointer data) +{ + GSList *result = NULL; + GList *node; + + if (!grp) return(NULL); + if (!thunk) return(NULL); + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + gpointer thunk_result = thunk (account, data); + + if (thunk_result) + result = g_slist_prepend (result, thunk_result); + } + + return(g_slist_reverse (result)); +} + +gpointer +xaccGroupForEachAccount (AccountGroup *grp, + gpointer (*thunk)(Account *a, gpointer data), + gpointer data, + gboolean deeply) +{ + GList *node; + + if (!grp) return(NULL); + if (!thunk) return(NULL); + + for (node = grp->accounts; node; node = node->next) + { + Account *account = node->data; + gpointer result = thunk (account, data); + + if (result) + return(result); + + if(deeply) + result = xaccGroupForEachAccount (account->children, + thunk, data, TRUE); + + if (result) + return(result); + } + + return(NULL); +} diff --git a/src/engine/Group.h b/src/engine/Group.h new file mode 100644 index 0000000000..22af9cfa90 --- /dev/null +++ b/src/engine/Group.h @@ -0,0 +1,327 @@ +/********************************************************************\ + * Group.h -- chart of accounts (hierarchical tree of accounts) * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997, 1998, 1999, 2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef XACC_ACCOUNT_GROUP_H +#define XACC_ACCOUNT_GROUP_H + +#include "config.h" + +#include + +#include "Account.h" +#include "GNCId.h" +#include "gnc-common.h" + + +/** PROTOTYPES ******************************************************/ +AccountGroup *xaccMallocAccountGroup (void); +void xaccFreeAccountGroup (AccountGroup *account_group); +void xaccAccountGroupBeginEdit (AccountGroup *grp); +void xaccAccountGroupCommitEdit (AccountGroup *grp); + +/* + * The xaccGroupConcatGroup() subroutine will move all accounts + * from the "from" group to the "to" group + * + * The xaccGroupMergeAccounts() subroutine will go through a group, + * merging all accounts that have the same name and description. + * This function is useful when importing Quicken(TM) files. + */ + +void xaccGroupConcatGroup (AccountGroup *to, AccountGroup *from); +void xaccGroupMergeAccounts (AccountGroup *grp); + +/* + * The xaccGroupNotSaved() subroutine will return TRUE + * if any account in the group or in any subgroup + * hasn't been saved. + * + * The xaccGroupMarkSaved() subroutine will mark + * the entire group as having been saved, including + * all of the child accounts. + * + * The xaccGroupMarkNotSaved() subroutine will mark + * the given group as not having been saved. + * + * The xaccGroupMarkDoFree() subroutine will mark + * all accounts in the group as being destroyed. + */ + +gboolean xaccGroupNotSaved (AccountGroup *grp); +void xaccGroupMarkSaved (AccountGroup *grp); +void xaccGroupMarkNotSaved (AccountGroup *grp); + +void xaccGroupMarkDoFree (AccountGroup *grp); + +/* + * The xaccGroupRemoveAccount() subroutine will remove the indicated + * account from its parent account group. It will NOT free the + * associated memory or otherwise alter the account: the account + * can now be reparented to a new location. + * Note, however, that it will mark the old parents as having + * been modified. + * + * The xaccAccountRemoveGroup() subroutine will remove the indicated + * account group from its parent account. It will NOT free the + * associated memory or otherwise alter the account group: the + * account group can now be reparented to a new location. + * Note, however, that it will mark the old parents as having + * been modified. + */ + +void xaccGroupRemoveAccount (AccountGroup *grp, Account *account); +void xaccAccountRemoveGroup (Account *acc); +void xaccGroupInsertAccount (AccountGroup *grp, Account *acc); +void xaccAccountInsertSubAccount (Account *parent, Account *child); + +/* + * The xaccGroupGetNumSubAccounts() subroutine returns the number + * of accounts, including subaccounts, in the account group + * + * The xaccGroupGetNumAccounts() subroutine returns the number + * of accounts in the indicated group only (children not counted). + * + * The xaccGroupGetDepth() subroutine returns the length of the + * longest tree branch. Each link between an account and its + * (non-null) children counts as one unit of length. + */ + +int xaccGroupGetNumSubAccounts (AccountGroup *grp); +int xaccGroupGetNumAccounts (AccountGroup *grp); +int xaccGroupGetDepth (AccountGroup *grp); +Account * xaccGroupGetAccount (AccountGroup *group, int index); + +/* + * The xaccGroupGetSubAccounts() subroutine returns an list of the accounts, + * including subaccounts, in the account group. The returned list + * should be freed with g_list_free when no longer needed. + * + * The xaccGroupGetAccountList() subroutines returns only the immediate + * children of the account group. The returned list should *not* + * be freed by the caller. + */ + +GList * xaccGroupGetSubAccounts (AccountGroup *grp); +GList * xaccGroupGetAccountList (AccountGroup *grp); + +/* + * The xaccGetAccountFromName() subroutine fetches the + * account by name from the collection of accounts + * in the indicated AccountGroup group. It returns NULL if the + * account was not found. + * + * The xaccGetAccountFromFullName() subroutine works like + * xaccGetAccountFromName, but uses fully-qualified names + * using the given separator. + * + * The xaccGetPeerAccountFromName() subroutine fetches the + * account by name from the collection of accounts + * in the same AccountGroup anchor group. It returns NULL if the + * account was not found. + * + * The xaccGetPeerAccountFromFullName() subroutine works like + * xaccGetPeerAccountFromName, but uses fully-qualified + * names using the given separator. + */ + +Account *xaccGetAccountFromName (AccountGroup *group, const char *name); +Account *xaccGetAccountFromFullName (AccountGroup *group, + const char *name, + const char separator); +Account *xaccGetPeerAccountFromName (Account *account, const char *name); +Account *xaccGetPeerAccountFromFullName (Account *acc, + const char * name, + const char separator); + +/* + * The xaccGetAccountRoot () subroutine will find the topmost + * (root) group to which this account belongs. + */ +AccountGroup * xaccGetAccountRoot (Account *account); + +/* The xaccGroupGetParentAccount() subroutine returns the parent + * account of the group, or NULL. + */ +Account * xaccGroupGetParentAccount (AccountGroup *group); + +/* if the function returns null for a given item, it won't show up in + the result list */ +typedef gpointer (*AccountCallback)(Account *a, gpointer data); +GSList *xaccGroupMapAccounts(AccountGroup *grp, + AccountCallback, + gpointer data); + +/* The xaccGroupForEachAccount() method will traverse the AccountGroup + * tree, calling 'func' on each account. Traversal will stop when + * func returns a non-null value, and the routine wil return with that + * value. If 'deeply' is FALSE, then only the immediate children of + * the account will be traversed. If TRUE, then the whole tree will + * be traversed. + */ + +gpointer xaccGroupForEachAccount (AccountGroup *grp, + AccountCallback, + gpointer data, + gboolean deeply); + +gboolean xaccGroupEqual(AccountGroup *a, AccountGroup *b, + gboolean check_guids); + +/* + * The following functions provide support for "staged traversals" + * over all of the transactions in an account or group. The idea + * is to be able to perform a sequence of traversals ("stages"), + * and perform an operation on each transaction exactly once + * for that stage. + * + * Only transactions whose current "stage" is less than the + * stage of the current traversal will be affected, and they will + * be "brought up" to the current stage when they are processed. + * + * For example, you could perform a stage 1 traversal of all the + * transactions in an account, and then perform a stage 1 traversal of + * the transactions in a second account. Presuming the traversal of + * the first account didn't abort prematurely, any transactions shared + * by both accounts would be ignored during the traversal of the + * second account since they had been processed while traversing the + * first account. + * + * However, if you had traversed the second account using a stage + * of 2, then all the transactions in the second account would have + * been processed. + * + * Traversal can be aborted by having the callback function return + * a non-zero value. The traversal is aborted immediately, and the + * non-zero value is returned. Note that an aborted traversal can + * be restarted; no information is lost due to an abort. + * + * The initial impetus for this particular approach came from + * generalizing a mark/sweep practice that was already being + * used in FileIO.c. + * + * Note that currently, there is a hard limit of 256 stages, which + * can be changed by enlarging "marker" in the transaction struct. + * */ + +/* xaccGroupBeginStagedTransactionTraversals() resets the traversal + * marker inside each of all the transactions in the group so that + * a new sequence of staged traversals can begin. + * + * xaccSplitsBeginStagedTransactionTraversals() resets the traversal + * marker for each transaction which is a parent of one of the + * splits in the list. + * + * xaccAccountBeginStagedTransactionTraversals() resets the traversal + * marker for each transaction which is a parent of one of the + * splits in the account. + * + */ + +void xaccGroupBeginStagedTransactionTraversals(AccountGroup *grp); +void xaccSplitsBeginStagedTransactionTraversals(GList *splits); +void xaccAccountBeginStagedTransactionTraversals(Account *account); + +/* xaccTransactionTraverse() checks the stage of the given transaction. + * If the transaction hasn't reached the given stage, the transaction + * is updated to that stage and the function returns TRUE. Otherwise + * no change is made and the function returns FALSE. + * + * xaccSplitTransactionTraverse() behaves as above using the parent of + * the given split. + */ + +gboolean xaccTransactionTraverse(Transaction *trans, int stage); +gboolean xaccSplitTransactionTraverse(Split *split, int stage); + +/* xaccGroupStagedTransactionTraversal() calls thunk on each + * transaction in the group whose current marker is less than the + * given `stage' and updates each transaction's marker to be `stage'. + * The traversal will stop if thunk() returns a non-zero value. + * xaccGroupStagedTransactionTraversal() function will return zero + * or the non-zero value returned by thunk(). This + * API does not handle handle recursive traversals. + * + * Currently the result of adding or removing transactions during + * a traversal is undefined, so don't do that. + */ + +typedef int (*TransactionCallbackInt)(Transaction *t, void *data); +int xaccGroupStagedTransactionTraversal(AccountGroup *grp, + unsigned int stage, + TransactionCallbackInt, + void *data); + +/* xaccAccountStagedTransactionTraversal() calls thunk on each + * transaction in the account whose current marker is less than the + * given `stage' and updates each transaction's marker to be `stage'. + * The traversal will stop if thunk() returns a non-zero value. + * xaccAccountStagedTransactionTraversal() function will return zero + * or the non-zero value returned by thunk(). + * This API does not handle handle recursive traversals. + * + * Currently the result of adding or removing transactions during + * a traversal is undefined, so don't do that. + */ + +int xaccAccountStagedTransactionTraversal(Account *a, + unsigned int stage, + TransactionCallbackInt, + void *data); + +/* Traverse all of the transactions in the given account group. + Continue processing IFF proc does not return FALSE. This function + will descend recursively to traverse transactions in the + children of the accounts in the group. + + Proc will be called exactly once for each transaction that is + pointed to by at least one split in any account in the hierarchy + topped by AccountGroup g. + + Note too, that if you call this function on two separate account + groups and those accounts groups share transactions, proc will be + called once per account on the shared transactions. + + The result of this function will not be FALSE IFF every relevant + transaction was traversed exactly once. */ +gboolean +xaccGroupForEachTransaction(AccountGroup *g, + TransactionCallback, + void *data); + +/* Visit every transaction in the account that hasn't already been + visited exactly once. visited_txns must be a hash table created + via guid_hash_table_new() and is the authority about which + transactions have already been visited. Further, when this + procedure returns, visited_txns will have been modified to reflect + all the newly visited transactions. + + The result of this function will not be FALSE IFF every relevant + transaction was traversed exactly once. */ +gboolean +xaccGroupVisitUnvisitedTransactions(AccountGroup *g, + TransactionCallback, + void *data, + GHashTable *visited_txns); + +#endif /* XACC_ACCOUNT_GROUP_H */ diff --git a/src/engine/GroupP.h b/src/engine/GroupP.h new file mode 100644 index 0000000000..18f3455493 --- /dev/null +++ b/src/engine/GroupP.h @@ -0,0 +1,60 @@ +/********************************************************************\ + * GroupP.h -- private header file for chart of accounts * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997, 1998, 1999, 2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * GroupP.h + * + * FUNCTION: + * This is the *private* account group structure. + * This header should *not* be included by any code outside of the + * engine. + * + */ + +#ifndef XACC_ACCOUNT_GROUP_P_H +#define XACC_ACCOUNT_GROUP_P_H + +#include "config.h" + +#include "BackendP.h" +#include "GNCId.h" +#include "Transaction.h" +#include "gnc-numeric.h" + + +/** STRUCTS *********************************************************/ +struct _account_group +{ + /* The flags: */ + unsigned int saved : 1; + + Account *parent; /* back-pointer to parent */ + + GList *accounts; /* list of account pointers */ + + Backend *backend; /* persistant storage backend */ +}; + +#endif /* XACC_ACCOUNT_GROUP_P_H */ diff --git a/src/engine/NetIO.c b/src/engine/NetIO.c new file mode 100644 index 0000000000..24688f9d35 --- /dev/null +++ b/src/engine/NetIO.c @@ -0,0 +1,326 @@ +/********************************************************************\ + * NetIO.c -- read and write network IO * + * Copyright (C) 2001 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * Rudimentary implmentation right now; good enough for a demo, + * but that's all. + * + * HACK ALERT -- this should be moved into its own subdirectory + * Mostly so that the engine build doesn't require libghttp + * as a dependency. In general, backends should be dynamically + * loaded ... + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "BackendP.h" +#include "NetIO.h" +#include "gnc-book.h" +#include "gnc-engine-util.h" +#include "io-gncxml.h" + +static short module = MOD_BACKEND; + +typedef struct _xmlend XMLBackend; + +struct _xmlend { + Backend be; + + ghttp_request *request; + char * query_url; + char * auth_cookie; +}; + +Backend *xmlendNew (void); + + +#if 0 + +/* ==================================================================== */ +/* Perform various validty checks on the reply: + * -- was the content type text/gnc-xml ? + * -- was there a reply body, of positive length? + * -- did the body appear to contain gnc xml data? + * + * Also, if the reply contained a set-cookie command, process that. + */ + +static int +check_response (XMLBackend *be) +{ + ghttp_request *request; + const char *bufp; + int len; + + request = be->request; + + /* get the content type out of the header */ + bufp = ghttp_get_header(request, "Content-Type"); + PINFO ("Content-Type: %s", bufp ? bufp : "(null)"); + + /* in principle, we should reject content that isn't + * labelled as text/gnc-xml. But for now, we'll be soft ... + */ + if (strncmp (bufp, "text/gnc-xml", 12)) + { + PWARN ("content type is incorrectly labelled as %s", bufp); + be->be.last_err = ERR_NETIO_WRONG_CONTENT_TYPE; + // return 0; + } + + len = ghttp_get_body_len(request); + PINFO ("reply length=%d\n", len); + + /* body length must be postive */ + if (0 >= len) + { + const char * errstr = ghttp_get_error (request); + const char * reason = ghttp_reason_phrase (request); + PERR ("connection failed: %s %s\n", + errstr ? errstr : "", + reason ? reason : ""); + + be->be.last_err = ERR_NETIO_SHORT_READ; + return len; + } + + bufp = ghttp_get_body(request); + g_return_val_if_fail (bufp, 0); + DEBUG ("%s\n", bufp); + + /* skip paste whitespace */ + bufp += strspn (bufp, " \t\f\n\r\v\b"); + + /* see if this really appears to be gnc-xml content ... */ + if (strncmp (bufp, "be.last_err = ERR_NETIO_NOT_GNCXML; + return 0; + } + + /* if we got to here, the response looks good. + * if there is a cookie in the header, obey it + */ + bufp = ghttp_get_header(request, "Set-Cookie"); + if (bufp) + { + if (be->auth_cookie) g_free (be->auth_cookie); + be->auth_cookie = g_strdup (bufp); + } + + /* must be good */ + return len; +} + +/* ==================================================================== */ + +static void +setup_request (XMLBackend *be) +{ + ghttp_request *request = be->request; + + /* clean is needed to clear out old request bodies, headers, etc. */ + ghttp_clean (request); + ghttp_set_header (be->request, http_hdr_Connection, "close"); + ghttp_set_header (be->request, http_hdr_User_Agent, + PACKAGE "/" VERSION + " (Financial Browser for Linux; http://gnucash.org)"); + ghttp_set_sync (be->request, ghttp_sync); + + if (be->auth_cookie) { + ghttp_set_header (request, "Cookie", be->auth_cookie); + } +} + +/* ==================================================================== */ +/* Initialize the connection ... ?? */ +/* validate the URL for syntactic correctness ?? */ +/* maybe check to see if the server is reachable? */ + +static void +xmlbeBookBegin (GNCBook *book, const char *url) +{ + XMLBackend *be; + + if (!book || !url) return; + + ENTER ("url is %s", url); + + be = (XMLBackend *) xaccGNCBookGetBackend (book); + + /* hack alert -- we store this first url as the url for inital + * contact & * for sending queries to + * this should be made customizable, I suppose ???? */ + be->query_url = g_strdup (url); + + LEAVE(" "); +} + +/* ==================================================================== */ +/* Load a set of accounts and currencies from the indicated URL. */ + +static AccountGroup * +xmlbeBookLoad (Backend *bend) +{ + XMLBackend *be = (XMLBackend *) bend; + AccountGroup *grp; + ghttp_request *request; + const char *bufp; + int len; + + if (!be) return NULL; + + /* build up a request for the URL */ + setup_request (be); + request = be->request; + ghttp_set_uri (request, be->query_url); + ghttp_set_type (request, ghttp_type_get); + ghttp_prepare (request); + ghttp_process (request); + + /* perform various error and validity checking on the response */ + len = check_response (be); + if (0 >= len) return NULL; + + bufp = ghttp_get_body(request); + + grp = gnc_book_load_from_buf ((char *)bufp, len); + + LEAVE(" "); + return grp; +} + +/* ==================================================================== */ + +static void +xmlbeRunQuery (Backend *b, Query *q) +{ + XMLBackend *be = (XMLBackend *) b; + AccountGroup *grp, *reply_grp; + ghttp_request *request; + char *bufp; + int len; + + if (!be || !q) return; + ENTER ("be=%p q=%p", b, q); + + /* set up a new http request, of type POST */ + setup_request (be); + request = be->request; + ghttp_set_uri (request, be->query_url); + ghttp_set_type (request, ghttp_type_post); + + /* convert the query to XML */ + gncxml_write_query_to_buf (q, &bufp, &len); + + /* put the XML into the request body */ + ghttp_set_body (request, bufp, len); + + /* send it off the the webserver, wait for the reply */ + ghttp_prepare (request); + ghttp_process (request); + + /* free the query xml */ + free (bufp); + + /* perform various error and validity checking on the response */ + len = check_response (be); + if (0 >= len) return; + + /* we get back a list of splits */ + bufp = ghttp_get_body(request); + reply_grp = gncxml_read_from_buf (bufp, len); + + /* merge the splits into our local cache */ + grp = xaccQueryGetGroup (q); + xaccGroupConcatGroup (grp, reply_grp); + xaccGroupMergeAccounts (grp); + xaccFreeAccountGroup (reply_grp); + + LEAVE(" "); + return; +} + +/* ==================================================================== */ + +static void +xmlbeBookEnd (Backend *b) +{ + XMLBackend *be = (XMLBackend *) b; + + ghttp_request_destroy (be->request); + g_free (be->query_url); +} + +/* ==================================================================== */ + +#endif + +Backend * +xmlendNew (void) +{ +#if 0 + XMLBackend *be; + + be = (XMLBackend *) malloc (sizeof (XMLBackend)); + + /* generic backend handlers */ + be->be.book_begin = NULL; + be->be.book_load = xmlbeBookLoad; + be->be.price_load = NULL; + be->be.book_end = xmlbeBookEnd; + + be->be.account_begin_edit = NULL; + be->be.account_commit_edit = NULL; + be->be.trans_begin_edit = NULL; + be->be.trans_commit_edit = NULL; + be->be.trans_rollback_edit = NULL; + be->be.price_begin_edit = NULL; + be->be.price_commit_edit = NULL; + be->be.run_query = xmlbeRunQuery; + be->be.price_lookup = NULL; + be->be.sync = NULL; + be->be.sync_price = NULL; + be->be.events_pending = NULL; + be->be.process_events = NULL; + + be->be.last_err = ERR_BACKEND_NO_ERR; + + be->request = ghttp_request_new(); + be->auth_cookie = NULL; + + be->query_url = NULL; + + return (Backend *) be; +#endif + return NULL; +} + +/* ============================== END OF FILE ======================== */ diff --git a/src/engine/NetIO.h b/src/engine/NetIO.h new file mode 100644 index 0000000000..85fd08b2dc --- /dev/null +++ b/src/engine/NetIO.h @@ -0,0 +1,32 @@ +/********************************************************************\ + * NetIO.h -- read and write network IO * + * Copyright (C) 2001 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + + +#ifndef XACC_NET_IO_H +#define XACC_NET_IO_H + +#include "BackendP.h" + +Backend * xmlendNew (void); + +#endif /* XACC_NET_IO_H */ diff --git a/src/engine/Query.c b/src/engine/Query.c new file mode 100644 index 0000000000..0d7e420dc1 --- /dev/null +++ b/src/engine/Query.c @@ -0,0 +1,2794 @@ +/********************************************************************\ + * Query.c : api for finding transactions * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * +\********************************************************************/ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "gnc-common.h" +#include "gnc-engine-util.h" +#include "gnc-numeric.h" +#include "Account.h" +#include "BackendP.h" +#include "GNCId.h" +#include "Group.h" +#include "Query.h" +#include "Transaction.h" +#include "TransactionP.h" + +static short module = MOD_QUERY; + +/* the Query makes a subset of all splits based on 3 things: + * - an AND-OR tree of predicates which combine to make a + * split filter + * - a sorting order for the matched splits + * - a chop limit which gives the maximum number of sorted + * splits to return. */ + +struct _querystruct { + /* terms is a list of the OR-terms in a sum-of-products + * logical expression. */ + GList * terms; + + /* sorting and chopping is independent of the search filter */ + sort_type_t primary_sort; + sort_type_t secondary_sort; + sort_type_t tertiary_sort; + gboolean primary_increasing; + gboolean secondary_increasing; + gboolean tertiary_increasing; + int max_splits; + + /* cache the results so we don't have to run the whole search + * again until it's really necessary */ + int changed; + query_run_t last_run_type; + AccountGroup * acct_group; + + GList * split_list; + GList * xtn_list; +}; + +/******************************************************************* + * predicates for standard match types + *******************************************************************/ + +static int xaccAccountMatchPredicate(Split * s, PredicateData * pd); +static int xaccActionMatchPredicate(Split * s, PredicateData * pd); +static int xaccAmountMatchPredicate(Split * s, PredicateData * pd); +static int xaccBalanceMatchPredicate(Split * s, PredicateData * pd); +static int xaccClearedMatchPredicate(Split * s, PredicateData * pd); +static int xaccDateMatchPredicate(Split * s, PredicateData * pd); +static int xaccDescriptionMatchPredicate(Split * s, PredicateData * pd); +static int xaccGUIDMatchPredicate(Split * s, PredicateData * pd); +static int xaccMemoMatchPredicate(Split * s, PredicateData * pd); +static int xaccNumberMatchPredicate(Split * s, PredicateData * pd); +static int xaccSharePriceMatchPredicate(Split * s, PredicateData * pd); +static int xaccSharesMatchPredicate(Split * s, PredicateData * pd); + +/******************************************************************** + ********************************************************************/ + + +void +xaccQueryPrint(Query * q) +{ + GList * aterms; + GList * i, * j; + QueryTerm * qt; + + if (!q) + { + printf("Query: null\n"); + return; + } + + printf("Query: max splits = %d\n", q->max_splits); + + /* print and & or terms */ + for(i=q->terms; i; i=i->next) { + aterms = i->data; + printf("("); + for(j=aterms; j; j=j->next) { + qt = (QueryTerm *)j->data; + if(!qt->data.base.sense) printf("~"); + printf("%d ", qt->data.type); + } + printf(")"); + if(i->next) { + printf(" | "); + } + } + printf("\n"); + + /* print the node contents */ + for(i=q->terms; i; i=i->next) { + aterms = i->data; + printf("aterm=%p\n", aterms); + for(j=aterms; j; j=j->next) { + qt = (QueryTerm *)j->data; + switch (qt->data.base.term_type) + { + case PR_ACCOUNT: { + GList *p; + char buff[40]; + printf ("account sense=%d how=%d\n", + qt->data.base.sense, + qt->data.acct.how); + for (p=qt->data.acct.account_guids; p; p=p->next) { + guid_to_string_buff (p->data, buff); + printf ("\tguid=%s\n", buff); + } + for (p=qt->data.acct.accounts; p; p=p->next) { + printf ("\tacct ptr=%p\n", p->data); + } + break; + } + case PR_ACTION: + printf ("action sense=%d case sensitive=%d\n", qt->data.str.sense, qt->data.str.case_sens); + printf ("\tmatch string=%s \n", qt->data.str.matchstring); + break; + case PR_AMOUNT: + printf ("amount sense=%d how=%d\n", qt->data.amount.sense, qt->data.amount.how); + printf ("\tsign=%d amount=%f\n", qt->data.amount.amt_sgn, qt->data.amount.amount); + break; + case PR_BALANCE: + printf ("balance sense=%d how=%d\n", qt->data.balance.sense, qt->data.balance.how); + break; + case PR_CLEARED: + printf ("cleared sense=%d how=%d\n", qt->data.cleared.sense, qt->data.cleared.how); + break; + case PR_DATE: { + char buff[40]; + printf ("date sense=%d use_start=%d use_end=%d\n", + qt->data.base.sense, + qt->data.date.use_start, + qt->data.date.use_end + ); + if (qt->data.date.use_start) { + gnc_timespec_to_iso8601_buff (qt->data.date.start, buff); + printf ("\tstart date=%s\n", buff); + } + if (qt->data.date.use_end) { + gnc_timespec_to_iso8601_buff (qt->data.date.end, buff); + printf ("\tend date=%s\n", buff); + } + break; + } + case PR_DESC: + printf ("desc sense=%d case sensitive=%d\n", qt->data.str.sense, qt->data.str.case_sens); + printf ("\tmatch string=%s \n", qt->data.str.matchstring); + break; + + case PR_GUID: { + char buff[40]; + printf ("guid sense=%d\n", qt->data.guid.sense); + guid_to_string_buff (&qt->data.guid.guid, buff); + printf ("\tguid %s\n", buff); + break; + } + case PR_MEMO: + printf ("memo sense=%d case sensitive=%d\n", qt->data.str.sense, qt->data.str.case_sens); + printf ("\tmatch string=%s \n", qt->data.str.matchstring); + break; + case PR_MISC: + printf ("misc\n"); + break; + case PR_NUM: + printf ("num sense=%d case sensitive=%d\n", qt->data.str.sense, qt->data.str.case_sens); + printf ("\tmatch string=%s \n", qt->data.str.matchstring); + break; + case PR_PRICE: + printf ("price sense=%d how=%d\n", qt->data.amount.sense, qt->data.amount.how); + printf ("\tsign=%d amount=%f\n", qt->data.amount.amt_sgn, qt->data.amount.amount); + break; + case PR_SHRS: + printf ("shrs sense=%d how=%d\n", qt->data.amount.sense, qt->data.amount.how); + printf ("\tsign=%d amount=%f\n", qt->data.amount.amt_sgn, qt->data.amount.amount); + break; + + default: + printf ("unkown term type=%d \n", qt->data.base.term_type); + } + } + printf("\n"); + if(i->next) { + printf("\n"); + } + } +} + + +/******************************************************************** + * xaccInitQuery + ********************************************************************/ + +/* initial_term has hand-over semantics! Thus, initial_term must point + * to newly allocated memory or be NULL. */ +static void +xaccInitQuery(Query * q, QueryTerm * initial_term) { + GList * or = NULL; + GList * and = NULL; + + if(initial_term) { + or = g_list_alloc(); + and = g_list_alloc(); + and->data = initial_term; + or->data = and; + } + + if(q->terms) + xaccQueryClear(q); + + q->terms = or; + q->split_list = NULL; + q->changed = 1; + + q->max_splits = -1; + + q->primary_sort = BY_STANDARD; + q->secondary_sort = BY_NONE; + q->tertiary_sort = BY_NONE; + + q->primary_increasing = TRUE; + q->secondary_increasing = TRUE; + q->tertiary_increasing = TRUE; +} + + +/******************************************************************** + * xaccMallocQuery + ********************************************************************/ + +Query * +xaccMallocQuery(void) { + Query * qp = g_new0(Query, 1); + xaccInitQuery(qp, NULL); + return qp; +} + + +/******************************************************************** + * xaccQuerySwapTerms + * swaps the terms fields of two queries, mostly to + * allow quick pass-off of results. + ********************************************************************/ + +static void +xaccQuerySwapTerms(Query * q1, Query * q2) { + GList * g; + + if (!q1 || !q2) + return; + + g = q1->terms; + q1->terms = q2->terms; + q2->terms = g; + + q1->changed = 1; + q2->changed = 1; +} + + +/******************************************************************** + * xaccQueryHasTerms + * returns the number of 'OR' terms in the query, which is generally + * used as a truth test. + ********************************************************************/ + +int +xaccQueryHasTerms(Query * q) +{ + if (!q) + return 0; + + return g_list_length(q->terms); +} + +int +xaccQueryNumTerms(Query * q) +{ + GList *o; + int n=0; + if (!q) + return 0; + + for(o=q->terms; o; o=o->next) { + n += g_list_length(o->data); + } + return n; +} + +GList * +xaccQueryGetTerms (Query *q) +{ + if (!q) return NULL; + return q->terms; +} + + +/******************************************************************** + * xaccQueryHasTermType + * returns TRUE if the query has any terms of the given type + ********************************************************************/ +gboolean +xaccQueryHasTermType(Query * q, pd_type_t type) { + GList *or; + GList *and; + + if (!q) + return FALSE; + + for(or = q->terms; or; or = or->next) { + for(and = or->data; and; and = and->next) { + QueryTerm *qt = and->data; + if(qt->data.type == type) + return TRUE; + } + } + + return FALSE; +} + +static void +free_query_term(QueryTerm *qt) +{ + GList *node; + + if (qt == NULL) + return; + + switch (qt->data.type) + { + case PD_ACCOUNT: + g_list_free (qt->data.acct.accounts); + qt->data.acct.accounts = NULL; + + for (node = qt->data.acct.account_guids; node; node = node->next) + xaccGUIDFree (node->data); + + g_list_free (qt->data.acct.account_guids); + qt->data.acct.account_guids = NULL; + break; + + case PD_STRING: + g_free(qt->data.str.matchstring); + qt->data.str.matchstring = NULL; + break; + + default: + break; + } + + g_free(qt); +} + +static QueryTerm * +copy_query_term(QueryTerm * qt) { + QueryTerm * nqt; + GList *node; + + if (qt == NULL) + return NULL; + + nqt = g_new0(QueryTerm, 1); + + memcpy(nqt, qt, sizeof(QueryTerm)); + + switch (nqt->data.type) + { + case PD_ACCOUNT: + nqt->data.acct.accounts = g_list_copy (nqt->data.acct.accounts); + nqt->data.acct.account_guids = + g_list_copy (nqt->data.acct.account_guids); + for (node = nqt->data.acct.account_guids; node; node = node->next) + { + GUID *old = node->data; + GUID *new = xaccGUIDMalloc (); + + *new = *old; + node->data = new; + } + break; + + case PD_STRING: + nqt->data.str.matchstring = g_strdup(nqt->data.str.matchstring); + break; + + default: + break; + } + + return nqt; +} + +static GList * +copy_and_terms(GList *and_terms) { + GList *and = NULL; + GList *cur_and; + + for(cur_and = and_terms; cur_and; cur_and = cur_and->next) + and = g_list_prepend(and, copy_query_term (cur_and->data)); + + return g_list_reverse(and); +} + +static GList * +copy_or_terms(GList * or_terms) { + GList * or = NULL; + GList * cur_or; + + for(cur_or = or_terms; cur_or; cur_or = cur_or->next) + or = g_list_prepend(or, copy_and_terms(cur_or->data)); + + return g_list_reverse(or); +} + + +/******************************************************************** + * xaccFreeQuery + * note that the terms list is freed, so you must have newly + * allocated it + ********************************************************************/ + +static void +xaccFreeQueryMembers(Query *q) { + GList * cur_or; + + if (q == NULL) + return; + + for(cur_or = q->terms; cur_or; cur_or = cur_or->next) { + GList * cur_and; + + for(cur_and = cur_or->data; cur_and; cur_and = cur_and->next) { + free_query_term(cur_and->data); + cur_and->data = NULL; + } + + g_list_free(cur_or->data); + cur_or->data = NULL; + } + + g_list_free(q->terms); + q->terms = NULL; + + g_list_free(q->split_list); + q->split_list = NULL; +} + +void +xaccFreeQuery(Query * q) { + if (q == NULL) + return; + + xaccFreeQueryMembers (q); + + g_free(q); +} + +Query * +xaccQueryCopy(Query *q) { + Query *copy; + + if (q == NULL) + return NULL; + + copy = xaccMallocQuery (); + xaccFreeQueryMembers (copy); + + copy->terms = copy_or_terms (q->terms); + + copy->primary_sort = q->primary_sort; + copy->secondary_sort = q->secondary_sort; + copy->tertiary_sort = q->tertiary_sort; + + copy->primary_increasing = q->primary_increasing; + copy->secondary_increasing = q->secondary_increasing; + copy->tertiary_increasing = q->tertiary_increasing; + copy->max_splits = q->max_splits; + + copy->changed = q->changed; + copy->acct_group = q->acct_group; + copy->split_list = g_list_copy (q->split_list); + + return copy; +} + +/******************************************************************** + * xaccQueryInvert + * return a newly-allocated Query object which is the + * logical inverse of the original. + ********************************************************************/ + +Query * +xaccQueryInvert(Query * q) { + Query * retval; + Query * right, * left, * iright, * ileft; + QueryTerm * qt; + GList * aterms; + GList * cur; + GList * new_oterm; + int num_or_terms; + + num_or_terms = g_list_length(q->terms); + + switch(num_or_terms) { + case 0: + retval = xaccMallocQuery(); + retval->max_splits = q->max_splits; + retval->acct_group = q->acct_group; + break; + + /* this is demorgan expansion for a single AND expression. */ + /* !(abc) = !a + !b + !c */ + case 1: + retval = xaccMallocQuery(); + retval->max_splits = q->max_splits; + retval->acct_group = q->acct_group; + + aterms = g_list_nth_data(q->terms, 0); + new_oterm = NULL; + for(cur=aterms; cur; cur=cur->next) { + qt = copy_query_term(cur->data); + qt->data.base.sense = !(qt->data.base.sense); + new_oterm = g_list_append(NULL, qt); + retval->terms = g_list_append(retval->terms, new_oterm); + } + break; + + /* if there are multiple OR-terms, we just recurse by + * breaking it down to !(a + b + c) = + * !a * !(b + c) = !a * !b * !c. */ + default: + right = xaccMallocQuery(); + right->terms = copy_or_terms(g_list_nth(q->terms, 1)); + + left = xaccMallocQuery(); + left->terms = g_list_append(NULL, + copy_and_terms(g_list_nth_data(q->terms, 0))); + + iright = xaccQueryInvert(right); + ileft = xaccQueryInvert(left); + + retval = xaccQueryMerge(iright, ileft, QUERY_AND); + retval->max_splits = q->max_splits; + retval->acct_group = q->acct_group; + retval->changed = 1; + + xaccFreeQuery(iright); + xaccFreeQuery(ileft); + xaccFreeQuery(right); + xaccFreeQuery(left); + break; + } + + return retval; +} + + +/******************************************************************** + * xaccQueryMerge + * combine 2 Query objects by the logical operation in "op". + ********************************************************************/ + +Query * +xaccQueryMerge(Query * q1, Query * q2, QueryOp op) { + + Query * retval = NULL; + Query * i1, * i2; + Query * t1, * t2; + GList * i, * j; + + if(!q1 || !q2 || !(q1->acct_group == q2->acct_group)) { + return NULL; + } + + switch(op) { + case QUERY_OR: + retval = xaccMallocQuery(); + retval->terms = + g_list_concat(copy_or_terms(q1->terms), copy_or_terms(q2->terms)); + retval->max_splits = q1->max_splits; + retval->split_list = NULL; /* fixme */ + retval->changed = 1; + retval->acct_group = q1->acct_group; + break; + + case QUERY_AND: + retval = xaccMallocQuery(); + retval->max_splits = q1->max_splits; + retval->split_list = NULL; /* fixme */ + retval->changed = 1; + retval->acct_group = q1->acct_group; + + for(i=q1->terms; i; i=i->next) { + for(j=q2->terms; j; j=j->next) { + retval->terms = + g_list_append(retval->terms, + g_list_concat + (copy_and_terms(i->data), + copy_and_terms(j->data))); + } + } + break; + + case QUERY_NAND: + /* !(a*b) = (!a + !b) */ + i1 = xaccQueryInvert(q1); + i2 = xaccQueryInvert(q2); + retval = xaccQueryMerge(i1, i2, QUERY_OR); + xaccFreeQuery(i1); + xaccFreeQuery(i2); + break; + + case QUERY_NOR: + /* !(a+b) = (!a*!b) */ + i1 = xaccQueryInvert(q1); + i2 = xaccQueryInvert(q2); + retval = xaccQueryMerge(i1, i2, QUERY_AND); + xaccFreeQuery(i1); + xaccFreeQuery(i2); + break; + + case QUERY_XOR: + /* a xor b = (a * !b) + (!a * b) */ + i1 = xaccQueryInvert(q1); + i2 = xaccQueryInvert(q2); + t1 = xaccQueryMerge(q1, i2, QUERY_AND); + t2 = xaccQueryMerge(i1, q2, QUERY_AND); + retval = xaccQueryMerge(t1, t2, QUERY_OR); + + xaccFreeQuery(i1); + xaccFreeQuery(i2); + xaccFreeQuery(t1); + xaccFreeQuery(t2); + break; + } + + return retval; +} + + +/* this sort function just puts account queries at the top of the + * list. this lets us skip accounts that have no chance. */ +static gint +query_sort_func(gconstpointer pa, gconstpointer pb) { + const QueryTerm * a = pa; + const QueryTerm * b = pb; + if(a->data.type == PD_ACCOUNT) { + return -1; + } + else if(b->data.type == PD_ACCOUNT) { + return 1; + } + else if(a->data.type == PD_AMOUNT) { + return -1; + } + else if(b->data.type == PD_AMOUNT) { + return 1; + } + else { + return 0; + } +} + +static int +acct_query_matches(QueryTerm * qt, Account * acct) { + GList *node; + gboolean account_in_set = FALSE; + gboolean first_account = TRUE; + + assert(qt && acct); + assert(qt->data.type == PD_ACCOUNT); + + for(node = qt->data.acct.accounts; node ; node = node->next) { + if(acct == node->data) { + account_in_set = TRUE; + break; + } + first_account = FALSE; + } + + /* if we need the query to match "ALL" accounts, we only return + * true for the first acct in the set. */ + switch(qt->data.acct.how) { + case ACCT_MATCH_ALL: + return (account_in_set && first_account); + break; + + case ACCT_MATCH_ANY: + return account_in_set; + break; + + case ACCT_MATCH_NONE: + return !account_in_set; + break; + } + + return FALSE; +} + +static Query * split_sort_query = NULL; + +static int +date_cmp_func(Timespec *t1, Timespec *t2) { + /* check seconds */ + if (t1->tv_sec < t2->tv_sec) { + return -1; + } + else if (t1->tv_sec > t2->tv_sec) { + return +1; + } + + /* else, seconds match. check nanoseconds */ + if (t1->tv_nsec < t2->tv_nsec) { + return -1; + } + else if (t1->tv_nsec > t2->tv_nsec) { + return +1; + } + + return 0; +} + +/* compared by dates but return 0 if on same day */ + +static int +date_rounded_cmp_func(Timespec *t1, Timespec *t2) +{ + Timespec canon_t1, canon_t2; + canon_t1 = timespecCanonicalDayTime(*t1); + canon_t2 = timespecCanonicalDayTime(*t2); + return date_cmp_func(&canon_t1, &canon_t2); +} + + +static int +split_cmp_func(sort_type_t how, gconstpointer ga, gconstpointer gb) +{ + Split * sa = (Split *)ga; + Split * sb = (Split *)gb; + Transaction * ta; + Transaction * tb; + unsigned long n1; + unsigned long n2; + const char *da, *db; + gnc_numeric fa, fb; + + if (sa && !sb) return -1; + if (!sa && sb) return 1; + if (!sa && !sb) return 0; + + ta = sa->parent; + tb = sb->parent; + + if (ta->orig) ta = ta->orig; + if (tb->orig) tb = tb->orig; + + if ( (ta) && !(tb) ) return -1; + if ( !(ta) && (tb) ) return +1; + if ( !(ta) && !(tb) ) return 0; + + + switch(how) { + case BY_STANDARD: + return xaccSplitDateOrder(sa, sb); + break; + + case BY_DATE: + return date_cmp_func(&(ta->date_posted), &(tb->date_posted)); + + break; + + case BY_DATE_ROUNDED: + return date_rounded_cmp_func(&(ta->date_posted), &(tb->date_posted)); + + break; + + + case BY_DATE_ENTERED: + return date_cmp_func(&(ta->date_entered), &(tb->date_entered)); + + break; + + case BY_DATE_ENTERED_ROUNDED: + return date_rounded_cmp_func(&(ta->date_entered), &(tb->date_entered)); + + break; + + case BY_DATE_RECONCILED: + return date_cmp_func(&(sa->date_reconciled), &(sb->date_reconciled)); + + break; + + case BY_DATE_RECONCILED_ROUNDED: + return date_rounded_cmp_func(&(sa->date_reconciled), &(sb->date_reconciled)); + + break; + + case BY_NUM: + /* sort on transaction number */ + da = ta->num; + db = tb->num; + if (gnc_strisnum(da)) { + if (!gnc_strisnum(db)) { + return -1; + } + sscanf(da, "%lu", &n1); + sscanf(db, "%lu", &n2); + if (n1 < n2) { + return -1; + } + if (n1 == n2) { + return 0; + } + return +1; + } + if (gnc_strisnum(db)) { + return +1; + } + return safe_strcmp (da, db); + break; + + case BY_MEMO: + /* sort on memo strings */ + return safe_strcmp (sa->memo, sb->memo); + break; + + case BY_DESC: + /* sort on transaction strings */ + return safe_strcmp (ta->description, tb->description); + break; + + case BY_AMOUNT: + fa = sa->value; + fb = sb->value; + return gnc_numeric_compare(fa, fb); + break; + + case BY_RECONCILE: + /* Reconcile flags are sorted as: FREC = YREC < CREC = NREC */ + switch (sa->reconciled) { + case YREC: + case FREC: + if (sb->reconciled == YREC) + return 0; + if (sb->reconciled == FREC) + return 0; + return -1; + break; + + case CREC: + case NREC: + if (sb->reconciled == CREC) + return 0; + if (sb->reconciled == NREC) + return 0; + return 1; + break; + } + break; + + case BY_ACCOUNT_FULL_NAME: + return xaccSplitCompareAccountFullNames(sa, sb); + + case BY_ACCOUNT_CODE: + return xaccSplitCompareAccountCodes(sa, sb); + break; + + case BY_CORR_ACCOUNT_FULL_NAME: + return xaccSplitCompareOtherAccountFullNames(sa, sb); + + case BY_CORR_ACCOUNT_CODE: + return xaccSplitCompareOtherAccountCodes(sa, sb); + + case BY_NONE: + return 0; + break; + } + + return 0; +} + +static int +split_sort_func(gconstpointer a, gconstpointer b) { + int retval; + + + assert(split_sort_query); + + retval = split_cmp_func(split_sort_query->primary_sort, a, b); + if((retval == 0) && + (split_sort_query->secondary_sort != BY_NONE)) { + retval = split_cmp_func(split_sort_query->secondary_sort, a, b); + if((retval == 0) && + (split_sort_query->tertiary_sort != BY_NONE)) { + retval = split_cmp_func(split_sort_query->tertiary_sort, a, b); + return split_sort_query->tertiary_increasing ? retval : - retval; + } + else { + return split_sort_query->secondary_increasing ? retval : - retval; + } + } + else { + return split_sort_query->primary_increasing ? retval : - retval; + } +} + + +/******************************************************************** + * xaccQueryCheckSplit + * check a single split against the query. This is a short-circuited + * and-or check, so sorting it with best criteria first is a win. + ********************************************************************/ + +static int +xaccQueryCheckSplit(Query * q, Split * s) { + GList * and_ptr; + GList * or_ptr; + QueryTerm * qt; + int and_terms_ok=1; + + for(or_ptr = q->terms; or_ptr; or_ptr = or_ptr->next) { + and_terms_ok = 1; + for(and_ptr = or_ptr->data; and_ptr; and_ptr = and_ptr->next) { + qt = (QueryTerm *)(and_ptr->data); + if(((qt->p)(s, &(qt->data))) != qt->data.base.sense) { + and_terms_ok = 0; + break; + } + } + if(and_terms_ok) { + return 1; + } + } + return 0; +} + +static GList * +account_list_to_guid_list (GList *accounts) +{ + GList *guids = NULL; + GList *node; + + for (node = accounts; node; node = node->next) + { + Account *account = node->data; + GUID *guid; + + if (!account) + continue; + + guid = xaccGUIDMalloc (); + *guid = *xaccAccountGetGUID (account); + + guids = g_list_prepend (guids, guid); + } + + return g_list_reverse (guids); +} + +static GList * +copy_guid_list (GList *guids) +{ + GList *new_guids = NULL; + GList *node; + + for (node = guids; node; node = node->next) + { + GUID *guid = node->data; + GUID *new_guid; + + if (!guid) + continue; + + new_guid = xaccGUIDMalloc (); + *new_guid = *guid; + + new_guids = g_list_prepend (new_guids, new_guid); + } + + return g_list_reverse (new_guids); +} + +static GList * +guid_list_to_account_list (GList *guids) +{ + GList *accounts = NULL; + GList *node; + + for (node = guids; node; node = node->next) + { + GUID *guid = node->data; + Account *account; + + if (!guid) + continue; + + account = xaccAccountLookup (guid); + if (!account) + continue; + + accounts = g_list_prepend (accounts, account); + } + + return g_list_reverse (accounts); +} + +/******************************************************************** + * xaccQueryCompileTerms + * Prepare terms for processing by xaccQueryGetSplits + ********************************************************************/ +static void +xaccQueryCompileTerms (Query *q) +{ + GList * or_ptr, * and_ptr; + + for(or_ptr = q->terms; or_ptr ; or_ptr = or_ptr->next) { + for(and_ptr = or_ptr->data; and_ptr; and_ptr = and_ptr->next) { + QueryTerm *qt = and_ptr->data; + switch (qt->data.type) + { + case PD_ACCOUNT: + g_list_free (qt->data.acct.accounts); + qt->data.acct.accounts = + guid_list_to_account_list (qt->data.acct.account_guids); + break; + + default: + break; + } + } + } +} + +/******************************************************************** + * xaccQueryGetSplits + * Run the search. + ********************************************************************/ + +GList * +xaccQueryGetSplits(Query * q) { + GList * matching_splits=NULL; + GList * or_ptr, * and_ptr, * mptr; + GList * all_accts, * node; + Account * current; + QueryTerm * qt; + Backend * be; + + int total_splits_checked = 0; + int split_count = 0; + int acct_ok; + + if (!q) return NULL; + ENTER("query=%p", q); + + /* tmp hack alert */ + q->changed = 1; + + if(q->changed == 0) { + return q->split_list; + } + + /* prioritize the query terms for a faster search. This + * will put account queries at the top of the AND chains. */ + + /* FIXME : sort for securities queries and eliminate non- + * security accounts */ + for(or_ptr = q->terms; or_ptr ; or_ptr = or_ptr->next) { + and_ptr = or_ptr->data; + or_ptr->data = g_list_sort(and_ptr, query_sort_func); + } + + /* prepare the terms for processing */ + xaccQueryCompileTerms (q); + + /* if there is a backend, query the backend, let it fetch the data */ + be = xaccGroupGetBackend (q->acct_group); + if (be && be->run_query) { + (be->run_query) (be, q); + } + + /* iterate over accounts */ + all_accts = xaccGroupGetSubAccounts (q->acct_group); + + for (node = all_accts; node; node = node->next) { + current = node->data; + + if (!current) + continue; + + /* first, check this account to see if we need to look at it at + * all. If the query is "ANY" matching or "NONE" matching, you + * get what you expect; if it's "ALL" matching, only the first + * account in the query matches. This could be optimized to only + * look at the smallest account. */ + acct_ok = 0; + for(or_ptr = q->terms; or_ptr ; or_ptr = or_ptr->next) { + and_ptr = or_ptr->data; + qt = and_ptr->data; + + if(qt->data.type == PD_ACCOUNT) { + if(acct_query_matches(qt, current)) { + acct_ok = 1; + break; + } + } + else { + /* if the first query term isn't an account, then we have to + * look at every split in every account. */ + + /* FIXME : security accounts can be ruled out */ + acct_ok = 1; + break; + } + } + + if(acct_ok) { + GList *lp; + + /* iterate over splits */ + for(lp = xaccAccountGetSplitList(current); lp; lp = lp->next) { + Split *s = (Split *) lp->data; + if(xaccQueryCheckSplit(q, s)) { + matching_splits = g_list_prepend(matching_splits, s); + split_count++; + } + total_splits_checked++; + } + } + } + + g_list_free (all_accts); + + /* There is no absolute need to reverse this list, since it's + * being sorted below. However, in the common case, we will be + * searching in a single account and returning in the account + * order, thus reversing will put us in the correct order we + * want and make the sorting go much faster. */ + matching_splits = g_list_reverse(matching_splits); + + /* now sort the matching splits based on the search criteria + * split_sort_query is an unforgivable use of static global data... + * I just can't figure out how else to do this sanely. */ + split_sort_query = q; + matching_splits = g_list_sort(matching_splits, split_sort_func); + + /* crop the list to limit the number of splits */ + if((split_count > q->max_splits) && (q->max_splits > -1)) { + if(q->max_splits > 0) { + /* mptr is set to the first node of what will be the new list */ + mptr = g_list_nth(matching_splits, split_count - q->max_splits); + /* mptr should not be NULL, but let's be safe */ + if (mptr != NULL) { + if (mptr->prev != NULL) + mptr->prev->next = NULL; + mptr->prev = NULL; + } + g_list_free(matching_splits); + matching_splits = mptr; + } + else { /* q->max_splits == 0 */ + g_list_free(matching_splits); + matching_splits = NULL; + } + split_count = q->max_splits; + } + + q->changed = 0; + + g_list_free(q->split_list); + q->split_list = matching_splits; + + return matching_splits; +} + +/******************************************************************** + * xaccQueryGetSplitsUniqueTrans + * Get splits but no more than one from a given transaction. + ********************************************************************/ + +GList * +xaccQueryGetSplitsUniqueTrans(Query *q) +{ + GList * splits = xaccQueryGetSplits(q); + GList * current; + GList * result = NULL; + GHashTable * trans_hash = g_hash_table_new(g_direct_hash, g_direct_equal); + + for (current = splits; current; current = current->next) + { + Split *split = current->data; + Transaction *trans = xaccSplitGetParent (split); + + if (!g_hash_table_lookup (trans_hash, trans)) + { + g_hash_table_insert (trans_hash, trans, trans); + result = g_list_prepend (result, split); + } + } + + g_hash_table_destroy (trans_hash); + + return g_list_reverse (result); +} + +/******************************************************************** + * xaccQueryGetTransactions + * Get transactions matching the query terms, specifying whether + * we require some or all splits to match + ********************************************************************/ + +static void +query_match_all_filter_func(gpointer key, gpointer value, gpointer user_data) { + Transaction * t = key; + int num_matches = GPOINTER_TO_INT(value); + GList ** matches = user_data; + + if(num_matches == xaccTransCountSplits(t)) { + *matches = g_list_prepend(*matches, t); + } +} + +static void +query_match_any_filter_func(gpointer key, gpointer value, gpointer user_data) { + Transaction * t = key; + GList ** matches = user_data; + *matches = g_list_prepend(*matches, t); +} + +GList * +xaccQueryGetTransactions (Query * q, query_run_t runtype) { + GList * splits = xaccQueryGetSplits(q); + GList * current = NULL; + GList * retval = NULL; + GHashTable * trans_hash = g_hash_table_new(g_direct_hash, g_direct_equal); + Transaction * trans = NULL; + gpointer val = NULL; + int count = 0; + + /* iterate over matching splits, incrementing a match-count in + * the hash table */ + for(current = splits; current; current=current->next) { + trans = xaccSplitGetParent((Split *)(current->data)); + + /* don't waste time looking up unless we need the count + * information */ + if(runtype == QUERY_MATCH_ALL) { + val = g_hash_table_lookup(trans_hash, trans); + count = GPOINTER_TO_INT(val); + } + g_hash_table_insert(trans_hash, trans, GINT_TO_POINTER(count + 1)); + } + + /* now pick out the transactions that match */ + if(runtype == QUERY_MATCH_ALL) { + g_hash_table_foreach(trans_hash, query_match_all_filter_func, + &retval); + } + else { + g_hash_table_foreach(trans_hash, query_match_any_filter_func, + &retval); + } + + /* ATM we rerun the xtn filter every time. Need to add checks and + * updates for using cached results. */ + q->last_run_type = runtype; + q->xtn_list = retval; + + g_hash_table_destroy(trans_hash); + + return retval; +} + +/******************************************************************** + * xaccQueryEqual + * Compare two queries for equality + ********************************************************************/ + +static gboolean +xaccQueryTermEqual (QueryTerm *qt1, QueryTerm *qt2) +{ + GList *l1, *l2; + + if (qt1 == qt2) return TRUE; + if (!qt1 || !qt2) return FALSE; + + if (qt1->p != qt2->p) return FALSE; + if (qt1->data.type != qt2->data.type) return FALSE; + if (qt1->data.base.term_type != qt2->data.base.term_type) return FALSE; + if (qt1->data.base.sense != qt2->data.base.sense) return FALSE; + + switch (qt1->data.type) + { + case PD_DATE: + if (qt1->data.date.use_start != qt2->data.date.use_start) return FALSE; + if (qt1->data.date.use_end != qt2->data.date.use_end) return FALSE; + if (!timespec_equal (&qt1->data.date.start, &qt2->data.date.start)) + return FALSE; + if (!timespec_equal (&qt1->data.date.end, &qt2->data.date.end)) + return FALSE; + break; + + case PD_AMOUNT: + if (qt1->data.amount.how != qt2->data.amount.how) return FALSE; + if (qt1->data.amount.amt_sgn != qt2->data.amount.amt_sgn) return FALSE; + if (qt1->data.amount.amount != qt2->data.amount.amount) + return FALSE; + break; + + case PD_ACCOUNT: + if (qt1->data.acct.how != qt2->data.acct.how) return FALSE; + l1 = qt1->data.acct.account_guids; + l2 = qt2->data.acct.account_guids; + if (g_list_length (l1) != g_list_length (l2)) return FALSE; + for ( ; l1; l1 = l1->next, l2 = l2->next) + if (!guid_equal (l1->data, l2->data)) + return FALSE; + break; + + case PD_STRING: + if (qt1->data.str.case_sens != qt2->data.str.case_sens) + return FALSE; + if (qt1->data.str.use_regexp != qt2->data.str.use_regexp) + return FALSE; + if (strcmp (qt1->data.str.matchstring, qt2->data.str.matchstring) != 0) + return FALSE; + break; + + case PD_CLEARED: + if (qt1->data.cleared.how != qt2->data.cleared.how) return FALSE; + break; + + case PD_BALANCE: + if (qt1->data.balance.how != qt2->data.balance.how) return FALSE; + break; + + case PD_GUID: + if (!guid_equal (&qt1->data.guid.guid, &qt2->data.guid.guid)) + return FALSE; + break; + + case PD_MISC: + if (qt1->data.misc.how != qt2->data.misc.how) return FALSE; + if (qt1->data.misc.data != qt2->data.misc.data) return FALSE; + break; + + default: + PERR ("bad query term type"); + return FALSE; + } + + return TRUE; +} + +gboolean +xaccQueryEqual (Query *q1, Query *q2) +{ + GList *or1, *or2; + + if (q1 == q2) return TRUE; + if (!q1 || !q2) return FALSE; + + if (g_list_length (q1->terms) != g_list_length (q2->terms)) return FALSE; + + for (or1 = q1->terms, or2 = q2->terms; or1; + or1 = or1->next, or2 = or2->next) + { + GList *and1, *and2; + + and1 = or1->data; + and2 = or2->data; + + if (g_list_length (and1) != g_list_length (and2)) return FALSE; + + for ( ; and1; and1 = and1->next, and2 = and2->next) + if (!xaccQueryTermEqual (and1->data, and2->data)) + return FALSE; + } + + if (q1->primary_sort != q2->primary_sort) return FALSE; + if (q1->secondary_sort != q2->secondary_sort) return FALSE; + if (q1->tertiary_sort != q2->tertiary_sort) return FALSE; + + if (q1->primary_increasing != q2->primary_increasing) return FALSE; + if (q1->secondary_increasing != q2->secondary_increasing) return FALSE; + if (q1->tertiary_increasing != q2->tertiary_increasing) return FALSE; + + if (q1->max_splits != q2->max_splits) return FALSE; + + return TRUE; +} + +Predicate +xaccQueryGetPredicate (pr_type_t term_type) +{ + Predicate p = NULL; + + /* the predicates are only known in the local + * address space, which is why we have to set them + * from the abstract type here. + */ + switch (term_type) + { + case PR_ACCOUNT: + p = & xaccAccountMatchPredicate; + break; + case PR_ACTION: + p = & xaccActionMatchPredicate; + break; + case PR_AMOUNT: + p = & xaccAmountMatchPredicate; + break; + case PR_BALANCE: + p = & xaccBalanceMatchPredicate; + break; + case PR_CLEARED: + p = & xaccClearedMatchPredicate; + break; + case PR_DATE: + p = & xaccDateMatchPredicate; + break; + case PR_DESC: + p = & xaccDescriptionMatchPredicate; + break; + case PR_GUID: + p = & xaccGUIDMatchPredicate; + break; + case PR_MEMO: + p = & xaccMemoMatchPredicate; + break; + case PR_NUM: + p = & xaccNumberMatchPredicate; + break; + case PR_PRICE: + p = & xaccSharePriceMatchPredicate; + break; + case PR_SHRS: + p = & xaccSharesMatchPredicate; + break; + case PR_MISC: + PERR ("misc term must not appear"); + break; + } + return p; +} + +/******************************************************************** + * xaccQueryAddPredicate + * Add a predicate an existing query. + ********************************************************************/ + +void +xaccQueryAddPredicate (Query * q, + PredicateData *pred, + QueryOp op) +{ + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->data = *pred; + qt->p = xaccQueryGetPredicate (qt->data.base.term_type); + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************** + * xaccQueryAddAccountMatch + * Add an account filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddAccountMatch(Query * q, GList * accounts, acct_match_t how, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccAccountMatchPredicate; + qt->data.type = PD_ACCOUNT; + qt->data.base.term_type = PR_ACCOUNT; + qt->data.base.sense = 1; + qt->data.acct.how = how; + qt->data.acct.accounts = NULL; + qt->data.acct.account_guids = account_list_to_guid_list (accounts); + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************** + * xaccQueryAddAccountGUIDMatch + * Add an account filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddAccountGUIDMatch(Query * q, GList * account_guids, + acct_match_t how, QueryOp op) +{ + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccAccountMatchPredicate; + qt->data.type = PD_ACCOUNT; + qt->data.base.term_type = PR_ACCOUNT; + qt->data.base.sense = 1; + qt->data.acct.how = how; + qt->data.acct.accounts = NULL; + qt->data.acct.account_guids = copy_guid_list (account_guids); + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************** + * xaccQueryAddSingleAccountMatch + * Add an account filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddSingleAccountMatch(Query * q, Account * acct, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccAccountMatchPredicate; + qt->data.type = PD_ACCOUNT; + qt->data.base.term_type = PR_ACCOUNT; + qt->data.base.sense = 1; + qt->data.acct.how = ACCT_MATCH_ANY; + qt->data.acct.accounts = g_list_prepend(NULL, acct); + qt->data.acct.account_guids + = account_list_to_guid_list (qt->data.acct.accounts); + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * xaccQueryAddDescriptionMatch + * Add a description filter to an existing query + ********************************************************************/ + +void +xaccQueryAddDescriptionMatch(Query * q, const char * matchstring, + int case_sens, int use_regexp, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + int flags = REG_EXTENDED; + + qt->p = & xaccDescriptionMatchPredicate; + qt->data.type = PD_STRING; + qt->data.base.term_type = PR_DESC; + qt->data.base.sense = 1; + qt->data.str.case_sens = case_sens; + qt->data.str.use_regexp = use_regexp; + qt->data.str.matchstring = g_strdup(matchstring); + + if(!case_sens) { + flags |= REG_ICASE; + } + + if(use_regexp) { + regcomp(&qt->data.str.compiled, matchstring, flags); + } + else if(!case_sens) { + char *s; + + for(s = qt->data.str.matchstring; *s; s++) { + *s = tolower(*s); + } + } + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * xaccQueryAddMemoMatch + * Add a memo conparison to an existing query + ********************************************************************/ + +void +xaccQueryAddMemoMatch(Query * q, const char * matchstring, + int case_sens, int use_regexp, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + int flags = REG_EXTENDED; + + qt->p = & xaccMemoMatchPredicate; + qt->data.type = PD_STRING; + qt->data.base.term_type = PR_MEMO; + qt->data.base.sense = 1; + qt->data.str.case_sens = case_sens; + qt->data.str.use_regexp = use_regexp; + qt->data.str.matchstring = g_strdup(matchstring); + + if(!case_sens) { + flags |= REG_ICASE; + } + + if(use_regexp) { + regcomp(&qt->data.str.compiled, matchstring, flags); + } + else if(!case_sens) { + char *s; + + for(s = qt->data.str.matchstring; *s; s++) { + *s = tolower(*s); + } + } + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * xaccQueryAddDateMatchTS + * Add a date filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddDateMatchTS(Query * q, + int use_start, + Timespec sts, + int use_end, + Timespec ets, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccDateMatchPredicate; + qt->data.type = PD_DATE; + qt->data.base.term_type = PR_DATE; + qt->data.base.sense = 1; + qt->data.date.use_start = use_start; + qt->data.date.use_end = use_end; + qt->data.date.start = sts; + qt->data.date.end = ets; + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************** + * xaccQueryAddDateMatch + * Add a date filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddDateMatch(Query * q, + int use_start, int sday, int smonth, int syear, + int use_end, int eday, int emonth, int eyear, + QueryOp op) +{ + /* gcc -O3 will auto-inline this function, avoiding a call overhead */ + xaccQueryAddDateMatchTS (q, use_start, + gnc_dmy2timespec(sday, smonth, syear), + use_end, + gnc_dmy2timespec_end(eday, emonth, eyear), + op); +} + +/******************************************************************** + * xaccQueryAddDateMatchTT + * Add a date filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddDateMatchTT(Query * q, + int use_start, + time_t stt, + int use_end, + time_t ett, + QueryOp op) +{ + Timespec sts; + Timespec ets; + + sts.tv_sec = (long long)stt; + sts.tv_nsec = 0; + + ets.tv_sec = (long long)ett; + ets.tv_nsec = 0; + + /* gcc -O3 will auto-inline this function, avoiding a call overhead */ + xaccQueryAddDateMatchTS (q, use_start, sts, + use_end, ets, op); + +} + +/******************************************************************** + * xaccQueryAddNumberMatch + * Add a number-field filter + ********************************************************************/ +void +xaccQueryAddNumberMatch(Query * q, const char * matchstring, int case_sens, + int use_regexp, QueryOp op) { + + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + int flags = REG_EXTENDED; + + qt->p = & xaccNumberMatchPredicate; + qt->data.type = PD_STRING; + qt->data.base.term_type = PR_NUM; + qt->data.base.sense = 1; + qt->data.str.case_sens = case_sens; + qt->data.str.use_regexp = use_regexp; + qt->data.str.matchstring = g_strdup(matchstring); + + if(!case_sens) { + flags |= REG_ICASE; + } + + if(use_regexp) { + regcomp(&qt->data.str.compiled, matchstring, flags); + } + else if(!case_sens) { + char *s; + + for(s = qt->data.str.matchstring; *s; s++) { + *s = tolower(*s); + } + } + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * xaccQueryAddActionMatch + * Add a action-field filter + ********************************************************************/ +void +xaccQueryAddActionMatch(Query * q, const char * matchstring, int case_sens, + int use_regexp, QueryOp op) { + + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + int flags = REG_EXTENDED; + + qt->p = & xaccActionMatchPredicate; + qt->data.type = PD_STRING; + qt->data.base.term_type = PR_ACTION; + qt->data.base.sense = 1; + qt->data.str.case_sens = case_sens; + qt->data.str.use_regexp = use_regexp; + qt->data.str.matchstring = g_strdup(matchstring); + + if(!case_sens) { + flags |= REG_ICASE; + } + + if(use_regexp) { + regcomp(&qt->data.str.compiled, matchstring, flags); + } + else if(!case_sens) { + char *s; + + for(s = qt->data.str.matchstring; *s; s++) { + *s = tolower(*s); + } + } + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * DxaccQueryAddAmountMatch + * Add a value filter to an existing query. + * FIXME ?? fix what ?? + ********************************************************************/ + +void +DxaccQueryAddAmountMatch(Query * q, double amt, + amt_match_sgn_t amt_sgn, + amt_match_t how, + QueryOp op) { + + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccAmountMatchPredicate; + qt->data.type = PD_AMOUNT; + qt->data.base.term_type = PR_AMOUNT; + qt->data.base.sense = 1; + qt->data.amount.how = how; + qt->data.amount.amt_sgn = amt_sgn; + qt->data.amount.amount = amt; + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * DxaccQueryAddSharePriceMatch + * Add a share-price filter to an existing query. + * FIXME ?? fix what ?? + ********************************************************************/ + +void +DxaccQueryAddSharePriceMatch(Query * q, double amt, + amt_match_t how, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccSharePriceMatchPredicate; + qt->data.type = PD_AMOUNT; + qt->data.base.term_type = PR_PRICE; + qt->data.base.sense = 1; + qt->data.amount.how = how; + qt->data.amount.amt_sgn = AMT_SGN_MATCH_EITHER; + qt->data.amount.amount = amt; + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * DxaccQueryAddSharesMatch + * Add a quantity filter to an existing query. + * FIXME ?? fix what ?? + ********************************************************************/ + +void +DxaccQueryAddSharesMatch(Query * q, double amt, + amt_match_t how, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccSharesMatchPredicate; + qt->data.type = PD_AMOUNT; + qt->data.base.term_type = PR_SHRS; + qt->data.base.sense = 1; + qt->data.amount.how = how; + qt->data.amount.amt_sgn = AMT_SGN_MATCH_EITHER; + qt->data.amount.amount = amt; + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + + +/******************************************************************** + * xaccQueryAddMiscMatch + * Add an arbitrary predicate to a query. You really shouldn't + * do this. + ********************************************************************/ + +void +xaccQueryAddMiscMatch(Query * q, Predicate p, int how, int data, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = p; + qt->data.type = PD_MISC; + qt->data.base.term_type = PR_MISC; + qt->data.base.sense = 1; + qt->data.misc.how = how; + qt->data.misc.data = data; + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************** + * xaccQueryAddClearedMatch + * Add a 'cleared' filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddClearedMatch(Query * q, cleared_match_t how, + QueryOp op) { + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccClearedMatchPredicate; + qt->data.type = PD_CLEARED; + qt->data.base.term_type = PR_CLEARED; + qt->data.base.sense = 1; + qt->data.cleared.how = how; + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************** + * xaccQueryAddBalanceMatch + * Add a 'balance' filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddBalanceMatch(Query * q, balance_match_t how, QueryOp op) +{ + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccBalanceMatchPredicate; + qt->data.type = PD_BALANCE; + qt->data.base.term_type = PR_BALANCE; + qt->data.base.sense = 1; + qt->data.balance.how = how; + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************** + * xaccQueryAddGUIDMatch + * Add a 'guid' filter to an existing query. + ********************************************************************/ + +void +xaccQueryAddGUIDMatch(Query * q, const GUID *guid, QueryOp op) +{ + Query * qs = xaccMallocQuery(); + QueryTerm * qt = g_new0(QueryTerm, 1); + Query * qr; + + qt->p = & xaccGUIDMatchPredicate; + qt->data.type = PD_GUID; + qt->data.base.term_type = PR_GUID; + qt->data.base.sense = 1; + qt->data.guid.guid = guid ? *guid : *xaccGUIDNULL (); + + xaccInitQuery(qs, qt); + xaccQuerySetGroup(qs, q->acct_group); + + if(xaccQueryHasTerms(q)) { + qr = xaccQueryMerge(q, qs, op); + } + else { + qr = xaccQueryMerge(q, qs, QUERY_OR); + } + xaccQuerySwapTerms(q, qr); + xaccFreeQuery(qs); + xaccFreeQuery(qr); +} + +/******************************************************************* + * xaccQueryPurgeTerms + * delete any terms of a particular type + *******************************************************************/ + +void +xaccQueryPurgeTerms(Query * q, pd_type_t type) { + QueryTerm * qt; + GList * or; + GList * and; + + if (!q) + return; + + for(or = q->terms; or; or = or->next) { + for(and = or->data; and; and = and->next) { + qt = and->data; + if(qt->data.type == type) { + if(g_list_length(or->data) == 1) { + q->terms = g_list_remove_link(q->terms, or); + g_list_free_1(or); + or = q->terms; + break; + } + else { + or->data = g_list_remove_link(or->data, and); + g_list_free_1(and); + and = or->data; + if(!and) break; + } + q->changed = 1; + free_query_term(qt); + } + } + if(!or) break; + } +} + + +/******************************************************************* + * xaccQueryClear + * remove all terms from a query. + *******************************************************************/ + +void +xaccQueryClear(Query * q) +{ + Query * q2 = xaccMallocQuery(); + xaccQuerySwapTerms(q, q2); + q->changed = 1; + xaccFreeQuery(q2); +} + + +/******************************************************************* + * string_match_predicate + * internal subroutine for description and memo matching + *******************************************************************/ + +static int +string_match_predicate(const char * s, PredicateData * pd) +{ + regmatch_t match; + + assert(s && pd && (pd->type == PD_STRING)); + + if(!pd->str.matchstring) return 0; + + if(pd->str.use_regexp) { + if(!regexec(&pd->str.compiled, s, 1, &match, 0)) { + return 1; + } + else { + return 0; + } + } + else if(pd->str.case_sens) { + if(strstr(s, pd->str.matchstring)) return 1; + else return 0; + } + else { + /* use case-insensitive compare */ + if(strcasestr(s, pd->str.matchstring)) return 1; + else return 0; + } + +} + + +/******************************************************************* + * value_match_predicate + *******************************************************************/ +static int +value_match_predicate(double splitamt, PredicateData * pd) { + switch(pd->amount.how) { + case AMT_MATCH_ATLEAST: + return fabs(splitamt) >= pd->amount.amount; + break; + case AMT_MATCH_ATMOST: + return fabs(splitamt) <= pd->amount.amount; + break; + case AMT_MATCH_EXACTLY: + return fabs(fabs(splitamt) - fabs(pd->amount.amount)) < .0001; + break; + } + + return 0; +} + + +/******************************************************************* + * xaccAccountMatchPredicate + *******************************************************************/ +static int +xaccAccountMatchPredicate(Split * s, PredicateData * pd) { + Transaction * parent; + Split * split; + Account * split_acct; + GList * acct_node; + int i; + int numsplits; + + assert(s && pd); + assert(pd->type == PD_ACCOUNT); + + + switch(pd->acct.how) { + case ACCT_MATCH_ALL: + /* there must be a split in parent that matches each of the + * accounts listed in pd. */ + parent = xaccSplitGetParent(s); + assert(parent); + numsplits = xaccTransCountSplits(parent); + for(acct_node=pd->acct.accounts; acct_node; acct_node=acct_node->next) { + for(i=0; i < numsplits; i++) { + split = xaccTransGetSplit(parent, i); + if(acct_node->data == xaccSplitGetAccount(split)) { + /* break here means we found a match before running out + * of splits (success) */ + break; + } + } + if(i == numsplits) { + /* break here means we ran out of splits before finding + * an account (failure) */ + break; + } + } + if(acct_node) return 0; + else return 1; + + break; + + case ACCT_MATCH_ANY: + /* s must match an account in pd */ + split_acct = xaccSplitGetAccount(s); + return (g_list_find(pd->acct.accounts, split_acct) != NULL); + break; + + case ACCT_MATCH_NONE: + /* s must match no account in pd */ + split_acct = xaccSplitGetAccount(s); + return (g_list_find(pd->acct.accounts, split_acct) == NULL); + break; + } + + return 0; +} + + +/******************************************************************* + * xaccDescriptionMatchPredicate + *******************************************************************/ +static int +xaccDescriptionMatchPredicate(Split * s, PredicateData * pd) { + Transaction * parent; + const char * descript; + + assert(s && pd); + assert(pd->type == PD_STRING); + + parent = xaccSplitGetParent(s); + assert(parent); + + descript = xaccTransGetDescription(parent); + return string_match_predicate(descript, pd); +} + +/******************************************************************* + * xaccGUIDMatchPredicate + *******************************************************************/ +static int +xaccGUIDMatchPredicate(Split * s, PredicateData * pd) +{ + GUIDPredicateData *gpd; + GUID *guid; + + assert(s && pd); + assert(pd->type == PD_GUID); + + guid = &pd->guid.guid; + + switch (xaccGUIDType (guid)) + { + case GNC_ID_NONE: + case GNC_ID_NULL: + default: + return 0; + + case GNC_ID_ACCOUNT: + return xaccSplitGetAccount (s) == xaccAccountLookup (guid); + + case GNC_ID_TRANS: + return xaccSplitGetParent (s) == xaccTransLookup (guid); + + case GNC_ID_SPLIT: + return s == xaccSplitLookup (guid); + } +} + +/******************************************************************* + * xaccNumberMatchPredicate + *******************************************************************/ +static int +xaccNumberMatchPredicate(Split * s, PredicateData * pd) { + Transaction * parent; + const char * number; + + assert(s && pd); + assert(pd->type == PD_STRING); + + parent = xaccSplitGetParent(s); + assert(parent); + + number = xaccTransGetNum(parent); + return string_match_predicate(number, pd); +} + + +/******************************************************************* + * xaccActionMatchPredicate + *******************************************************************/ +static int +xaccActionMatchPredicate(Split * s, PredicateData * pd) { + const char * action; + + assert(s && pd); + assert(pd->type == PD_STRING); + + action = xaccSplitGetAction(s); + return string_match_predicate(action, pd); +} + + +/******************************************************************* + * xaccMemoMatchPredicate + *******************************************************************/ +static int +xaccMemoMatchPredicate(Split * s, PredicateData * pd) { + const char * memo; + + assert(s && pd); + memo = xaccSplitGetMemo(s); + + return string_match_predicate(memo, pd); +} + + +/******************************************************************* + * xaccAmountMatchPredicate + *******************************************************************/ +static int +xaccAmountMatchPredicate(Split * s, PredicateData * pd) { + double splitamt; + + assert(s && pd); + assert(pd->type == PD_AMOUNT); + + splitamt = DxaccSplitGetValue(s); + + switch(pd->amount.amt_sgn) { + case AMT_SGN_MATCH_CREDIT: + if(splitamt > 0.0) return 0; + break; + case AMT_SGN_MATCH_DEBIT: + if(splitamt < 0.0) return 0; + break; + default: + break; + } + + return value_match_predicate(splitamt, pd); +} + +/******************************************************************* + * xaccSharePriceMatchPredicate + *******************************************************************/ +static int +xaccSharePriceMatchPredicate(Split * s, PredicateData * pd) { + double splitamt; + Account * acct; + int type; + + assert(s && pd); + assert(pd->type == PD_AMOUNT); + + acct = xaccSplitGetAccount(s); + type = xaccAccountGetType(acct); + + if((type != STOCK) && (type != MUTUAL)) { + return 0; + } + splitamt = DxaccSplitGetSharePrice(s); + + return value_match_predicate(splitamt, pd); +} + + +/******************************************************************* + * xaccSharesMatchPredicate + *******************************************************************/ +static int +xaccSharesMatchPredicate(Split * s, PredicateData * pd) { + double splitamt; + Account * acct; + int type; + + assert(s && pd); + assert(pd->type == PD_AMOUNT); + + acct = xaccSplitGetAccount(s); + type = xaccAccountGetType(acct); + + if((type != STOCK) && (type != MUTUAL)) { + return 0; + } + + splitamt = DxaccSplitGetShareAmount(s); + + return value_match_predicate(splitamt, pd); +} + + +/******************************************************************* + * xaccDateMatchPredicate + *******************************************************************/ +static int +xaccDateMatchPredicate(Split * s, PredicateData * pd) { + Timespec transtime; + + assert(s && pd); + assert(pd->type == PD_DATE); + + xaccTransGetDatePostedTS(xaccSplitGetParent(s), &transtime); + + if(pd->date.use_start && pd->date.use_end) { + return ((transtime.tv_sec >= pd->date.start.tv_sec) && + (transtime.tv_sec <= pd->date.end.tv_sec)); + } + else if(pd->date.use_start) { + return ((transtime.tv_sec >= pd->date.start.tv_sec)); + } + else if(pd->date.use_end) { + return ((transtime.tv_sec <= pd->date.end.tv_sec)); + } + else { + return 1; + } +} + +/******************************************************************* + * xaccClearedMatchPredicate + *******************************************************************/ +static int +xaccClearedMatchPredicate(Split * s, PredicateData * pd) { + int cstate; + + assert(s && pd); + assert(pd->type == PD_CLEARED); + + cstate = xaccSplitGetReconcile(s); + switch(cstate) { + case CREC: + return ((pd->cleared.how & CLEARED_CLEARED) ? 1 : 0); + break; + case YREC: + return ((pd->cleared.how & CLEARED_RECONCILED) ? 1 : 0); + break; + case FREC: + return ((pd->cleared.how & CLEARED_FROZEN) ? 1 : 0); + break; + case NREC: + return ((pd->cleared.how & CLEARED_NO) ? 1 : 0); + break; + } + + return 0; +} + +/******************************************************************* + * xaccBalanceMatchPredicate + *******************************************************************/ +static int +xaccBalanceMatchPredicate(Split * s, PredicateData * pd) { + gboolean balanced; + + assert(s && pd); + assert(pd->type == PD_BALANCE); + + if ((pd->balance.how & BALANCE_BALANCED) && + (pd->balance.how & BALANCE_UNBALANCED)) + return 1; + + balanced = + gnc_numeric_zero_p (xaccTransGetImbalance (xaccSplitGetParent (s))); + + if (balanced && (pd->balance.how & BALANCE_BALANCED)) + return 1; + + if (!balanced && (pd->balance.how & BALANCE_UNBALANCED)) + return 1; + + return 0; +} + + +/******************************************************************* + * xaccQuerySetSortOrder + *******************************************************************/ +void +xaccQuerySetSortOrder(Query * q, sort_type_t primary, + sort_type_t secondary, sort_type_t tertiary) { + if (!q) return; + q->primary_sort = primary; + q->secondary_sort = secondary; + q->tertiary_sort = tertiary; + + q->changed = 1; +} + +/******************************************************************* + * xaccQueryGetPrimarySortOrder + *******************************************************************/ +sort_type_t +xaccQueryGetPrimarySortOrder(Query * q) +{ + if (!q) return BY_NONE; + return q->primary_sort; +} + +/******************************************************************* + * xaccQueryGetSecondarySortOrder + *******************************************************************/ +sort_type_t +xaccQueryGetSecondarySortOrder(Query * q) +{ + if (!q) return BY_NONE; + return q->secondary_sort; +} + +/******************************************************************* + * xaccQueryGetTertiarySortOrder + *******************************************************************/ +sort_type_t +xaccQueryGetTertiarySortOrder(Query * q) +{ + if (!q) return BY_NONE; + return q->tertiary_sort; +} + +/******************************************************************* + * xaccQuerySetSortIncreasing + *******************************************************************/ +void +xaccQuerySetSortIncreasing(Query * q, gboolean prim_increasing, + gboolean sec_increasing, + gboolean tert_increasing) +{ + if (!q) return; + q->primary_increasing = prim_increasing; + q->secondary_increasing = sec_increasing; + q->tertiary_increasing = tert_increasing; + return; +} + +/******************************************************************* + * xaccQueryGetSortPrimaryIncreasing + *******************************************************************/ +gboolean +xaccQueryGetSortPrimaryIncreasing (Query *q) +{ + if (!q) return TRUE; + return q->primary_increasing; +} + +/******************************************************************* + * xaccQueryGetSortSecondaryIncreasing + *******************************************************************/ +gboolean +xaccQueryGetSortSecondaryIncreasing (Query *q) +{ + if (!q) return TRUE; + return q->secondary_increasing; +} + +/******************************************************************* + * xaccQueryGetSortTertiaryIncreasing + *******************************************************************/ +gboolean +xaccQueryGetSortTertiaryIncreasing (Query *q) +{ + if (!q) return TRUE; + return q->tertiary_increasing; +} + +/******************************************************************* + * xaccQuerySetMaxSplits + *******************************************************************/ +void +xaccQuerySetMaxSplits(Query * q, int n) { + if (!q) return; + q->max_splits = n; +} + +int +xaccQueryGetMaxSplits(Query * q) { + if (!q) return 0; + return q->max_splits; +} + + +/******************************************************************* + * xaccQuerySetGroup + *******************************************************************/ +void +xaccQuerySetGroup(Query * q, AccountGroup * g) { + if (!q) return; + q->acct_group = g; +} + + +/******************************************************************* + * xaccQueryGetGroup + *******************************************************************/ +AccountGroup * +xaccQueryGetGroup(Query * q) { + if (!q) return NULL; + return (q->acct_group); +} + + +/******************************************************************* + * xaccQueryGetEarliestDateFound + *******************************************************************/ +time_t +xaccQueryGetEarliestDateFound(Query * q) { + GList * spl; + Split * sp; + time_t earliest = LONG_MAX; + + if (!q) return 0; + if (!q->split_list) return 0; + + for(spl = q->split_list; spl; spl=spl->next) { + sp = spl->data; + if(sp->parent->date_posted.tv_sec < earliest) { + earliest = (time_t) sp->parent->date_posted.tv_sec; + } + } + return earliest; +} + +/******************************************************************* + * xaccQueryGetEarliestDateFound + *******************************************************************/ +time_t +xaccQueryGetLatestDateFound(Query * q) { + Split * sp; + GList * spl; + time_t latest = 0; + + if(!q) return 0; + if(!q->split_list) return 0; + + for(spl = q->split_list; spl; spl=spl->next) { + sp = spl->data; + if(sp->parent->date_posted.tv_sec > latest) { + latest = (time_t) sp->parent->date_posted.tv_sec; + } + } + return latest; +} + +#if 0 +int +main(int argc, char ** argv) { + Query * q = xaccMallocQuery(); + Query * q2 = xaccMallocQuery(); + Query * r; + + printf("testing queries...\n"); + + xaccQueryAddMiscMatch(q, NULL, 1); + xaccQueryAddMiscMatch(q, NULL, 2); + xaccQueryAddMiscMatch(q, NULL, 3); + print_query(q); + + xaccQueryAddMiscMatch(q2, NULL, 4); + xaccQueryAddMiscMatch(q2, NULL, 5); + xaccQueryAddMiscMatch(q2, NULL, 6); + print_query(q2); + + printf("AND of two simple queries:\n"); + + r = xaccQueryMerge(q, q2, QUERY_AND); + print_query(r); + + printf("OR of two simple queries:\n"); + + r = xaccQueryMerge(q, q2, QUERY_OR); + print_query(r); + + printf("NAND of two simple queries:\n"); + r = xaccQueryMerge(q, q2, QUERY_NAND); + print_query(r); + + printf("XOR of two simple queries:\n"); + r = xaccQueryMerge(q, q2, QUERY_XOR); + print_query(r); + +} +#endif diff --git a/src/engine/Query.h b/src/engine/Query.h new file mode 100644 index 0000000000..f5ee1b811c --- /dev/null +++ b/src/engine/Query.h @@ -0,0 +1,348 @@ +/********************************************************************\ + * Query.h : api for finding transactions * + * Copyright 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * +\********************************************************************/ + +#ifndef GNUCASH_QUERY_H +#define GNUCASH_QUERY_H + +#include +#include +#include +#include + +#include "gnc-common.h" +#include "Account.h" +#include "Transaction.h" + +typedef enum { + QUERY_AND=1, + QUERY_OR, + QUERY_NAND, + QUERY_NOR, + QUERY_XOR +} QueryOp; + +typedef enum { + BY_STANDARD=1, + BY_DATE, + BY_DATE_ROUNDED, + BY_DATE_ENTERED, + BY_DATE_ENTERED_ROUNDED, + BY_DATE_RECONCILED, + BY_DATE_RECONCILED_ROUNDED, + BY_NUM, + BY_AMOUNT, + BY_MEMO, + BY_DESC, + BY_RECONCILE, + BY_ACCOUNT_FULL_NAME, + BY_ACCOUNT_CODE, + BY_CORR_ACCOUNT_FULL_NAME, + BY_CORR_ACCOUNT_CODE, + BY_NONE +} sort_type_t; + +typedef enum { + PD_DATE=1, + PD_AMOUNT, + PD_ACCOUNT, + PD_STRING, + PD_CLEARED, + PD_BALANCE, + PD_GUID, + PD_MISC +} pd_type_t; + +typedef enum { + PR_ACCOUNT=1, + PR_ACTION, + PR_AMOUNT, /* FIXME: misnamed, should be PR_VALUE */ + PR_BALANCE, + PR_CLEARED, + PR_DATE, + PR_DESC, + PR_GUID, + PR_MEMO, + PR_MISC, + PR_NUM, + PR_PRICE, + PR_SHRS /* FIXME: misnamed, should be PR_AMT */ +} pr_type_t; + +typedef enum { + ACCT_MATCH_ALL=1, + ACCT_MATCH_ANY, + ACCT_MATCH_NONE +} acct_match_t; + +typedef enum +{ + AMT_MATCH_ATLEAST=1, + AMT_MATCH_ATMOST, + AMT_MATCH_EXACTLY +} amt_match_t; + +typedef enum { + AMT_SGN_MATCH_EITHER=1, + AMT_SGN_MATCH_CREDIT, + AMT_SGN_MATCH_DEBIT +} amt_match_sgn_t; + +typedef enum { + CLEARED_NO = 1 << 0, + CLEARED_CLEARED = 1 << 1, + CLEARED_RECONCILED = 1 << 2, + CLEARED_FROZEN = 1 << 3 +} cleared_match_t; + +enum { + STRING_MATCH_CASE = 1 << 0, + STRING_MATCH_REGEXP = 1 << 1 +}; + +typedef enum { + BALANCE_BALANCED = 1 << 0, + BALANCE_UNBALANCED = 1 << 1 +} balance_match_t; + +/* query_run_t describes whether to require all splits or + * any to match for a transaction to be returned by + * xaccQueryGetTransactions */ + +typedef enum { + QUERY_MATCH_ALL=1, + QUERY_MATCH_ANY=2 +} query_run_t; + +typedef struct _querystruct Query; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; +} BasePredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + int use_start; + Timespec start; + int use_end; + Timespec end; +} DatePredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + amt_match_t how; + amt_match_sgn_t amt_sgn; + double amount; +} AmountPredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + acct_match_t how; + GList *accounts; + GList *account_guids; +} AccountPredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + int case_sens; + int use_regexp; + char *matchstring; + regex_t compiled; +} StringPredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + cleared_match_t how; +} ClearedPredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + balance_match_t how; +} BalancePredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + GUID guid; +} GUIDPredicateData; + +typedef struct { + pd_type_t type; + pr_type_t term_type; + int sense; + int how; + int data; +} MiscPredicateData; + +typedef union { + pd_type_t type; + BasePredicateData base; + DatePredicateData date; + AmountPredicateData amount; + AccountPredicateData acct; + StringPredicateData str; + ClearedPredicateData cleared; + BalancePredicateData balance; + GUIDPredicateData guid; + MiscPredicateData misc; +} PredicateData; + +typedef int (* Predicate)(Split * to_test, PredicateData * test_data); + +typedef struct { + PredicateData data; + Predicate p; +} QueryTerm; + + +/******************************************************************* + * basic Query API + *******************************************************************/ + +Query * xaccMallocQuery(void); +void xaccFreeQuery(Query *); +Query * xaccQueryCopy(Query *q); +void xaccQuerySetGroup(Query * q, AccountGroup * group); +AccountGroup *xaccQueryGetGroup(Query * q); + +Query * xaccQueryInvert(Query * q1); +Query * xaccQueryMerge(Query * q1, Query * q2, QueryOp op); +void xaccQueryClear(Query * q); + +/* The xaccQueryHasTerms() routine returns the number of 'OR' terms in the query. + * The xaccQueryNumTerms() routine returns the total number of terms in the query. + */ + +void xaccQueryPurgeTerms(Query * q, pd_type_t type); +int xaccQueryHasTerms(Query * q); +gboolean xaccQueryHasTermType(Query * q, pd_type_t type); +GList * xaccQueryGetTerms(Query * q); +int xaccQueryNumTerms(Query * q); + + +/* after the query has been set up, call this to run the query */ +GList * xaccQueryGetSplits(Query * q); +GList * xaccQueryGetSplitsUniqueTrans(Query *q); +GList * xaccQueryGetTransactions(Query * q, query_run_t type); + +/* compare two queries for equality. this is a simplistic + * implementation -- logical equivalences between different + * and/or trees are ignored. */ +gboolean xaccQueryEqual(Query *q1, Query *q2); + +/* handy for debugging */ +void xaccQueryPrint(Query *q); + +/******************************************************************* + * match-adding API + *******************************************************************/ + +void xaccQueryAddAccountMatch(Query * q, GList * accounts, + acct_match_t how, QueryOp op); +void xaccQueryAddAccountGUIDMatch(Query * q, GList * account_guids, + acct_match_t how, QueryOp op); +void xaccQueryAddSingleAccountMatch(Query * q, Account * acct, + QueryOp op); + +void xaccQueryAddDescriptionMatch(Query * q, const char * matchstring, + int case_sens, int use_regexp, QueryOp op); +void xaccQueryAddNumberMatch(Query * q, const char * matchstring, + int case_sens, int use_regexp, QueryOp op); +void xaccQueryAddActionMatch(Query * q, const char * matchstring, + int case_sens, int use_regexp, QueryOp op); +void DxaccQueryAddAmountMatch(Query * q, double amount, + amt_match_sgn_t amt_sgn, + amt_match_t how, QueryOp op); +void DxaccQueryAddSharePriceMatch(Query * q, double amount, + amt_match_t how, QueryOp op); +void DxaccQueryAddSharesMatch(Query * q, double amount, + amt_match_t how, QueryOp op); +void xaccQueryAddDateMatch(Query * q, + int use_start, int sday, int smonth, int syear, + int use_end, int eday, int emonth, int eyear, + QueryOp op); +void xaccQueryAddDateMatchTS(Query * q, + int use_start, Timespec sts, + int use_end, Timespec ets, + QueryOp op); +void xaccQueryAddDateMatchTT(Query * q, + int use_start, time_t stt, + int use_end, time_t ett, + QueryOp op); +void xaccQueryAddMemoMatch(Query * q, const char * matchstring, + int case_sens, int use_regexp, QueryOp op); +void xaccQueryAddClearedMatch(Query * q, cleared_match_t how, QueryOp op); +void xaccQueryAddBalanceMatch(Query * q, balance_match_t how, QueryOp op); +void xaccQueryAddGUIDMatch(Query * q, const GUID *guid, QueryOp op); +void xaccQueryAddMiscMatch(Query * q, Predicate p, int how, int data, + QueryOp op); + +void xaccQueryAddPredicate (Query * q, PredicateData *pred, QueryOp op); + + +/******************************************************************* + * sort-related functions + *******************************************************************/ + +void xaccQuerySetSortOrder(Query * q, sort_type_t primary, + sort_type_t secondary, sort_type_t tertiary); +sort_type_t xaccQueryGetPrimarySortOrder(Query * q); +sort_type_t xaccQueryGetSecondarySortOrder(Query * q); +sort_type_t xaccQueryGetTertiarySortOrder(Query * q); + +void xaccQuerySetSortIncreasing(Query * q, + gboolean prim_increasing, + gboolean sec_increasing, + gboolean tert_increasing); +gboolean xaccQueryGetSortPrimaryIncreasing (Query *q); +gboolean xaccQueryGetSortSecondaryIncreasing (Query *q); +gboolean xaccQueryGetSortTertiaryIncreasing (Query *q); + +void xaccQuerySetMaxSplits(Query * q, int n); +int xaccQueryGetMaxSplits(Query * q); + + +/******************************************************************* + * compatibility interface with old Query API + *******************************************************************/ +time_t xaccQueryGetEarliestDateFound(Query * q); +time_t xaccQueryGetLatestDateFound(Query * q); + + +/* This is useful for network systems */ +Predicate xaccQueryGetPredicate (pr_type_t term_type); + +#endif diff --git a/src/engine/README b/src/engine/README new file mode 100644 index 0000000000..b3aaf6b636 --- /dev/null +++ b/src/engine/README @@ -0,0 +1,14 @@ + +This directory contains code for the accounting engine. +Its fairly clean but far from perfect, and it certainly +lacks advanced features. + +There should be no GUI code in this subdirectory, and, +ideally, it should build cleanly and independently of +any GUI elements or assumptions. + +For design documentation, please see the file "design.txt", +and also, look at the header files carefully. The documentation +for each routine is in the header files for that routine. + +September 1998 diff --git a/src/engine/README.query-api b/src/engine/README.query-api new file mode 100644 index 0000000000..b2a7e898aa --- /dev/null +++ b/src/engine/README.query-api @@ -0,0 +1,205 @@ +Gnucash Query API + + +BASIC QUERY API: With this API you can create arbitrary logical +queries to find sets of splits in an account group. To make simple +queries (1 term, such as an account query), create the appropriate +QueryTerm structure and stick it in a Query object using +xaccInitQuery. The QueryTerm should be malloced but the Query object +will handle freeing it. To make compound queries, make multiple +simple queries and combine them using xaccMergeQuery and the logical +operations of your choice. + +----------------------------------------------------------------- +Query * xaccMallocQuery() + +Allocates and initializes a Query structure which must be freed by the +user with xaccFreeQuery. A newly-allocated Query object matches +nothing (xaccQueryGetSplits will return NULL). + +----------------------------------------------------------------- +void xaccInitQuery(Query * q, QueryTerm * qt) + +Initializes an allocated Query object with initial term qt (possibly +NULL). Any previous query terms are freed. + +----------------------------------------------------------------- +void xaccFreeQuery(Query * q) + +Frees the resources associate with a Query object. + +----------------------------------------------------------------- +void xaccQuerySetGroup(Query * q, AccountGroup * group) + +Set the Gnucash account group that the query applies to. +xaccQuerySetGroup must be called before a Query object created with +xaccMallocQuery can be used. Queries created with xaccQueryInvert and +xaccQueryMerge inherit the account group of the arguments to those +functions. + +----------------------------------------------------------------- +Query * xaccQueryInvert(Query * q) + +Logically invert the query. xaccInvertQuery returns a newly allocated +Query object such that the union of the splits matched by query q and +query (p = xaccQueryInvert(q)) is the entire account group that q +applies to. + +----------------------------------------------------------------- +Query * xaccQueryMerge(Query * q1, Query * q2, QueryOp how) + +Combine queries q1 and q2 using logical operator 'how'. 'how' must be +one of QUERY_AND, QUERY_OR, QUERY_NAND, QUERY_NOR, QUERY_XOR. The +account groups of q1 and q2 must be the same. xaccQueryMerge returns +a newly-allocated Query object or NULL on error. + +----------------------------------------------------------------- +void xaccQueryClear(Query * q) + +Remove all query terms from q. q matches nothing after xaccQueryClear. + +----------------------------------------------------------------- +void xaccQueryPurgeTerms(Query * q, pd_type_t type); + +Remove query terms of a particular type from q. The "type" of a term +is determined by the type of data that gets passed to the predicate +function. The currently-supported values of 'type' are PD_DATE, +PD_AMOUNT, PD_ACCOUNT, PD_STRING, PD_CLEARED, PD_MISC. This function +is really only used in one place: in window-register.c, to modify +in-place a query to remove any date tests prior to adding new ones. +This should probably be removed from the API in favor of an extra +argument to xaccQueryMerge specifying what to do with existing terms +of that type. + + +----------------------------------------------------------------- +int xaccQueryHasTerms(Query * q) + +Returns the number of terms in the canonical form of the query. Can +be used as a predicate to see if the query has been initialized +(return value > 0) or is "blank" (return value == 0). + + +----------------------------------------------------------------- + +CONVENIENCE API: The remainder of the API (in particular, any function +called xaccQueryAdd***Match) is a set of convenience functions for +creating and modifying specific types of queries. All of these +functions can be duplicated using the Basic API specified above, +directly manipulating QueryTerm objects and creating and merging +queries as needed. One slight advantage of the convenience API is +that it uses a standard set of predicates that are more-or-less +opaque. This may be important later. + +It's probably more useful to describe the various types of +PredicateData than the convenience functions, which are pretty +self-explanatory once you understand what the underlying process is. +For example, AddMemoMatch and AddDescriptionMatch are essentially the +same function because they both use PD_STRING predicate data; they +just use a different predicate (one compares data.string.matchstring +with the split's Memo, one compares with the parent transaction's +Description). + +Each function in the convenience API takes a Query *, some arguments +which fill in the fields of the appropriate PredicateData type, and a +QueryOp. The Query object is modified in place, using the logical +operation specified by the QueryOp to combine a single new QueryTerm +with the existing Query. This works by making a new Query of one term +and combining with the existing Query using xaccQueryMerge and the +specified QueryOp. If you have an existing Query (a + b + c) and +combine using QueryOp QUERY_AND in a convenience function representing +predicate d, you will get (ad + bd + cd). + + +STRUCTURE OF A QUERY: A Query is a logical function of any number of +QueryTerms. A QueryTerm consists of a C function pointer (the +Predicate) and a PredicateData structure containing data passed to the +predicate funtion. The PredicateData structure is a constant +associated with the Term and is identical for every Split that is +tested. + +The terms of the Query may represent any logical function and are +stored in canonical form, i.e. the function is expressed as a logical +sum of logical products. So if you have QueryTerms a, b, c, d, e and +you have the logical function a(b+c) + !(c(d+e)), it gets stored as +ab + ac + !c + !c!e +!d!c + !d!e. This may not be optimal for evaluation +of some functions but it's easy to store, easy to manipulate, and it +doesn't require a complete algebra system to deal with. + +The representation is of a GList of GLists of QueryTerms. The +"backbone" GList q->terms represents the OR-chain, and every item on +the backbone is a GList of QueryTerms representing an AND-chain +corresponding to a single product-term in the canonical +representation. QueryTerms are duplicated when necessary to fill out +the canonical form, and the same predicate may be evaluated multiple +times per split for complex queries. This is a place where we could +probably optimize. + +Evaluation of a Query (see xaccQueryGetSplits) is optimized as much as +possible by short-circuited evaluation. The predicates in each +AND-chain are sorted by predicate type, with Account queries sorted +first to allow the evaluator to completely eliminate accounts from the +search if there's no chance of them having splits that match. + + +PREDICATE DATA TYPES: All the predicate data types are rolled up into +the union type PredicateData. The "type" field specifies which type +the union is. The values of type are: + +----------------------------------------------------------------- +PD_DATE : match a date range. Specify a start date and an end date. + +Used in: xaccQueryAddDateMatch + xaccQueryAddDateMatchTS + xaccQueryAddDateMatchTT + +----------------------------------------------------------------- +PD_AMOUNT : match a numeric amount. Specify an amount (always +positive), a funds-flow direction (credit, debit, or either), and +"how", specifying the type of amount comparison to be used : + + AMT_MATCH_ATLEAST : split >= pd amount + AMT_MATCH_ATMOST : split >= pd amount + AMT_MATCH_EXACTLY : split == pd amount + +Used in: xaccQueryAddAmountMatch + xaccQueryAddSharePriceMatch + xaccQueryAddSharesMatch + +----------------------------------------------------------------- +PD_ACCOUNT : match an account or set of accounts. Specify a set +of accounts and "how": + + ACCT_MATCH_ALL : a transaction must have at least one split + affecting each account in pd.acct.accounts. + ACCT_MATCH_ANY : a transaction must have at least one split + affecting any account in the set + ACCT_MATCH_NONE : a transaction may not affect any account in + the set. + +Used in: xaccQueryAddAccountMatch + xaccQueryAddSingleAccountMatch + +----------------------------------------------------------------- +PD_STRING : match a string. Specify a string, bool signifying +case sensitivity, bool signifying regexp or simple string. + +Used in: xaccQueryAddDescriptionMatch + xaccQueryAddNumberMatch + xaccQueryAddActionMatch + xaccQueryAddMemoMatch + +----------------------------------------------------------------- +PD_CLEARED : match the Cleared state of the transaction. Specify +a bit-mask that is an OR combination of one or more of the +following: + CLEARED_NO (state == 'n') + CLEARED_CLEARED (state == 'c') + CLEARED_RECONCILED (state == 'y') + +Used in: xaccQueryAddClearedMatch + +----------------------------------------------------------------- +PD_MISC : match some "other" user predicate. Not used at the moment. + +----------------------------------------------------------------- diff --git a/src/engine/SchedXaction.c b/src/engine/SchedXaction.c new file mode 100644 index 0000000000..3b80aca3fd --- /dev/null +++ b/src/engine/SchedXaction.c @@ -0,0 +1,486 @@ +/********************************************************************\ + * SchedXaction.c -- Scheduled Transaction implementation. * + * Copyright (C) 2001 Joshua Sled * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#include "config.h" + +#include +#include + +#include "SchedXaction.h" +#include "FreqSpec.h" +#include "GNCIdP.h" +#include "Transaction.h" +#include "TransactionP.h" +#include "date.h" +#include "gnc-engine-util.h" +#include "gnc-event-p.h" +#include "messages.h" +#include "Account.h" +#include "Group.h" +#include "guid.h" +#include "gnc-book.h" +#include "FileDialog.h" + +static short module = MOD_SX; + +/** Local data defs *****/ +void sxprivtransactionListMapDelete( gpointer data, gpointer user_data ); + +/** Local Prototypes *****/ + + +static void +xaccSchedXactionInit( SchedXaction *sx, GNCBook *book) +{ + AccountGroup *ag; + char *name; + sx->freq = xaccFreqSpecMalloc(); + + xaccGUIDNew( &sx->guid ); + xaccStoreEntity( sx, &sx->guid, GNC_ID_SCHEDXACTION ); + g_date_clear( &sx->last_date, 1 ); + g_date_clear( &sx->start_date, 1 ); + g_date_clear( &sx->end_date, 1 ); + + sx->num_occurances_total = -1; + sx->kvp_data = kvp_frame_new(); + sx->autoCreateOption = FALSE; + sx->autoCreateNotify = FALSE; + sx->advanceCreateDays = 0; + sx->advanceRemindDays = 0; + sx->dirty = TRUE; + + /* create a new template account for our splits */ + sx->template_acct = xaccMallocAccount(); + name = guid_to_string( &sx->guid ); + xaccAccountSetName( sx->template_acct, name ); + xaccAccountSetCommodity( sx->template_acct, + gnc_commodity_new( "template", "template", + "template", "template", 1 ) ); + g_free( name ); + xaccAccountSetType( sx->template_acct, BANK ); + ag = gnc_book_get_template_group( book ); + xaccGroupInsertAccount( ag, sx->template_acct ); +} + +SchedXaction* +xaccSchedXactionMalloc( GNCBook *book ) +{ + SchedXaction *sx; + sx = g_new0( SchedXaction, 1 ); + xaccSchedXactionInit( sx, book ); + gnc_engine_generate_event( &sx->guid, GNC_EVENT_CREATE ); + return sx; +} + +void +sxprivtransactionListMapDelete( gpointer data, gpointer user_data ) +{ + Transaction *t = (Transaction*)data; + xaccTransBeginEdit( t ); + xaccTransDestroy( t ); + xaccTransCommitEdit( t ); + return; +} + +void +sxprivsplitListMapDelete( gpointer data, gpointer user_data ) +{ + Split *s = (Split *) data; + Transaction *t = xaccSplitGetParent(s); + xaccTransBeginEdit( t ); + xaccTransDestroy( t ); + xaccTransCommitEdit( t ); +} + +void +xaccSchedXactionFree( SchedXaction *sx ) +{ + AccountGroup *group; + GList *templ_acct_splits; + if ( sx == NULL ) return; + + xaccFreqSpecFree( sx->freq ); + gnc_engine_generate_event( &sx->guid, GNC_EVENT_DESTROY ); + xaccRemoveEntity( &sx->guid ); + + if ( sx->name ) + g_free( sx->name ); + + + + /* + * we have to delete the transactions in the + * template account ourselves + */ + + templ_acct_splits + = xaccAccountGetSplitList(sx->template_acct); + + g_list_foreach(templ_acct_splits, + sxprivsplitListMapDelete, + NULL); + + /* + * xaccAccountDestroy removes the account from + * its group for us AFAICT + */ + + xaccAccountBeginEdit(sx->template_acct); + xaccAccountDestroy(sx->template_acct); + + g_free( sx ); + + return; +} + + + + + +FreqSpec * +xaccSchedXactionGetFreqSpec( SchedXaction *sx ) +{ + return sx->freq; +} + +void +xaccSchedXactionSetFreqSpec( SchedXaction *sx, FreqSpec *fs ) +{ + g_return_if_fail( fs ); + + xaccFreqSpecFree( sx->freq ); + sx->freq = fs; + sx->dirty = TRUE; +} + +gchar * +xaccSchedXactionGetName( SchedXaction *sx ) +{ + return sx->name; +} + +void +xaccSchedXactionSetName( SchedXaction *sx, const gchar *newName ) +{ + g_return_if_fail( newName != NULL ); + if ( sx->name != NULL ) { + g_free( sx->name ); + sx->name = NULL; + } + sx->dirty = TRUE; + sx->name = g_strdup( newName ); +} + +GDate* +xaccSchedXactionGetStartDate( SchedXaction *sx ) +{ + return &sx->start_date; +} + +void +xaccSchedXactionSetStartDate( SchedXaction *sx, GDate* newStart ) +{ + sx->start_date = *newStart; + sx->dirty = TRUE; +} + +gboolean +xaccSchedXactionHasEndDate( SchedXaction *sx ) +{ + return g_date_valid( &sx->end_date ); +} + +GDate* +xaccSchedXactionGetEndDate( SchedXaction *sx ) +{ + return &sx->end_date; +} + +void +xaccSchedXactionSetEndDate( SchedXaction *sx, GDate *newEnd ) +{ + if ( g_date_valid( newEnd ) ) { + if ( g_date_compare( newEnd, &sx->start_date ) < 0 ) { + /* XXX: I reject the bad data - is this the right + * thing to do . + * This warning is only human readable - the caller + * doesn't know the call failed. This is bad + */ + PWARN( "New end date before start date" ); + } + else + { + sx->end_date = *newEnd; + sx->dirty = TRUE; + } + + } + else + { + PWARN("New end date invalid"); + } + return; +} + +GDate* +xaccSchedXactionGetLastOccurDate( SchedXaction *sx ) +{ + return &sx->last_date; +} + +void +xaccSchedXactionSetLastOccurDate( SchedXaction *sx, GDate* newLastOccur ) +{ + sx->last_date = *newLastOccur; + sx->dirty = TRUE; + return; +} + +gboolean +xaccSchedXactionHasOccurDef( SchedXaction *sx ) +{ + return ( xaccSchedXactionGetNumOccur( sx ) != 0 ); +} + +gint +xaccSchedXactionGetNumOccur( SchedXaction *sx ) +{ + return sx->num_occurances_total; +} + +void +xaccSchedXactionSetNumOccur( SchedXaction *sx, gint newNum ) +{ + sx->num_occurances_remain = sx->num_occurances_total = newNum; + sx->dirty = TRUE; + +} + +gint +xaccSchedXactionGetRemOccur( SchedXaction *sx ) +{ + return sx->num_occurances_remain; +} + +void +xaccSchedXactionSetRemOccur( SchedXaction *sx, + gint numRemain ) +{ + /* FIXME This condition can be tightened up */ + if ( numRemain > sx->num_occurances_total ) { + PWARN("The number remaining is greater than the \ +total occurrences"); + + } + else + { + sx->num_occurances_remain = numRemain; + sx->dirty = TRUE; + } + return; +} + + +kvp_value * +xaccSchedXactionGetSlot( SchedXaction *sx, const char *slot ) +{ + if (!sx) + { + return NULL; + } + + return kvp_frame_get_slot(sx->kvp_data, slot); +} + +void +xaccSchedXactionSetSlot( SchedXaction *sx, + const char *slot, + const kvp_value *value ) +{ + if (!sx) + { + return; + } + + kvp_frame_set_slot( sx->kvp_data, slot, value ); + sx->dirty = TRUE; + return; +} + +kvp_frame* +xaccSchedXactionGetSlots( SchedXaction *sx ) +{ + return sx->kvp_data; +} + +void +xaccSchedXactionSetSlots( SchedXaction *sx, kvp_frame *frm ) +{ + sx->kvp_data = frm; + sx->dirty = TRUE; +} + +const GUID* +xaccSchedXactionGetGUID( SchedXaction *sx ) +{ + return &sx->guid; +} + +void +xaccSchedXactionSetGUID( SchedXaction *sx, GUID g ) +{ + sx->guid = g; + sx->dirty = TRUE; +} + +void +xaccSchedXactionGetAutoCreate( SchedXaction *sx, + gboolean *outAutoCreate, + gboolean *outNotify ) +{ + *outAutoCreate = sx->autoCreateOption; + *outNotify = sx->autoCreateNotify; + return; +} + +void +xaccSchedXactionSetAutoCreate( SchedXaction *sx, + gboolean newAutoCreate, + gboolean newNotify ) +{ + + sx->autoCreateOption = newAutoCreate; + sx->autoCreateNotify = newNotify; + sx->dirty = TRUE; + return; +} + +gint +xaccSchedXactionGetAdvanceCreation( SchedXaction *sx ) +{ + return sx->advanceCreateDays; +} + +void +xaccSchedXactionSetAdvanceCreation( SchedXaction *sx, gint createDays ) +{ + sx->advanceCreateDays = createDays; + sx->dirty = TRUE; +} + +gint +xaccSchedXactionGetAdvanceReminder( SchedXaction *sx ) +{ + return sx->advanceRemindDays; +} + +void +xaccSchedXactionSetAdvanceReminder( SchedXaction *sx, gint reminderDays ) +{ + sx->dirty = TRUE; + sx->advanceRemindDays = reminderDays; +} + +GDate +xaccSchedXactionGetNextInstance( SchedXaction *sx ) +{ + GDate last_occur, next_occur, tmpDate; + + g_date_clear( &last_occur, 1 ); + g_date_clear( &next_occur, 1 ); + g_date_clear( &tmpDate, 1 ); + + if ( g_date_valid( &sx->last_date ) ) { + last_occur = sx->last_date; + } + + if ( g_date_valid( &sx->start_date ) ) { + if ( g_date_valid(&last_occur) ) { + last_occur = + ( g_date_compare( &last_occur, + &sx->start_date ) > 0 ? + last_occur : sx->start_date ); + } else { + last_occur = sx->start_date; + } + } + +#if 0 + if ( g_date_valid( &last_occur ) ) { + g_date_set_time( &tmpDate, time(NULL) ); + last_occur = + ( g_date_compare( &last_occur, + &tmpDate ) > 0 ? + last_occur : tmpDate ); + } else { + g_date_set_time( &last_occur, time(NULL) ); + } +#endif /* 0 */ + + if ( g_date_valid( &sx->start_date ) + && ! g_date_valid( &sx->last_date ) ) { + /* Think about this for a second, and you realize + * that if the start date is _today_, we need a + * last-occur date such that the 'next instance' is + * after that date... one day should be good. + * + * This only holds for the first instance [read: if the + * last[-occur]_date is invalid. */ + g_date_subtract_days( &last_occur, 1 ); + } + + xaccFreqSpecGetNextInstance( sx->freq, &last_occur, &next_occur ); + return next_occur; +} + +GDate xaccSchedXactionGetInstanceAfter( SchedXaction *sx, GDate *date ) +{ + GDate next_occur; + xaccFreqSpecGetNextInstance( sx->freq, date, &next_occur ); + return next_occur; +} + +/* + * XXX: This does what you want, I think + */ + +GList * +xaccSchedXactionGetSplits( SchedXaction *sx ) +{ + g_return_val_if_fail( sx, NULL ); + return xaccAccountGetSplitList(sx->template_acct); +} + +void +xaccSchedXactionSetDirtyness( SchedXaction *sx, gboolean dirty_p) +{ + sx->dirty = dirty_p; + return; +} + +gboolean +xaccSchedXactionIsDirty(SchedXaction *sx) +{ + return sx->dirty; +} diff --git a/src/engine/SchedXaction.h b/src/engine/SchedXaction.h new file mode 100644 index 0000000000..c4845521fb --- /dev/null +++ b/src/engine/SchedXaction.h @@ -0,0 +1,214 @@ +/********************************************************************\ + * SchedXaction.h -- Scheduled Transaction * + * Copyright (C) 2001 Joshua Sled * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef XACC_SCHEDXACTION_H +#define XACC_SCHEDXACTION_H + +#include "config.h" + +#include +#include +#include "GNCId.h" +#include "FreqSpec.h" +#include "date.h" +#include "kvp_frame.h" +#include "gnc-book.h" + +/** + * A single scheduled transaction. + * + * Scheduled transactions have a list of transactions, and a frequency + * [and associated date anchors] with which they are scheduled. + * + * Things that make sense to have in a template transaction: + * [not] Date [though eventually some/multiple template transactions + * might have relative dates]. + * Memo + * Account + * Funds In/Out... or an expr involving 'amt' [A, x, y, a?] for + * variable expenses. + * + * Template transactions are instantiated by: + * . copying the fields of the template + * . setting the date to the calculated "due" date. + * + * We should be able to use the GeneralLedger [or, yet-another-subtype + * of the internal ledger] for this editing. + **/ +typedef struct gncp_SchedXaction { + gchar *name; + + FreqSpec *freq; + + GDate last_date; + + GDate start_date; + /* if end_date is invalid, then no end. */ + GDate end_date; + + /* if num_occurances_total == 0, then no limit */ + gint num_occurances_total; + /* reminaing occurances are as-of the 'last_date'. */ + gint num_occurances_remain; + + gboolean autoCreateOption; + gboolean autoCreateNotify; + gint advanceCreateDays; + gint advanceRemindDays; + + Account *template_acct; + GUID guid; + + /* Changed since last save? */ + gboolean dirty; + + kvp_frame *kvp_data; + + +} SchedXaction; + +/** + * Creates and initializes a scheduled transaction. + **/ +SchedXaction *xaccSchedXactionMalloc( GNCBook *book); + +/* + * returns true if the scheduled transaction is dirty and needs to + * be saved + */ + +gboolean xaccSchedXactionIsDirty(SchedXaction *sx); + +/* + * Set dirtyness state. Only save/load code should modify this outside + * SX engine CODE . . . + * (set it to FALSE after backend completes reading in data + * + * FIXME: put this into a private header . . . . + */ + +void xaccSchedXactionSetDirtyness(SchedXaction *sx, gboolean dirty_p); +/* + * Cleans up and frees a SchedXaction and it's associated data. + **/ +void xaccSchedXactionFree( SchedXaction *sx ); + +FreqSpec *xaccSchedXactionGetFreqSpec( SchedXaction *sx ); +/** + * The FreqSpec is given to the SchedXaction for mem mgmt; it should + * not be freed by the external code. + **/ +void xaccSchedXactionSetFreqSpec( SchedXaction *sx, FreqSpec *fs ); + +gchar *xaccSchedXactionGetName( SchedXaction *sx ); +/** + * A copy of the name is made. + **/ +void xaccSchedXactionSetName( SchedXaction *sx, const gchar *newName ); + +GDate* xaccSchedXactionGetStartDate( SchedXaction *sx ); +void xaccSchedXactionSetStartDate( SchedXaction *sx, GDate* newStart ); + +int xaccSchedXactionHasEndDate( SchedXaction *sx ); +/** + * Returns invalid date when there is no end-date specified. + **/ +GDate* xaccSchedXactionGetEndDate( SchedXaction *sx ); +void xaccSchedXactionSetEndDate( SchedXaction *sx, GDate* newEnd ); + +GDate* xaccSchedXactionGetLastOccurDate( SchedXaction *sx ); +void xaccSchedXactionSetLastOccurDate( SchedXaction *sx, GDate* newLastOccur ); + +/** + * Returns true if the scheduled transaction has a defined number of + * occurances, false if not. + **/ +gboolean xaccSchedXactionHasOccurDef( SchedXaction *sx ); +gint xaccSchedXactionGetNumOccur( SchedXaction *sx ); +void xaccSchedXactionSetNumOccur( SchedXaction *sx, gint numNum ); +gint xaccSchedXactionGetRemOccur( SchedXaction *sx ); +void xaccSchedXactionSetRemOccur( SchedXaction *sx, gint numRemain ); + +GList *xaccSchedXactionGetSplits( SchedXaction *sx ); +void xaccSchedXactionSetSplits( SchedXaction *sx, GList *newSplits ); + +void xaccSchedXactionGetAutoCreate( SchedXaction *sx, gboolean *outAutoCreate, gboolean *outNotify ); +void xaccSchedXactionSetAutoCreate( SchedXaction *sx, gboolean newAutoCreate, gboolean newNotify ); + +gint xaccSchedXactionGetAdvanceCreation( SchedXaction *sx ); +void xaccSchedXactionSetAdvanceCreation( SchedXaction *sx, gint createDays ); + +gint xaccSchedXactionGetAdvanceReminder( SchedXaction *sx ); +void xaccSchedXactionSetAdvanceReminder( SchedXaction *sx, gint reminderDays ); + +#if 0 +#error vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +GList *xaccSchedXactionGetXactions( SchedXaction *sx ); +void xaccSchedXactionClearXactions( SchedXaction *sx ); +void xaccSchedXactionAddXaction( SchedXaction *sx, + Transaction *t ); +#error ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#endif /* 0 */ + + +/* + * The following function is slightly risky. If you change + * the retrieved kvp_frame you must mark the SchedXaction + * dirty with xaccSchedXactionSetDirtyness + */ +kvp_frame *xaccSchedXactionGetSlots( SchedXaction *sx ); +/** + * Sets the SX kvp data to the given kvp_frame. + * NOTE: This is not copied, but set directly. + **/ +void xaccSchedXactionSetSlots( SchedXaction *sx, + kvp_frame *frm ); + + +/** + * Use the following two functions in preference to + * the above two . . . + */ +kvp_value *xaccSchedXactionGetSlot( SchedXaction *sx, + const char *slot ); + +/* + * This function copies value, so you don't have to + */ + +void xaccSchedXactionSetSlot( SchedXaction *sx, + const char *slot, + const kvp_value *value ); + +const GUID *xaccSchedXactionGetGUID( SchedXaction *sx ); +void xaccSchedXactionSetGUID( SchedXaction *sx, GUID g ); + +/** + * Returns the next occurance of a scheduled transaction. If the + * transaction hasn't occured, then it's based off the start date. + * Otherwise, it's based off the last-occurance date. + **/ +GDate xaccSchedXactionGetNextInstance( SchedXaction *sx ); +GDate xaccSchedXactionGetInstanceAfter( SchedXaction *sx, GDate *date ); + +#endif /* XACC_SCHEDXACTION_H */ diff --git a/src/engine/Scrub.c b/src/engine/Scrub.c new file mode 100644 index 0000000000..a30f70aa89 --- /dev/null +++ b/src/engine/Scrub.c @@ -0,0 +1,506 @@ +/********************************************************************\ + * Scrub.c -- convert single-entry accounts into clean double-entry * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * Scrub.c + * + * FUNCTION: + * Provides a set of functions and utilities for scrubbing clean + * single-entry accounts so that they can be promoted into + * self-consistent, clean double-entry accounts. + * + * HISTORY: + * Created by Linas Vepstas December 1998 + * Copyright (c) 1998, 1999, 2000 Linas Vepstas + */ + +#include "config.h" + +#include +#include +#include + +#include "Account.h" +#include "Group.h" +#include "GroupP.h" +#include "Scrub.h" +#include "Transaction.h" +#include "TransactionP.h" +#include "gnc-engine-util.h" +#include "messages.h" + +static short module = MOD_SCRUB; +static Account * GetOrMakeAccount (AccountGroup *root, Transaction *trans, + const char *name_root); + +/* ================================================================ */ + +void +xaccGroupScrubOrphans (AccountGroup *grp) +{ + GList *list; + GList *node; + + if (!grp) + return; + + list = xaccGroupGetAccountList (grp); + + for (node = list; node; node = node->next) + { + Account *account = node->data; + + xaccAccountTreeScrubOrphans (account); + } +} + +void +xaccAccountTreeScrubOrphans (Account *acc) +{ + if (!acc) + return; + + xaccGroupScrubOrphans (xaccAccountGetChildren(acc)); + xaccAccountScrubOrphans (acc); +} + +void +xaccAccountScrubOrphans (Account *acc) +{ + GList *node; + const char *str; + + if (!acc) + return; + + str = xaccAccountGetName (acc); + str = str ? str : "(null)"; + PINFO ("Looking for orphans in account %s \n", str); + + for (node = xaccAccountGetSplitList(acc); node; node = node->next) + { + Split *split = node->data; + + xaccTransScrubOrphans (xaccSplitGetParent (split), + xaccGetAccountRoot (acc)); + } +} + +void +xaccTransScrubOrphans (Transaction *trans, AccountGroup *root) +{ + GList *node; + + if (!trans) + return; + + for (node = xaccTransGetSplitList (trans); node; node = node->next) + { + Split *split = node->data; + Account *account; + Account *orph; + + account = xaccSplitGetAccount (split); + if (account) + continue; + + DEBUG ("Found an orphan \n"); + + orph = GetOrMakeAccount (root, trans, _("Orphan")); + + xaccAccountBeginEdit (orph); + xaccAccountInsertSplit (orph, split); + xaccAccountCommitEdit (orph); + } +} + +/* ================================================================ */ + +void +xaccGroupScrubSplits (AccountGroup *group) +{ + GList *list; + GList *node; + + if (!group) return; + + list = xaccGroupGetAccountList (group); + + for (node = list; node; node = node->next) + { + Account *account = node->data; + + xaccAccountTreeScrubSplits (account); + } +} + +void +xaccAccountTreeScrubSplits (Account *account) +{ + xaccGroupScrubSplits (xaccAccountGetChildren(account)); + xaccAccountScrubSplits (account); +} + +void +xaccAccountScrubSplits (Account *account) +{ + GList *node; + + for (node = xaccAccountGetSplitList (account); node; node = node->next) + xaccSplitScrub (node->data); +} + +void +xaccTransScrubSplits (Transaction *trans) +{ + GList *node; + + if (!trans) + return; + + for (node = trans->splits; node; node = node->next) + xaccSplitScrub (node->data); +} + +void +xaccSplitScrub (Split *split) +{ + Account *account; + Transaction *trans; + gnc_numeric value; + gboolean trans_was_open; + gnc_commodity *commodity; + gnc_commodity *currency; + int scu; + + if (!split) + return; + + trans = xaccSplitGetParent (split); + if (!trans) + return; + + account = xaccSplitGetAccount (split); + if (!account) + { + value = xaccSplitGetValue (split); + + if (gnc_numeric_same (xaccSplitGetAmount (split), + xaccSplitGetValue (split), + value.denom, GNC_RND_ROUND)) + return; + + xaccSplitSetAmount (split, value); + + return; + } + + commodity = xaccAccountGetCommodity (account); + currency = xaccTransGetCurrency (trans); + + if (!commodity || !gnc_commodity_equiv (commodity, currency)) + return; + + scu = MIN (xaccAccountGetCommoditySCU (account), + gnc_commodity_get_fraction (currency)); + + value = xaccSplitGetValue (split); + + if (gnc_numeric_same (xaccSplitGetAmount (split), + value, scu, GNC_RND_ROUND)) + return; + + PINFO ("split with mismatched values"); + + trans_was_open = xaccTransIsOpen (trans); + + if (!trans_was_open) + xaccTransBeginEdit (trans); + + xaccSplitSetAmount (split, value); + + if (!trans_was_open) + xaccTransCommitEdit (trans); +} + +/* ================================================================ */ + +void +xaccGroupScrubImbalance (AccountGroup *grp) +{ + GList *list; + GList *node; + + if (!grp) return; + + list = xaccGroupGetAccountList (grp); + + for (node = list; node; node = node->next) + { + Account *account = node->data; + + xaccAccountTreeScrubImbalance (account); + } +} + +void +xaccAccountTreeScrubImbalance (Account *acc) +{ + xaccGroupScrubImbalance (xaccAccountGetChildren(acc)); + xaccAccountScrubImbalance (acc); +} + +void +xaccAccountScrubImbalance (Account *acc) +{ + GList *node; + const char *str; + + str = xaccAccountGetName(acc); + str = str ? str : "(null)"; + PINFO ("Looking for imbalance in account %s \n", str); + + for(node = xaccAccountGetSplitList(acc); node; node = node->next) + { + Split *split = node->data; + Transaction *trans = xaccSplitGetParent(split); + + xaccTransScrubImbalance (trans, xaccGetAccountRoot (acc), NULL); + } +} + +void +xaccTransScrubImbalance (Transaction *trans, AccountGroup *root, + Account *parent) +{ + Split *balance_split = NULL; + gnc_numeric imbalance; + + if (!trans) + return; + + xaccTransScrubSplits (trans); + + { + Account *account; + GList *node; + + imbalance = xaccTransGetImbalance (trans); + if (gnc_numeric_zero_p (imbalance)) + return; + + if (!parent) + account = GetOrMakeAccount (root, trans, _("Imbalance")); + else + account = parent; + + for (node = xaccTransGetSplitList (trans); node; node = node->next) + { + Split *split = node->data; + + if (xaccSplitGetAccount (split) == account) + { + balance_split = split; + break; + } + } + + /* put split into account before setting split value */ + if (!balance_split) + { + balance_split = xaccMallocSplit (); + + xaccAccountBeginEdit (account); + xaccAccountInsertSplit (account, balance_split); + xaccAccountCommitEdit (account); + } + } + + PINFO ("unbalanced transaction"); + + { + const gnc_commodity *currency; + const gnc_commodity *commodity; + gboolean trans_was_open; + gnc_numeric new_value; + Account *account; + + trans_was_open = xaccTransIsOpen (trans); + + if (!trans_was_open) + xaccTransBeginEdit (trans); + + currency = xaccTransGetCurrency (trans); + account = xaccSplitGetAccount (balance_split); + + new_value = xaccSplitGetValue (balance_split); + + new_value = gnc_numeric_sub (new_value, imbalance, + new_value.denom, GNC_RND_ROUND); + + xaccSplitSetValue (balance_split, new_value); + + commodity = xaccAccountGetCommodity (account); + if (gnc_commodity_equiv (currency, commodity)) + xaccSplitSetAmount (balance_split, new_value); + + if (!parent && gnc_numeric_zero_p (new_value)) + { + xaccSplitDestroy (balance_split); + balance_split = NULL; + } + + if (balance_split) + xaccTransAppendSplit (trans, balance_split); + + xaccSplitScrub (balance_split); + + if (!trans_was_open) + xaccTransCommitEdit (trans); + } +} + +/* ================================================================ */ + +void +xaccTransScrubCurrency (Transaction *trans) +{ + gnc_commodity *currency; + + if (!trans) return; + + currency = xaccTransGetCurrency (trans); + if (currency) return; + + currency = xaccTransFindOldCommonCurrency (trans); + if (currency) + { + xaccTransBeginEdit (trans); + xaccTransSetCurrency (trans, currency); + xaccTransCommitEdit (trans); + } + else + { + PWARN ("no common transaction currency found"); + } +} + +/* ================================================================ */ + +void +xaccAccountScrubCommodity (Account *account) +{ + gnc_commodity *commodity; + + if (!account) return; + + commodity = xaccAccountGetCommodity (account); + if (commodity) return; + + commodity = DxaccAccountGetSecurity (account); + if (commodity) + { + xaccAccountSetCommodity (account, commodity); + return; + } + + commodity = DxaccAccountGetCurrency (account); + if (commodity) + { + xaccAccountSetCommodity (account, commodity); + return; + } + + PERR ("account with no commodity"); +} + +/* ================================================================ */ + +static gboolean +scrub_trans_currency_helper (Transaction *t, void *unused) +{ + xaccTransScrubCurrency (t); + return TRUE; +} + +static gpointer +scrub_account_commodity_helper (Account *account, gpointer unused) +{ + xaccAccountScrubCommodity (account); + xaccAccountDeleteOldData (account); + return NULL; +} + +void +xaccGroupScrubCommodities (AccountGroup *group) +{ + if (!group) return; + + xaccAccountGroupBeginEdit (group); + + xaccGroupForEachTransaction (group, scrub_trans_currency_helper, NULL); + + xaccGroupForEachAccount (group, scrub_account_commodity_helper, NULL, TRUE); + + xaccAccountGroupCommitEdit (group); +} + +/* ================================================================ */ + +static Account * +GetOrMakeAccount (AccountGroup *root, Transaction *trans, + const char *name_root) +{ + gnc_commodity * currency; + char * accname; + Account * acc; + + /* build the account name */ + currency = xaccTransGetCurrency (trans); + + accname = g_strconcat (name_root, "-", + gnc_commodity_get_mnemonic (currency), NULL); + + /* see if we've got one of these going already ... */ + acc = xaccGetAccountFromName (root, accname); + + if (acc == NULL) + { + /* guess not. We'll have to build one */ + acc = xaccMallocAccount (); + xaccAccountBeginEdit (acc); + xaccAccountSetName (acc, accname); + xaccAccountSetCommodity (acc, currency); + xaccAccountSetType (acc, BANK); + + /* hang the account off the root */ + xaccGroupInsertAccount (root, acc); + xaccAccountCommitEdit (acc); + } + + g_free (accname); + + return acc; +} + +/* ==================== END OF FILE ==================== */ diff --git a/src/engine/Scrub.h b/src/engine/Scrub.h new file mode 100644 index 0000000000..3ca5b811a6 --- /dev/null +++ b/src/engine/Scrub.h @@ -0,0 +1,99 @@ +/********************************************************************\ + * Scrub.h -- convert single-entry accounts to clean double-entry * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * Scrub.h + * + * FUNCTION: + * Provides a set of functions and utilities for scrubbing clean + * single-entry accounts so that they can be promoted into + * self-consistent, clean double-entry accounts. + * + * HISTORY: + * Created by Linas Vepstas December 1998 + * Copyright (c) 1998, 1999, 2000 Linas Vepstas + */ + +#ifndef XACC_SCRUB_H +#define XACC_SCRUB_H + +#include "Account.h" +#include "Group.h" + +/* The ScrubOrphans() methods search for transacations that contain + * splits that do not have a parent account. These "orphaned splits" + * are placed into an "orphan account" which the user will have to + * go into and clean up. Kind of like the unix "Lost+Found" directory + * for orphaned inodes. + * + * The xaccTransScrubOrphans() method scrubs only the splits in the + * given transaction. A root account group must be provided. + * + * The xaccAccountScrubOrphans() method performs this scrub only for the + * indicated account, and not for any of its children. + * + * The xaccAccountTreeScrubOrphans() method performs this scrub for the + * indicated account and its children. + * + * The xaccGroupScrubOrphans() method performs this scrub for the + * child accounts of this group. + */ +void xaccTransScrubOrphans (Transaction *trans, AccountGroup *root); +void xaccAccountScrubOrphans (Account *acc); +void xaccAccountTreeScrubOrphans (Account *acc); +void xaccGroupScrubOrphans (AccountGroup *grp); + +/* The ScrubSplit methods ensure that splits with the same commodity + * and command currency have the same amount and value. + */ +void xaccSplitScrub (Split *split); +void xaccTransScrubSplits (Transaction *trans); +void xaccAccountScrubSplits (Account *account); +void xaccAccountTreeScrubSplits (Account *account); +void xaccGroupScrubSplits (AccountGroup *group); + +/* The xaccScrubImbalance() method searches for transactions that do + * not balance to zero. If any such transactions are found, a split + * is created to offset this amount and is added to an "imbalance" + * account. + */ +void xaccTransScrubImbalance (Transaction *trans, AccountGroup *root, + Account *parent); +void xaccAccountScrubImbalance (Account *acc); +void xaccAccountTreeScrubImbalance (Account *acc); +void xaccGroupScrubImbalance (AccountGroup *grp); + +/* The xaccTransScrubCurrency method fixes transactions without a + * common_currency by using the old account currency and security + * fields of the parent accounts of the transaction's splits. */ +void xaccTransScrubCurrency (Transaction *trans); + +/* The xaccAccountScrubCommodity method fixed accounts without + * a commodity by using the old account currency and security. */ +void xaccAccountScrubCommodity (Account *account); + +/* The xaccGroupScrubCommodities will scrub the currency/commodity + * of all accounts & transactions in the group. */ +void xaccGroupScrubCommodities (AccountGroup *group); + +#endif /* XACC_SCRUB_H */ diff --git a/src/engine/TransLog.c b/src/engine/TransLog.c new file mode 100644 index 0000000000..b789330707 --- /dev/null +++ b/src/engine/TransLog.c @@ -0,0 +1,363 @@ +/********************************************************************\ + * TransLog.c -- the transaction logger * + * Copyright (C) 1998 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#define _GNU_SOURCE +#include "config.h" + +#include +#include +#include + +#include + +#include "Account.h" +#include "AccountP.h" +#include "DateUtils.h" +#include "date.h" +#include "Transaction.h" +#include "TransactionP.h" +#include "TransLog.h" +#include "gnc-engine-util.h" + +/* + * The logfiles are useful for tracing, journalling, error recovery. + * Note that the current support for journalling is at best + * embryonic, at worst, is dangerous by setting the wrong expectations. + */ + +/* + * Some design philosphy that I think would be good to keep in mind: + * (0) Simplicity and foolproofness are the over-riding design points. + * This is supposed to be a fail-safe safety net. We don't want + * our safety net to fail because of some whiz-bang shenanigans. + * + * (1) Try to keep the code simple. Want to make it simple and obvious + * that we are recording everything that we need to record. + * + * (2) Keep the printed format human readable, for the same reasons. + * (2.a) Keep the format, simple, flat, more or less unstructured, + * record oriented. This will help parsing by perl scripts. + * No, using a perl script to analyze a file that's supposed to + * be human readable is not a contradication in terms -- that's + * exactly the point. + * (2.b) Use tabs as a human freindly field separator; its also a + * character that does not (should not) appear naturally anywhere + * in the data, as it serves no formatting purpose in the current + * GUI design. (hack alert -- this is not currently tested for + * or enforced, so this is a very unsafe assumption. Maybe + * urlencoding should be used.) + * (2.c) Don't print redundant information in a single record. This + * would just confuse any potential user of this file. + * (2.d) Saving space, being compact is not a priority, I don't think. + * + * (3) There are no compatibility requirements from release to release. + * Sounds OK to me to change the format of the output when needed. + * + * (-) print transaction start and end delimiters + * (-) print a unique transaction id as a handy label for anyone + * who actually examines these logs. + * The C address pointer to the transaction struct should be fine, + * as it is simple and unique until the transaction is deleted ... + * and we log deletions, so that's OK. Just note that the id + * for a deleted transaction might be recycled. + * (-) print the current timestamp, so that if it is known that a bug + * occurred at a certain time, it can be located. + * (-) hack alert -- something better than just the account name + * is needed for identifying the account. + */ +/* ------------------------------------------------------------------ */ +/* + * The engine currently uses the log mechanism with flag char set as + * follows: + * + * 'B' for 'begin edit' (followed by the transaction as it looks + * before any changes, i.e. the 'old value') + * 'D' for delete (i.e. delete the previous B; echoes the data in the + * 'old B') + * 'C' for commit (i.e. accept a previous B; data that follows is the + * 'new value') + * 'R' for rollback (i.e. revert to previous B; data that follows should + * be identical to old B) + */ + + +static int gen_logs = 1; +static FILE * trans_log = NULL; +static char * log_base_name = NULL; + +/********************************************************************\ +\********************************************************************/ + +void xaccLogDisable (void) { gen_logs = 0; } +void xaccLogEnable (void) { gen_logs = 1; } + +/********************************************************************\ +\********************************************************************/ + +void +xaccLogSetBaseName (const char *basepath) +{ + if (!basepath) return; + + g_free (log_base_name); + log_base_name = g_strdup (basepath); + + if (trans_log) { + xaccCloseLog(); + xaccOpenLog(); + } +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccOpenLog (void) +{ + char * filename; + char * timestamp; + + if (!gen_logs) return; + if (trans_log) return; + + if (!log_base_name) log_base_name = g_strdup ("translog"); + + /* tag each filename with a timestamp */ + timestamp = xaccDateUtilGetStampNow (); + + filename = g_strconcat (log_base_name, ".", timestamp, ".log", NULL); + + trans_log = fopen (filename, "a"); + if (!trans_log) { + int norr = errno; + printf ("Error: xaccOpenLog(): cannot open journal \n" + "\t %d %s\n", norr, strerror (norr)); + + g_free (filename); + g_free (timestamp); + return; + } + + g_free (filename); + g_free (timestamp); + + /* use tab-separated fields */ + fprintf (trans_log, "mod id time_now " \ + "date_entered date_posted " \ + "account num description " \ + "memo action reconciled " \ + "amount price date_reconciled\n"); + fprintf (trans_log, "-----------------\n"); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccCloseLog (void) +{ + if (!trans_log) return; + fflush (trans_log); + fclose (trans_log); + trans_log = NULL; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccTransWriteLog (Transaction *trans, char flag) +{ + GList *node; + char *dnow, *dent, *dpost, *drecn; + + if (!gen_logs) return; + if (!trans_log) return; + + dnow = xaccDateUtilGetStampNow (); + dent = xaccDateUtilGetStamp (trans->date_entered.tv_sec); + dpost = xaccDateUtilGetStamp (trans->date_posted.tv_sec); + + fprintf (trans_log, "===== START\n"); + + for (node = trans->splits; node; node = node->next) { + Split *split = node->data; + const char * accname = ""; + + if (xaccSplitGetAccount(split)) + accname = xaccAccountGetName (xaccSplitGetAccount(split)); + + drecn = xaccDateUtilGetStamp (split->date_reconciled.tv_sec); + + /* use tab-separated fields */ + fprintf (trans_log, + "%c\t%p/%p\t%s\t%s\t%s\t%s\t%s\t" + "%s\t%s\t%s\t%c\t%lld/%lld\t%lld/%lld\t%s\n", + flag, + trans, split, /* trans+split make up unique id */ + dnow ? dnow : "", + dent ? dent : "", + dpost ? dpost : "", + accname ? accname : "", + trans->num ? trans->num : "", + trans->description ? trans->description : "", + split->memo ? split->memo : "", + split->action ? split->action : "", + split->reconciled, + (long long int) gnc_numeric_num(split->amount), + (long long int) gnc_numeric_denom(split->amount), + (long long int) gnc_numeric_num(split->value), + (long long int) gnc_numeric_denom(split->value), + drecn ? drecn : ""); + + g_free (drecn); + } + + fprintf (trans_log, "===== END\n"); + + g_free (dnow); + g_free (dent); + g_free (dpost); + + /* get data out to the disk */ + fflush (trans_log); +} + +/********************************************************************\ +\********************************************************************/ + +#if 0 +/* open_memstream seems to give various distros fits + * this has resulted in warfare on the mailing list. + * I think the truce called required changing this to asprintf + * this code is not currently used ... so its ifdef out + */ + +char * +xaccSplitAsString(Split *split, const char prefix[]) { + char *result = NULL; + size_t result_size; + FILE *stream = open_memstream(&result, &result_size); + const char *split_memo = xaccSplitGetMemo(split); + const double split_value = DxaccSplitGetValue(split); + Account *split_dest = xaccSplitGetAccount(split); + const char *dest_name = + split_dest ? xaccAccountGetName(split_dest) : NULL; + + assert(stream); + + fputc('\n', stream); + fputs(prefix, stream); + fprintf(stream, " %10.2f | %15s | %s", + split_value, + dest_name ? dest_name : "", + split_memo ? split_memo : ""); + fclose(stream); + return(result); +} + +static char * +xaccTransGetDateStr (Transaction *trans) +{ + char buf [MAX_DATE_LENGTH]; + struct tm *date; + time_t secs; + + secs = xaccTransGetDate (trans); + + date = localtime (&secs); + + printDate(buf, date->tm_mday, date->tm_mon+1, date->tm_year +1900); + + return g_strdup (buf); +} + +char * +xaccTransAsString(Transaction *txn, const char prefix[]) { + char *result = NULL; + size_t result_size; + FILE *stream = open_memstream(&result, &result_size); + time_t date = xaccTransGetDate(txn); + const char *num = xaccTransGetNum(txn); + const char *desc = xaccTransGetDescription(txn); + const char *memo = xaccSplitGetMemo(xaccTransGetSplit(txn, 0)); + const double total = DxaccSplitGetValue(xaccTransGetSplit(txn, 0)); + + assert(stream); + + fputs(prefix, stream); + if(date) { + char *datestr = xaccTransGetDateStr(txn); + fprintf(stream, "%s", datestr); + free(datestr); + } else { + fprintf(stream, ""); + } + fputc(' ', stream); + if(num) { + fputs(num, stream); + } else { + fprintf(stream, ""); + } + + fputc('\n', stream); + fputs(prefix, stream); + if(desc) { + fputs(" ", stream); + fputs(desc, stream); + } else { + fprintf(stream, ""); + } + + fputc('\n', stream); + fputs(prefix, stream); + if(memo) { + fputs(" ", stream); + fputs(memo, stream); + } else { + fprintf(stream, ""); + } + + { + int split_count = xaccTransCountSplits(txn); + int i; + for(i = 1; i < split_count; i++) { + Split *split = xaccTransGetSplit(txn, i); + char *split_text = xaccSplitAsString(split, prefix); + fputs(split_text, stream); + free(split_text); + } + } + fputc('\n', stream); + + fputs(prefix, stream); + fprintf(stream, " %10.2f -- Transaction total\n", total); + fclose(stream); + + return(result); +} + +#endif + +/************************ END OF ************************************\ +\************************* FILE *************************************/ diff --git a/src/engine/TransLog.h b/src/engine/TransLog.h new file mode 100644 index 0000000000..583c001b3a --- /dev/null +++ b/src/engine/TransLog.h @@ -0,0 +1,46 @@ +/********************************************************************\ + * TransLog.h -- the transaction logger * + * Copyright (C) 1998 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef XACC_TRANS_LOG_H +#define XACC_TRANS_LOG_H + +#include "config.h" + +#include "Account.h" +#include "Transaction.h" + +void xaccOpenLog (void); +void xaccCloseLog (void); +void xaccTransWriteLog (Transaction *, char); +void xaccLogEnable (void); +void xaccLogDisable (void); + +/* The xaccLogSetBaseName() method sets the base filepath and the + * root part of the journal file name. If the journal file is + * already open, it will close it and reopen it with the new + * base name. + */ +void xaccLogSetBaseName (const char *); + +#endif /* XACC_TRANS_LOG_H */ + diff --git a/src/engine/Transaction.c b/src/engine/Transaction.c new file mode 100644 index 0000000000..f28937573a --- /dev/null +++ b/src/engine/Transaction.c @@ -0,0 +1,2538 @@ +/********************************************************************\ + * Transaction.c -- transaction & split implementation * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997-2000 Linas Vepstas * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "Account.h" +#include "AccountP.h" +#include "BackendP.h" +#include "GNCIdP.h" +#include "Group.h" +#include "Scrub.h" +#include "Transaction.h" +#include "TransactionP.h" +#include "TransLog.h" +#include "date.h" +#include "gnc-commodity.h" +#include "gnc-engine-util.h" +#include "gnc-engine.h" +#include "gnc-event-p.h" +#include "messages.h" + + +/* + * The "force_double_entry" flag determines how + * the splits in a transaction will be balanced. + * + * The following values have significance: + * 0 -- anything goes + * 1 -- The sum of all splits in a transaction will be + * forced to be zero, even if this requires the + * creation of additional splits. Note that a split + * whose value is zero (e.g. a stock price) can exist + * by itself. Otherwise, all splits must come in at + * least pairs. + * 2 -- splits without parents will be forced into a + * lost & found account. (Not implemented) + */ +int force_double_entry = 0; + +#define PRICE_SIGFIGS 6 + +/* This static indicates the debugging module that this .o belongs to. */ +static short module = MOD_ENGINE; + + +G_INLINE_FUNC void check_open (Transaction *trans); +G_INLINE_FUNC void +check_open (Transaction *trans) +{ + if (trans && 0 >= trans->editlevel) + { + PERR ("transaction %p not open for editing\n", trans); + // assert (trans->editlevel); + PERR ("\t%s:%d \n", __FILE__, __LINE__); + } +} + +/********************************************************************\ + * xaccInitSplit + * Initialize a Split structure +\********************************************************************/ + +static void +xaccInitSplit(Split * split) +{ + /* fill in some sane defaults */ + xaccSplitSetAccount(split, NULL); + split->parent = NULL; + + split->action = g_cache_insert(gnc_engine_get_string_cache(), ""); + split->memo = g_cache_insert(gnc_engine_get_string_cache(), ""); + split->reconciled = NREC; + split->amount = gnc_numeric_zero(); + split->value = gnc_numeric_zero(); + + split->date_reconciled.tv_sec = 0; + split->date_reconciled.tv_nsec = 0; + + split->balance = gnc_numeric_zero(); + split->cleared_balance = gnc_numeric_zero(); + split->reconciled_balance = gnc_numeric_zero(); + + split->kvp_data = kvp_frame_new(); + split->idata = 0; + + xaccGUIDNew(&split->guid); + xaccStoreEntity(split, &split->guid, GNC_ID_SPLIT); +} + +/********************************************************************\ +\********************************************************************/ + +Split * +xaccMallocSplit(void) +{ + Split *split = g_new(Split, 1); + xaccInitSplit (split); + return split; +} + +/********************************************************************\ +\********************************************************************/ +/* This routine is not exposed externally, since it does weird things, + * like not really setting up the parent account correctly, and ditto + * the parent transaction. This routine is prone to programmer error + * if not used correctly. It is used only by the edit-rollback code. + */ + +static Split * +xaccCloneSplit (Split *s) +{ + Split *split = g_new0 (Split, 1); + + /* copy(!) the guid. The cloned split is *not* unique, + * is a sick twisted clone that holds 'undo' information. */ + split->guid = s->guid; + + xaccSplitSetAccountGUID(split, s->acc_guid); + split->parent = s->parent; + + split->memo = g_cache_insert (gnc_engine_get_string_cache(), s->memo); + split->action = g_cache_insert (gnc_engine_get_string_cache(), s->action); + + split->kvp_data = kvp_frame_copy (s->kvp_data); + + split->reconciled = s->reconciled; + split->date_reconciled = s->date_reconciled; + + split->value = s->value; + split->amount = s->amount; + + /* no need to futz with the balances; these get wiped each time ... + * split->balance = s->balance; + * split->cleared_balance = s->cleared_balance; + * split->reconciled_balance = s->reconciled_balance; + */ + + return split; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccFreeSplit (Split *split) +{ + if (!split) return; + + kvp_frame_delete (split->kvp_data); + + g_cache_remove(gnc_engine_get_string_cache(), split->memo); + g_cache_remove(gnc_engine_get_string_cache(), split->action); + + /* just in case someone looks up freed memory ... */ + split->memo = NULL; + split->action = NULL; + split->kvp_data = NULL; + split->reconciled = NREC; + split->amount = gnc_numeric_zero(); + split->value = gnc_numeric_zero(); + split->parent = NULL; + xaccSplitSetAccount(split, NULL); + + split->date_reconciled.tv_sec = 0; + split->date_reconciled.tv_nsec = 0; + + g_free(split); +} + +/******************************************************************** + * xaccSplitEqual + ********************************************************************/ +gboolean +xaccSplitEqual(const Split *sa, const Split *sb, + gboolean check_guids, + gboolean check_txn_splits) { + + if(!sa && !sb) return TRUE; + if(!sa) return FALSE; + if(!sb) return FALSE; + + if(check_guids) { + if(!guid_equal(&(sa->guid), &(sb->guid))) return FALSE; + } + + /* Since these strings are cached we can just use pointer equality */ + if(sa->memo != sb->memo) return FALSE; + if(sa->action != sb->action) return FALSE; + + if(kvp_frame_compare(sa->kvp_data, sb->kvp_data) != 0) return FALSE; + + if(sa->reconciled != sb->reconciled) return FALSE; + if(timespec_cmp(&(sa->date_reconciled), + &(sb->date_reconciled))) return FALSE; + + if(!gnc_numeric_eq(sa->amount, sb->amount)) return FALSE; + if(!gnc_numeric_eq(sa->value, sb->value)) return FALSE; + + if(!xaccTransEqual(sa->parent, sb->parent, + check_guids, + check_txn_splits)) { + return FALSE; + } + + return(TRUE); +} + +/******************************************************************** + * xaccSplitGetSlots + ********************************************************************/ + +kvp_frame * +xaccSplitGetSlots(Split * s) { + if(!s) return NULL; + return(s->kvp_data); +} + +void +xaccSplitSetSlots_nc(Split *s, kvp_frame *frm) +{ + g_return_if_fail(s); + g_return_if_fail(frm); + + if(s->kvp_data) + { + kvp_frame_delete(s->kvp_data); + } + + s->kvp_data = frm; +} + +/******************************************************************** + * Account funcs + ********************************************************************/ + +static void +xaccSplitSetAccount_Internal(Split *s, Account *act) +{ + if(!act) + { + return; + } + s->acc = act; +} + +Account* +xaccSplitGetAccount(Split *s) +{ + if(!s) return NULL; + + if(!s->acc) + { + xaccSplitSetAccount_Internal(s, xaccAccountLookup(&s->acc_guid)); + } + + return s->acc; +} + +const GUID * +xaccSplitGetAccountGUID(Split *split) +{ + return (const GUID*) &split->acc_guid; +} + +void +xaccSplitSetAccount(Split *s, Account *act) +{ + if(!act) + { + s->acc_guid = *xaccGUIDNULL(); + } + else + { + const GUID *id = xaccAccountGetGUID(act); + s->acc_guid = *id; + } + + s->acc = act; +} + +void +xaccSplitSetAccountGUID(Split *s, GUID id) +{ + s->acc_guid = id; + s->acc = NULL; +} + + +/********************************************************************\ +\********************************************************************/ + +const GUID * +xaccSplitGetGUID (Split *split) +{ + if (!split) return xaccGUIDNULL(); + return &split->guid; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccSplitSetGUID (Split *split, const GUID *guid) +{ + if (!split || !guid) return; + check_open (split->parent); + xaccRemoveEntity(&split->guid); + split->guid = *guid; + xaccStoreEntity(split, &split->guid, GNC_ID_SPLIT); +} + +/********************************************************************\ +\********************************************************************/ + +Split * +xaccSplitLookup (const GUID *guid) +{ + if (!guid) return NULL; + return xaccLookupEntity(guid, GNC_ID_SPLIT); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccConfigSetForceDoubleEntry (int force) +{ + force_double_entry = force; +} + +int +xaccConfigGetForceDoubleEntry (void) +{ + return (force_double_entry); +} + +/********************************************************************\ +\********************************************************************/ + +G_INLINE_FUNC void mark_split_internal (Split *split, + gboolean generate_events); +G_INLINE_FUNC void +mark_split_internal (Split *split, gboolean generate_events) +{ + Account *account = xaccSplitGetAccount(split); + Transaction *trans; + + if (account) + { + account->balance_dirty = TRUE; + account->sort_dirty = TRUE; + + xaccGroupMarkNotSaved (account->parent); + + if (generate_events) + gnc_engine_generate_event (&account->guid, GNC_EVENT_MODIFY); + } + + trans = split->parent; + if (trans && generate_events) + gnc_engine_generate_event (&trans->guid, GNC_EVENT_MODIFY); +} + +G_INLINE_FUNC void mark_split (Split *split); +G_INLINE_FUNC void +mark_split (Split *split) +{ + mark_split_internal (split, TRUE); +} + +G_INLINE_FUNC void mark_trans (Transaction *trans); +G_INLINE_FUNC void +mark_trans (Transaction *trans) +{ + GList *node; + + for (node = trans->splits; node; node = node->next) + mark_split_internal (node->data, FALSE); + + gnc_engine_generate_event (&trans->guid, GNC_EVENT_MODIFY); +} + +/********************************************************************\ +\********************************************************************/ + +static int +get_currency_denom(Split * s) +{ + if(!s) + { + return 0; + } + else if(!s->parent || !s->parent->common_currency) + { + return 100000; + } + else + { + return gnc_commodity_get_fraction (s->parent->common_currency); + } +} + +static int +get_commodity_denom(Split * s) +{ + if(!s) + { + return 0; + } + else if(!xaccSplitGetAccount(s)) + { + return 100000; + } + else + { + return xaccAccountGetCommoditySCU(xaccSplitGetAccount(s)); + } +} + +/********************************************************************\ +\********************************************************************/ + +void +DxaccSplitSetSharePriceAndAmount (Split *s, double price, double amt) +{ + if (!s) return; + check_open (s->parent); + + s->amount = double_to_gnc_numeric(amt, get_commodity_denom(s), + GNC_RND_ROUND); + s->value = double_to_gnc_numeric(price * amt, get_currency_denom(s), + GNC_RND_ROUND); + + mark_split (s); +} + +void +xaccSplitSetSharePriceAndAmount (Split *s, gnc_numeric price, + gnc_numeric amt) +{ + if (!s) return; + check_open (s->parent); + + s->amount = gnc_numeric_convert(amt, get_commodity_denom(s), GNC_RND_ROUND); + s->value = gnc_numeric_mul(s->amount, price, + get_currency_denom(s), GNC_RND_ROUND); + + mark_split (s); +} + +void +DxaccSplitSetSharePrice (Split *s, double amt) +{ + xaccSplitSetSharePrice + (s, double_to_gnc_numeric(amt, GNC_DENOM_AUTO, + GNC_DENOM_SIGFIGS(PRICE_SIGFIGS) | + GNC_RND_ROUND)); +} + +void +xaccSplitSetSharePrice (Split *s, gnc_numeric price) +{ + if (!s) return; + check_open (s->parent); + + s->value = gnc_numeric_mul(s->amount, price, get_currency_denom(s), + GNC_RND_ROUND); + + mark_split (s); +} + +void +DxaccSplitSetShareAmount (Split *s, double damt) +{ + gnc_numeric old_price; + gnc_numeric amt = double_to_gnc_numeric(damt, get_commodity_denom(s), + GNC_RND_ROUND); + if (!s) return; + check_open (s->parent); + + if(!gnc_numeric_zero_p(s->amount)) { + old_price = gnc_numeric_div(s->value, s->amount, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + } + else { + old_price = gnc_numeric_create(1, 1); + } + + s->amount = gnc_numeric_convert(amt, get_commodity_denom(s), + GNC_RND_NEVER); + s->value = gnc_numeric_mul(s->amount, old_price, + get_currency_denom(s), GNC_RND_ROUND); + + mark_split (s); +} + +void +xaccSplitSetAmount (Split *s, gnc_numeric amt) +{ + if(!s) return; + check_open (s->parent); + + s->amount = gnc_numeric_convert(amt, get_commodity_denom(s), GNC_RND_ROUND); + + mark_split (s); +} + +void +DxaccSplitSetValue (Split *s, double damt) +{ + gnc_numeric amt = double_to_gnc_numeric(damt, + get_currency_denom(s), + GNC_RND_ROUND); + gnc_numeric old_price; + if (!s) return; + check_open (s->parent); + + if(!gnc_numeric_zero_p(s->amount)) { + old_price = gnc_numeric_div(s->value, s->amount, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + } + else { + old_price = gnc_numeric_create(1, 1); + } + + s->value = gnc_numeric_convert(amt, get_currency_denom(s), + GNC_RND_NEVER); + + if(!gnc_numeric_zero_p(old_price)) { + s->amount = gnc_numeric_div(s->value, old_price, get_currency_denom(s), + GNC_RND_ROUND); + } + + mark_split (s); +} + +void +xaccSplitSetValue (Split *s, gnc_numeric amt) +{ + if(!s) return; + check_open (s->parent); + + s->value = gnc_numeric_convert(amt, get_currency_denom(s), GNC_RND_ROUND); + + mark_split (s); +} + +/********************************************************************\ +\********************************************************************/ + +gnc_numeric +xaccSplitGetBalance (Split *s) { + if (!s) return gnc_numeric_zero(); + return s->balance; +} + +gnc_numeric +xaccSplitGetClearedBalance (Split *s) { + if (!s) return gnc_numeric_zero(); + return s->cleared_balance; +} + +gnc_numeric +xaccSplitGetReconciledBalance (Split *s) { + if (!s) return gnc_numeric_zero(); + return s->reconciled_balance; +} + +/********************************************************************\ + * xaccInitTransaction + * Initialize a transaction structure +\********************************************************************/ + +static void +xaccInitTransaction (Transaction * trans) +{ + /* Fill in some sane defaults */ + trans->num = g_cache_insert(gnc_engine_get_string_cache(), ""); + trans->description = g_cache_insert(gnc_engine_get_string_cache(), ""); + + trans->common_currency = NULL; + trans->splits = NULL; + + trans->date_entered.tv_sec = 0; + trans->date_entered.tv_nsec = 0; + + trans->date_posted.tv_sec = 0; + trans->date_posted.tv_nsec = 0; + + trans->version = 0; + trans->version_check = 0; + trans->marker = 0; + trans->editlevel = 0; + trans->do_free = FALSE; + trans->orig = NULL; + + trans->kvp_data = kvp_frame_new(); + trans->idata = 0; + + xaccGUIDNew(&trans->guid); + xaccStoreEntity(trans, &trans->guid, GNC_ID_TRANS); +} + +/********************************************************************\ +\********************************************************************/ + +Transaction * +xaccMallocTransaction (void) +{ + Transaction *trans = g_new(Transaction, 1); + + xaccInitTransaction (trans); + + gnc_engine_generate_event (&trans->guid, GNC_EVENT_CREATE); + + return trans; +} + +/********************************************************************\ +\********************************************************************/ +/* This routine is not exposed externally, since it does weird things, + * like not really owning the splits correctly, and other weirdnesses. + * This routine is prone to programmer snafu if not used correctly. + * It is used only by the edit-rollback code. + */ + +static Transaction * +xaccCloneTransaction (Transaction *t) +{ + Transaction *trans; + GList *node; + + trans = g_new0 (Transaction, 1); + + trans->num = g_cache_insert (gnc_engine_get_string_cache(), t->num); + trans->description = g_cache_insert (gnc_engine_get_string_cache(), t->description); + + trans->kvp_data = kvp_frame_copy (t->kvp_data); + + trans->splits = g_list_copy (t->splits); + for (node = trans->splits; node; node = node->next) + node->data = xaccCloneSplit (node->data); + + trans->date_entered.tv_sec = t->date_entered.tv_sec; + trans->date_entered.tv_nsec = t->date_entered.tv_nsec; + + trans->date_posted.tv_sec = t->date_posted.tv_sec; + trans->date_posted.tv_nsec = t->date_posted.tv_nsec; + + trans->version = t->version; + trans->editlevel = 0; + trans->do_free = FALSE; + trans->orig = NULL; + + /* copy(!) the guid. The cloned transaction is *not* unique, + * is a sick twisted clone that holds 'undo' information. */ + trans->guid = t->guid; + + return trans; +} + + +/********************************************************************\ +\********************************************************************/ + +static void +xaccFreeTransaction (Transaction *trans) +{ + GList *node; + + if (!trans) return; + + ENTER ("addr=%p\n", trans); + + /* free up the destination splits */ + for (node = trans->splits; node; node = node->next) + xaccFreeSplit (node->data); + g_list_free (trans->splits); + trans->splits = NULL; + + /* free up transaction strings */ + g_cache_remove(gnc_engine_get_string_cache(), trans->num); + g_cache_remove(gnc_engine_get_string_cache(), trans->description); + + kvp_frame_delete (trans->kvp_data); + + /* just in case someone looks up freed memory ... */ + trans->num = NULL; + trans->description = NULL; + trans->kvp_data = NULL; + + trans->date_entered.tv_sec = 0; + trans->date_entered.tv_nsec = 0; + + trans->date_posted.tv_sec = 0; + trans->date_posted.tv_nsec = 0; + + trans->version = 0; + trans->editlevel = 0; + trans->do_free = FALSE; + + if (trans->orig) + { + xaccFreeTransaction (trans->orig); + trans->orig = NULL; + } + + g_free(trans); + + LEAVE ("addr=%p\n", trans); +} + +/******************************************************************** + xaccTransEqual + + Compare two transactions for equality. We don't pay any attention to + rollback issues here, and we only care about equality of "permanent + fields", basically the things that would survive a file save/load + cycle. + + ********************************************************************/ + +gboolean +xaccTransEqual(const Transaction *ta, const Transaction *tb, + gboolean check_guids, + gboolean check_splits) { + + if(!ta && !tb) return TRUE; + if(!ta) return FALSE; + if(!tb) return FALSE; + + if(check_guids) { + if(!guid_equal(&(ta->guid), &(tb->guid))) return FALSE; + } + + if(!gnc_commodity_equiv(ta->common_currency, tb->common_currency)) + return FALSE; + + if(timespec_cmp(&(ta->date_entered), &(tb->date_entered))) return FALSE; + if(timespec_cmp(&(ta->date_posted), &(tb->date_posted))) return FALSE; + /* Since we use cached strings, we can just compare pointer + * equality for num and description + */ + if(ta->num != tb->num) return FALSE; + if(ta->description != tb->description) return FALSE; + + if(kvp_frame_compare(ta->kvp_data, tb->kvp_data) != 0) return FALSE; + + if(check_splits) { + GList *sa = ta->splits; + GList *sb = tb->splits; + + if(!sa && sb) return FALSE; + if(!sb && sa) return FALSE; + + if(sa && sb) { + /* presume that the splits are in the same order */ + while(sa && sb) { + if(!xaccSplitEqual(sa->data, sb->data, check_guids, FALSE)) + return(FALSE); + sa = sa->next; + sb = sb->next; + } + if(sa != NULL) return(FALSE); + if(sb != NULL) return(FALSE); + } + } + + return(TRUE); +} + +/******************************************************************** + * xaccTransGetSlots + ********************************************************************/ + +kvp_frame * +xaccTransGetSlots(Transaction *t) { + if(!t) return NULL; + return(t->kvp_data); +} + +void +xaccTransSetSlots_nc(Transaction *t, kvp_frame *frm) +{ + g_return_if_fail(t); + g_return_if_fail(frm); + + if(t->kvp_data) + { + kvp_frame_delete(t->kvp_data); + } + + t->kvp_data = frm; +} + +/********************************************************************\ +\********************************************************************/ + +const GUID * +xaccTransGetGUID (Transaction *trans) +{ + if (!trans) return xaccGUIDNULL(); + return &trans->guid; +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccTransSetGUID (Transaction *trans, const GUID *guid) +{ + if (!trans || !guid) return; + xaccRemoveEntity(&trans->guid); + trans->guid = *guid; + xaccStoreEntity(trans, &trans->guid, GNC_ID_TRANS); +} + + +/********************************************************************\ +\********************************************************************/ + +Transaction * +xaccTransLookup (const GUID *guid) +{ + if (!guid) return NULL; + return xaccLookupEntity(guid, GNC_ID_TRANS); +} + +/********************************************************************\ +\********************************************************************/ + +void +DxaccSplitSetBaseValue (Split *s, double value, + const gnc_commodity * base_currency) +{ + xaccSplitSetBaseValue(s, + double_to_gnc_numeric(value, get_currency_denom(s), + GNC_RND_ROUND), + base_currency); +} + +void +xaccSplitSetBaseValue (Split *s, gnc_numeric value, + const gnc_commodity * base_currency) +{ + const gnc_commodity *currency; + const gnc_commodity *commodity; + + if (!s) return; + check_open (s->parent); + + /* Novice/casual users may not want or use the double entry + * features of this engine. So, in particular, there may be the + * occasional split without a parent account. Well, that's ok, + * we'll just go with the flow. */ + if (!xaccSplitGetAccount(s)) { + if (force_double_entry) { + PERR ("split must have a parent\n"); + assert (xaccSplitGetAccount(s)); + } + else { + /* this is a change in semantics. previously, calling + * setbasevalue on the same split twice would set the + * amount the first time and the value the second. + * that's bogus. -- bg */ + s->value = value; + s->amount = value; + } + mark_split (s); + return; + } + + currency = xaccTransGetCurrency (xaccSplitGetParent(s)); + commodity = xaccAccountGetCommodity (xaccSplitGetAccount(s)); + + /* if the base_currency is the account currency, set the + * value. If it's the account commodity, set the damount. + * If both, set both. */ + if (gnc_commodity_equiv(currency, base_currency)) { + if(gnc_commodity_equiv(commodity, base_currency)) { + s->amount = gnc_numeric_convert(value, + get_commodity_denom(s), + GNC_RND_NEVER); + } + s->value = gnc_numeric_convert(value, + get_currency_denom(s), + GNC_RND_NEVER); + } + else if (gnc_commodity_equiv(commodity, base_currency)) { + s->amount = gnc_numeric_convert(value, get_commodity_denom(s), + GNC_RND_NEVER); + } + else if ((NULL==base_currency) && (0 == force_double_entry)) { + s->value = gnc_numeric_convert(value, get_currency_denom(s), + GNC_RND_NEVER); + } + else { + PERR ("inappropriate base currency %s " + "given split currency=%s and commodity=%s\n", + gnc_commodity_get_printname(base_currency), + gnc_commodity_get_printname(currency), + gnc_commodity_get_printname(commodity)); + return; + } + + mark_split (s); +} + +gnc_numeric +xaccSplitGetBaseValue (Split *s, const gnc_commodity * base_currency) +{ + const gnc_commodity *currency; + const gnc_commodity *commodity; + gnc_numeric value; + + if (!s) return gnc_numeric_zero(); + + /* ahh -- users may not want or use the double entry + * features of this engine. So, in particular, there + * may be the occasional split without a parent account. + * Well, that's ok, we'll just go with the flow. + */ + if (!xaccSplitGetAccount(s)) { + if (force_double_entry) { + assert (xaccSplitGetAccount(s)); + } + else { + return s->value; + } + } + + currency = xaccTransGetCurrency (xaccSplitGetParent(s)); + commodity = xaccAccountGetCommodity (xaccSplitGetAccount(s)); + + /* be more precise -- the value depends on the currency we want it + * expressed in. */ + if (gnc_commodity_equiv(currency, base_currency)) { + value = s->value; + } + else if (gnc_commodity_equiv(commodity, base_currency)) { + value = s->amount; + } + else if ((NULL == base_currency) && (0 == force_double_entry)) { + value = s->value; + } + else { + PERR ("inappropriate base currency %s " + "given split currency=%s and commodity=%s\n", + gnc_commodity_get_printname(base_currency), + gnc_commodity_get_printname(currency), + gnc_commodity_get_printname(commodity)); + return gnc_numeric_zero(); + } + + return value; +} + +/********************************************************************\ +\********************************************************************/ + +gnc_numeric +xaccSplitsComputeValue (GList *splits, Split * skip_me, + const gnc_commodity * base_currency) +{ + GList *node; + gnc_numeric value; + + value = gnc_numeric_zero(); + + for (node = splits; node; node = node->next) + { + Split *s = node->data; + + if (s == skip_me) + continue; + + /* ahh -- users may not want or use the double entry features of + * this engine. So, in particular, there may be the occasional + * split without a parent account. Well, that's ok, we'll just + * go with the flow. */ + if (!xaccSplitGetAccount(s)) { + if (force_double_entry) { + assert (xaccSplitGetAccount(s)); + } + else { + value = gnc_numeric_add(value, s->value, + GNC_DENOM_AUTO, GNC_DENOM_LCD); + } + } + else if ((NULL == base_currency) && (0 == force_double_entry)) { + value = gnc_numeric_add(value, s->value, + GNC_DENOM_AUTO, GNC_DENOM_LCD); + } + else { + const gnc_commodity *currency; + const gnc_commodity *commodity; + + currency = xaccTransGetCurrency (xaccSplitGetParent(s)); + commodity = xaccAccountGetCommodity (xaccSplitGetAccount(s)); + + /* OK, we've got a parent account, we've got currency, lets + * behave like professionals now, instead of the shenanigans + * above. Note that just because the currencies are equivalent + * doesn't mean the denominators are the same! */ + if (base_currency && + gnc_commodity_equiv(currency, base_currency)) { + value = gnc_numeric_add(value, s->value, + GNC_DENOM_AUTO, GNC_DENOM_LCD); + } + else if (base_currency && + gnc_commodity_equiv(commodity, base_currency)) { + value = gnc_numeric_add(value, s->amount, + GNC_DENOM_AUTO, GNC_DENOM_LCD); + } + else { + PERR ("inconsistent currencies\n" + "\tbase = '%s', curr='%s', sec='%s'\n", + gnc_commodity_get_printname(base_currency), + gnc_commodity_get_printname(currency), + gnc_commodity_get_printname(commodity)); + assert (0); + } + } + } + + if (base_currency) + return gnc_numeric_convert (value, + gnc_commodity_get_fraction (base_currency), + GNC_RND_ROUND); + else + return gnc_numeric_convert (value, GNC_DENOM_AUTO, GNC_DENOM_REDUCE); +} + +gnc_numeric +xaccTransGetImbalance (Transaction * trans) +{ + const gnc_commodity * currency; + + if (!trans) + return gnc_numeric_zero (); + + currency = xaccTransGetCurrency (trans); + return xaccSplitsComputeValue (trans->splits, NULL, currency); +} + +/********************************************************************\ +\********************************************************************/ + +static gnc_commodity * +FindCommonExclSCurrency (GList *splits, + gnc_commodity * ra, gnc_commodity * rb, + Split *excl_split) +{ + GList *node; + + if (!splits) return NULL; + + for (node = splits; node; node = node->next) + { + Split *s = node->data; + gnc_commodity * sa, * sb; + + if (s == excl_split) + continue; + + /* Novice/casual users may not want or use the double entry + * features of this engine. Because of this, there + * may be the occasional split without a parent account. + * Well, that's ok, we'll just go with the flow. + */ + if (force_double_entry) + assert (xaccSplitGetAccount(s)); + else if (xaccSplitGetAccount(s) == NULL) + continue; + + sa = DxaccAccountGetCurrency (xaccSplitGetAccount(s)); + sb = DxaccAccountGetSecurity (xaccSplitGetAccount(s)); + + if (ra && rb) { + int aa = !gnc_commodity_equiv(ra,sa); + int ab = !gnc_commodity_equiv(ra,sb); + int ba = !gnc_commodity_equiv(rb,sa); + int bb = !gnc_commodity_equiv(rb,sb); + + if ( (!aa) && bb) rb = NULL; + else + if ( (!ab) && ba) rb = NULL; + else + if ( (!ba) && ab) ra = NULL; + else + if ( (!bb) && aa) ra = NULL; + else + if ( aa && bb && ab && ba ) { ra = NULL; rb = NULL; } + + if (!ra) { ra = rb; rb = NULL; } + } + else + if (ra && !rb) { + int aa = !gnc_commodity_equiv(ra,sa); + int ab = !gnc_commodity_equiv(ra,sb); + if ( aa && ab ) ra = NULL; + } + + if ((!ra) && (!rb)) return NULL; + } + + return (ra); +} + +/* This is the wrapper for those calls (i.e. the older ones) which + * don't exclude one split from the splitlist when looking for a + * common currency. + */ +static gnc_commodity * +FindCommonCurrency (GList *splits, gnc_commodity * ra, gnc_commodity * rb) +{ + return FindCommonExclSCurrency(splits, ra, rb, NULL); +} + +gnc_commodity * +xaccTransFindOldCommonCurrency (Transaction *trans) +{ + gnc_commodity *ra, *rb, *retval; + Split *split; + + if (!trans) return NULL; + + if (trans->splits == NULL) return NULL; + + split = trans->splits->data; + + if (xaccSplitGetAccount(split) == NULL) return NULL; + + ra = DxaccAccountGetCurrency (xaccSplitGetAccount(split)); + rb = DxaccAccountGetSecurity (xaccSplitGetAccount(split)); + + retval = FindCommonCurrency (trans->splits, ra, rb); + + /* compare this value to what we think should be the 'right' value */ + if (!trans->common_currency) + { + trans->common_currency = retval; + } + else if (!gnc_commodity_equiv (retval,trans->common_currency)) + { + PWARN ("expected common currency %s but found %s\n", + gnc_commodity_get_unique_name (trans->common_currency), + gnc_commodity_get_unique_name (retval)); + } + + if (NULL == retval) + { + /* in every situation I can think of, this routine should return + * common currency. So make note of this ... */ + PWARN ("unable to find a common currency, and that is strange."); + } + + return retval; +} + +/********************************************************************\ +\********************************************************************/ +/* The new routine for setting the common currency */ + +gnc_commodity * +xaccTransGetCurrency (Transaction *trans) +{ + if (!trans) return NULL; + return trans->common_currency; +} + +void +xaccTransSetCurrency (Transaction *trans, gnc_commodity *curr) +{ + GList *splits; + gint fraction; + + if (!trans || !curr) return; + check_open (trans); + + trans->common_currency = curr; + fraction = gnc_commodity_get_fraction (curr); + + for (splits = trans->splits; splits; splits = splits->next) + { + Split *s = splits->data; + s->value = gnc_numeric_convert(s->value, fraction, GNC_RND_ROUND); + } + + mark_trans (trans); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccTransBeginEdit (Transaction *trans) +{ + Backend *be; + if (!trans) return; + + trans->editlevel ++; + if (1 < trans->editlevel) return; + + if (0 >= trans->editlevel) + { + PERR ("unbalanced call - resetting (was %d)", trans->editlevel); + trans->editlevel = 1; + } + + /* See if there's a backend. If there is, invoke it. */ + be = xaccTransactionGetBackend (trans); + if (be && be->trans_begin_edit) + (be->trans_begin_edit) (be, trans); + + xaccOpenLog (); + xaccTransWriteLog (trans, 'B'); + + /* make a clone of the transaction; we will use this + * in case we need to roll-back the edit. + */ + trans->orig = xaccCloneTransaction (trans); +} + +void +xaccTransCommitEdit (Transaction *trans) +{ + Split *split; + Backend *be; + const char *str; + + if (!trans) return; + ENTER ("trans addr=%p", trans); + + trans->editlevel--; + if (0 < trans->editlevel) return; + + if (0 > trans->editlevel) + { + PERR ("unbalanced call - resetting (was %d)", trans->editlevel); + trans->editlevel = 0; + } + + /* At this point, we check to see if we have a valid transaction. + * There are two possiblities: + * 1) Its more or less OK, and needs a little cleanup + * 2) It has zero splits, i.e. is meant to be destroyed. + * We handle 1) immediately, and we call the backend before + * we go through with 2). + */ + if (trans->splits && !(trans->do_free)) + { + split = trans->splits->data; + + /* Try to get the sorting order lined up according to + * when the user typed things in. */ + if (0 == trans->date_entered.tv_sec) { + struct timeval tv; + gettimeofday (&tv, NULL); + trans->date_entered.tv_sec = tv.tv_sec; + trans->date_entered.tv_nsec = 1000 * tv.tv_usec; + } + + /* Alternately the transaction may have only one split in + * it, in which case that's OK if and only if the split has no + * value (i.e. is only recording a price). Otherwise, a single + * split with a value can't possibly balance, thus violating the + * rules of double-entry, and that's way bogus. So create + * a matching opposite and place it either here (if force==1), + * or in some dummy account (if force==2). + */ + if ((1 == force_double_entry) && + (NULL == g_list_nth(trans->splits, 1)) && + (!gnc_numeric_zero_p(split->amount))) { + Split * s = xaccMallocSplit(); + xaccTransAppendSplit (trans, s); + xaccAccountInsertSplit (xaccSplitGetAccount(s), s); + xaccSplitSetMemo (s, split->memo); + xaccSplitSetAction (s, split->action); + xaccSplitSetAmount(s, gnc_numeric_neg(split->amount)); + xaccSplitSetValue(s, gnc_numeric_neg(split->value)); + } + } + + /* ------------------------------------------------- */ + /* OK, at this point, we are done making sure that + * we've got a validly constructed transaction. + * Next, we send it off to the back-end, to see if the + * back-end will accept it. + */ + + /* See if there's a backend. If there is, invoke it. */ + str = xaccTransGetDescription(trans); + str = str ? str : "(null)"; + PINFO ("descr is %s", str); + + be = xaccTransactionGetBackend (trans); + if (be && be->trans_commit_edit) + { + GNCBackendError errcode; + + /* clear errors */ + do { + errcode = xaccBackendGetError (be); + } while (ERR_BACKEND_NO_ERR != errcode); + + (be->trans_commit_edit) (be, trans, trans->orig); + + errcode = xaccBackendGetError (be); + if (ERR_BACKEND_NO_ERR != errcode) + { + /* if the backend puked, then we must roll-back + * at this point, and let the user know that we failed. + */ + /* XXX hack alert -- turn this into a gui dialog */ + if (ERR_BACKEND_MODIFIED == errcode) + { + PWARN("Another user has modified this transaction\n" + "\tjust a moment ago. Please look at thier changes,\n" + "\t and try again, if needed.\n" + "\t(This dialog should be a gui dialog and \n" + "\tshould check for errors)\n"); + } + + /* push error back onto the stack */ + xaccBackendSetError (be, errcode); + + trans->editlevel++; + xaccTransRollbackEdit (trans); + return; + } + } + + /* ------------------------------------------------- */ + if (!trans->splits || trans->do_free) + { + PINFO ("delete trans at addr=%p", trans); + /* Make a log in the journal before destruction. */ + xaccTransWriteLog (trans, 'D'); + xaccRemoveEntity(&trans->guid); + xaccFreeTransaction (trans); + return; + } + + /* ------------------------------------------------- */ + /* Make sure all associated splits are in proper order + * in their accounts with the correct balances. */ + xaccTransFixSplitDateOrder (trans); + + trans->do_free = FALSE; + xaccTransWriteLog (trans, 'C'); + + /* Get rid of the copy we made. We won't be rolling back, + * so we don't need it any more. */ + xaccFreeTransaction (trans->orig); + trans->orig = NULL; + + LEAVE ("trans addr=%p\n", trans); +} + +void +xaccTransRollbackEdit (Transaction *trans) +{ + Backend *be; + Transaction *orig; + int force_it=0, mismatch=0; + int i; + ENTER ("trans addr=%p\n", trans); + + if (!trans) return; + trans->editlevel--; + if (0 < trans->editlevel) return; + + if (0 > trans->editlevel) + { + PERR ("unbalanced call - resetting (was %d)", trans->editlevel); + trans->editlevel = 0; + } + + /* copy the original values back in. */ + orig = trans->orig; + + /* If the transaction had been deleted before the rollback, + * the guid would have been unlisted. Restore that */ + xaccStoreEntity(trans, &trans->guid, GNC_ID_TRANS); + + g_cache_remove (gnc_engine_get_string_cache(), trans->num); + trans->num = orig->num; + orig->num = g_cache_insert(gnc_engine_get_string_cache(), ""); + + g_cache_remove (gnc_engine_get_string_cache(), trans->description); + trans->description = orig->description; + orig->description = g_cache_insert(gnc_engine_get_string_cache(), ""); + + kvp_frame_delete (trans->kvp_data); + trans->kvp_data = orig->kvp_data; + if (!trans->kvp_data) + trans->kvp_data = kvp_frame_new (); + orig->kvp_data = kvp_frame_new (); + + trans->date_entered.tv_sec = orig->date_entered.tv_sec; + trans->date_entered.tv_nsec = orig->date_entered.tv_nsec; + + trans->date_posted.tv_sec = orig->date_posted.tv_sec; + trans->date_posted.tv_nsec = orig->date_posted.tv_nsec; + + /* OK, we also have to restore the state of the splits. Of course, + * we could brute-force our way through this, and just clobber all of the + * old splits, and insert all of the new splits, but this kind of brute + * forcing will suck memory cycles. So instead we'll try the gentle + * approach first. Note that even in the gentle approach, the + * CheckDateOrder routine could be cpu-cyle brutal, so it maybe + * it could use some tuning. + */ + if (trans->do_free) + { + force_it = 1; + mismatch = 0; + } + else + { + GList *node; + GList *node_orig; + Split *s, *so; + + s = so = NULL; + + for (i = 0, node = trans->splits, node_orig = orig->splits ; + node && node_orig ; + i++, node = node->next, node_orig = node_orig->next) + { + s = node->data; + so = node_orig->data; + + if (xaccSplitGetAccount(so) != xaccSplitGetAccount(s)) + { + force_it = 1; + mismatch = i; + break; + } + + g_cache_remove (gnc_engine_get_string_cache(), s->action); + s->action = so->action; + so->action = g_cache_insert(gnc_engine_get_string_cache(), ""); + + g_cache_remove (gnc_engine_get_string_cache(), s->memo); + s->memo = so->memo; + so->memo = g_cache_insert(gnc_engine_get_string_cache(), ""); + + kvp_frame_delete (s->kvp_data); + s->kvp_data = so->kvp_data; + if (!s->kvp_data) + s->kvp_data = kvp_frame_new (); + so->kvp_data = kvp_frame_new (); + + s->reconciled = so->reconciled; + s->amount = so->amount; + s->value = so->value; + + s->date_reconciled.tv_sec = so->date_reconciled.tv_sec; + s->date_reconciled.tv_nsec = so->date_reconciled.tv_nsec; + + /* do NOT check date order until all of the other fields + * have been properly restored */ + xaccAccountFixSplitDateOrder (xaccSplitGetAccount(s), s); + xaccAccountRecomputeBalance (xaccSplitGetAccount(s)); + mark_split (s); + } + + if (so != s) + { + force_it = 1; + mismatch = i; + } + } + + /* OK, if force_it got set, we'll have to tough it out and brute-force + * the rest of the way. Clobber all the edited splits, add all new splits. + * Unfortunately, this can suck up CPU cycles in the Remove/Insert routines. + */ + if (force_it) + { + GList *node; + + for (i = 0, node = trans->splits ; + node && i < mismatch ; + i++, node = node->next) + { + Split *s = node->data; + GList *node_orig; + + node_orig = g_list_nth (orig->splits, i); + xaccFreeSplit (node_orig->data); + node_orig->data = s; + } + + for (node = g_list_nth (trans->splits, mismatch) ; + node ; node = node->next) + { + Split *s = node->data; + + mark_split (s); + xaccAccountRemoveSplit (xaccSplitGetAccount(s), s); + xaccAccountRecomputeBalance (xaccSplitGetAccount(s)); + xaccRemoveEntity(&s->guid); + xaccFreeSplit (s); + } + + g_list_free (trans->splits); + + trans->splits = orig->splits; + orig->splits = NULL; + + for (node = g_list_nth (trans->splits, mismatch) ; + node ; node = node->next) + { + Split *s = node->data; + Account *account = xaccSplitGetAccount(s); + + xaccSplitSetAccount(s, NULL); + xaccStoreEntity(s, &s->guid, GNC_ID_SPLIT); + xaccAccountInsertSplit (account, s); + xaccAccountRecomputeBalance (account); + mark_split (s); + } + } + + be = xaccTransactionGetBackend (trans); + if (be && be->trans_rollback_edit) + { + GNCBackendError errcode; + + /* clear errors */ + do { + errcode = xaccBackendGetError (be); + } while (ERR_BACKEND_NO_ERR != errcode); + + (be->trans_rollback_edit) (be, trans); + + errcode = xaccBackendGetError (be); + if (ERR_BACKEND_MOD_DESTROY == errcode) + { + /* The backend is asking us to delete this transaction. + * This typically happens because another (remote) user + * has deleted this transaction, and we haven't found + * out about it until this user tried to edit it. + */ + trans->editlevel++; + xaccTransDestroy (trans); + xaccFreeTransaction (trans); + + /* push error back onto the stack */ + xaccBackendSetError (be, errcode); + LEAVE ("deleted trans addr=%p\n", trans); + return; + } + if (ERR_BACKEND_NO_ERR != errcode) + { + PERR ("Rollback Failed. Ouch!"); + /* push error back onto the stack */ + xaccBackendSetError (be, errcode); + } + } + + xaccTransWriteLog (trans, 'R'); + + xaccFreeTransaction (trans->orig); + + trans->orig = NULL; + trans->do_free = FALSE; + + LEAVE ("trans addr=%p\n", trans); +} + +gboolean +xaccTransIsOpen (Transaction *trans) +{ + if (!trans) return FALSE; + return (0 < trans->editlevel); +} + +void +xaccTransSetVersion (Transaction *trans, gint32 vers) +{ + if (!trans) return; + trans->version = vers; +} + +gint32 +xaccTransGetVersion (Transaction *trans) +{ + if (!trans) return 0; + return (trans->version); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccTransDestroy (Transaction *trans) +{ + GList *node; + + if (!trans) return; + check_open (trans); + trans->do_free = TRUE; + xaccTransWriteLog (trans, 'D'); + + gnc_engine_generate_event (&trans->guid, GNC_EVENT_DESTROY); + + for (node = trans->splits; node; node = node->next) + { + Split *split = node->data; + + mark_split (split); + + xaccAccountRemoveSplit (xaccSplitGetAccount(split), split); + xaccAccountRecomputeBalance (xaccSplitGetAccount(split)); + xaccRemoveEntity(&split->guid); + xaccFreeSplit (split); + + node->data = NULL; + } + + g_list_free (trans->splits); + trans->splits = NULL; + + xaccRemoveEntity(&trans->guid); + + /* the actual free is done with the commit call, else its rolled back */ + /* xaccFreeTransaction (trans); don't do this here ... */ +} + +/********************************************************************\ + * TransRemoveSplit is an engine private function and does not/should + * not cause any rebalancing to occur. +\********************************************************************/ + +static void +xaccTransRemoveSplit (Transaction *trans, Split *split) +{ + if (trans == NULL) + return; + + trans->splits = g_list_remove (trans->splits, split); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccSplitDestroy (Split *split) +{ + Transaction *trans; + + if (!split) return; + + trans = split->parent; + check_open (trans); + + mark_split (split); + xaccRemoveEntity (&split->guid); + + if (trans) + { + gboolean ismember = (g_list_find (trans->splits, split) != NULL); + assert (ismember); + xaccTransRemoveSplit (trans, split); + } + + xaccAccountRemoveSplit (xaccSplitGetAccount(split), split); + xaccAccountRecomputeBalance (xaccSplitGetAccount(split)); + + xaccFreeSplit (split); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccTransAppendSplit (Transaction *trans, Split *split) +{ + Transaction *oldtrans; + + if (!trans || !split) return; + check_open (trans); + + /* first, make sure that the split isn't already inserted + * elsewhere. If so, then remove it. */ + oldtrans = split->parent; + if (oldtrans) + xaccTransRemoveSplit (oldtrans, split); + + /* now, insert the split into the array */ + split->parent = trans; + trans->splits = g_list_append (trans->splits, split); + + /* convert the split to the new transaction's commodity denominator */ + /* if the denominator can't be exactly converted, it's an error */ + if (trans->common_currency) + { + int fraction = gnc_commodity_get_fraction (trans->common_currency); + gnc_numeric new_value; + + new_value = gnc_numeric_convert(split->value, fraction, GNC_RND_ROUND); + if (gnc_numeric_check (new_value) == GNC_ERROR_OK) + split->value = new_value; + } +} + +/********************************************************************\ + * sorting comparison function + * + * returns a negative value if transaction a is dated earlier than b, + * returns a positive value if transaction a is dated later than b, + * + * This function tries very hard to uniquely order all transactions. + * If two transactions occur on the same date, then their "num" fields + * are compared. If the num fields are identical, then the description + * fields are compared. If these are identical, then the memo fields + * are compared. Hopefully, there will not be any transactions that + * occur on the same day that have all three of these values identical. + * + * Note that being able to establish this kind of absolute order is + * important for some of the ledger display functions. + * + * Yes, this kind of code dependency is ugly, but the alternatives seem + * ugly too. + * +\********************************************************************/ + + +#define DATE_CMP(aaa,bbb,field) { \ + /* if dates differ, return */ \ + if ( (aaa->field.tv_sec) < \ + (bbb->field.tv_sec)) { \ + return -1; \ + } else \ + if ( (aaa->field.tv_sec) > \ + (bbb->field.tv_sec)) { \ + return +1; \ + } \ + \ + /* else, seconds match. check nanoseconds */ \ + if ( (aaa->field.tv_nsec) < \ + (bbb->field.tv_nsec)) { \ + return -1; \ + } else \ + if ( (aaa->field.tv_nsec) > \ + (bbb->field.tv_nsec)) { \ + return +1; \ + } \ +} + + + +int +xaccSplitDateOrder (Split *sa, Split *sb) +{ + int retval; + int comp; + char *da, *db; + + if(sa == sb) return 0; + /* nothing is always less than something */ + if(!sa && sb) return -1; + if(sa && !sb) return +1; + + retval = xaccTransOrder (sa->parent, sb->parent); + if (0 != retval) return retval; + + /* otherwise, sort on memo strings */ + da = sa->memo; + db = sb->memo; + SAFE_STRCMP (da, db); + + /* otherwise, sort on action strings */ + da = sa->action; + db = sb->action; + SAFE_STRCMP (da, db); + + /* the reconciled flag ... */ + if ((sa->reconciled) < (sb->reconciled)) return -1; + if ((sa->reconciled) > (sb->reconciled)) return +1; + + /* compare amounts */ + comp = gnc_numeric_compare(sa->amount, sb->amount); + if(comp < 0) return -1; + if(comp > 0) return +1; + + comp = gnc_numeric_compare(sa->value, sb->value); + if(comp < 0) return -1; + if(comp > 0) return +1; + + /* if dates differ, return */ + DATE_CMP(sa,sb,date_reconciled); + +#if 0 + /* sort on txn guid. */ + if(sa->parent && !sb->parent) return -1; + if(!sa->parent && sb->parent) return 1; + if(sa->parent && sb->parent) { + retval = guid_compare(&(sa->guid), &(sb->guid)); + if(retval != 0) return retval; + } +#endif + + /* else, sort on guid - keeps sort stable. */ + retval = guid_compare(&(sa->guid), &(sb->guid)); + if(retval != 0) return retval; + + return 0; +} + +int +xaccTransOrder (Transaction *ta, Transaction *tb) +{ + char *da, *db; + int retval; + + if ( ta && !tb ) return -1; + if ( !ta && tb ) return +1; + if ( !ta && !tb ) return 0; + + /* if dates differ, return */ + DATE_CMP(ta,tb,date_posted); + + /* otherwise, sort on number string */ + da = ta->num; + db = tb->num; + SAFE_STRCMP (da, db); + + /* if dates differ, return */ + DATE_CMP(ta,tb,date_entered); + + /* otherwise, sort on description string */ + da = ta->description; + db = tb->description; + SAFE_STRCMP (da, db); + + /* else, sort on guid - keeps sort stable. */ + retval = guid_compare(&(ta->guid), &(tb->guid)); + if(retval != 0) return retval; + + return 0; +} +static gboolean +get_corr_account_split(Split *sa, Split **retval) +{ + + Split *current_split; + GList *split_list; + Transaction * ta; + gnc_numeric sa_value, current_value; + gboolean sa_value_positive, current_value_positive, seen_different = FALSE; + + *retval = NULL; + g_return_val_if_fail(sa, TRUE); + ta = xaccSplitGetParent(sa); + + sa_value = xaccSplitGetValue(sa); + sa_value_positive = gnc_numeric_positive_p(sa_value); + + for(split_list = xaccTransGetSplitList(ta);split_list; split_list = split_list->next) + { + current_split = split_list->data; + if(current_split != sa) + { + current_value = xaccSplitGetValue(current_split); + current_value_positive = gnc_numeric_positive_p(current_value); + if((sa_value_positive && !current_value_positive) || + (!sa_value_positive && current_value_positive)) + { + if(seen_different) + { + *retval = NULL; + return TRUE; + } + else + { + seen_different = TRUE; + *retval = current_split; + } + } + } + } + return FALSE; +} + +const char * +xaccSplitGetCorrAccountName(Split *sa) +{ + static const char *split_const = NULL; + Split *other_split; + Account *other_split_acc; + + if(get_corr_account_split(sa, &other_split)) + { + if (!split_const) + split_const = _("-- Split Transaction --"); + + return split_const; + } + else + { + other_split_acc = xaccSplitGetAccount(other_split); + return xaccAccountGetName(other_split_acc); + } +} + +char * +xaccSplitGetCorrAccountFullName(Split *sa, char separator) +{ + static const char *split_const = NULL; + Split *other_split; + Account *other_split_acc; + + if(get_corr_account_split(sa, &other_split)) + { + if (!split_const) + split_const = _("-- Split Transaction --"); + + return g_strdup(split_const); + } + else + { + other_split_acc = xaccSplitGetAccount(other_split); + return xaccAccountGetFullName(other_split_acc, separator); + } +} + +const char * +xaccSplitGetCorrAccountCode(Split *sa) +{ + static const char *split_const = NULL; + Split *other_split; + Account *other_split_acc; + + if(get_corr_account_split(sa, &other_split)) + { + if (!split_const) + split_const = _("Split"); + + return split_const; + } + else + { + other_split_acc = xaccSplitGetAccount(other_split); + return xaccAccountGetName(other_split_acc); + } +} + +int +xaccSplitCompareAccountFullNames(Split *sa, Split *sb) +{ + Account *aa, *ab; + char *full_a, *full_b; + int retval; + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + aa = xaccSplitGetAccount(sa); + ab = xaccSplitGetAccount(sb); + full_a = xaccAccountGetFullName(aa, ':'); + full_b = xaccAccountGetFullName(ab, ':'); + /* for comparison purposes it doesn't matter what we use as a separator */ + retval = safe_strcmp(full_a, full_b); + g_free(full_a); + g_free(full_b); + return retval; + +} + + +int +xaccSplitCompareAccountCodes(Split *sa, Split *sb) +{ + Account *aa, *ab; + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + aa = xaccSplitGetAccount(sa); + ab = xaccSplitGetAccount(sb); + + return safe_strcmp(xaccAccountGetName(aa), xaccAccountGetName(ab)); +} + +int +xaccSplitCompareOtherAccountFullNames(Split *sa, Split *sb) +{ + char *ca, *cb; + int retval; + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + /* doesn't matter what separator we use + * as long as they are the same + */ + + ca = xaccSplitGetCorrAccountFullName(sa, ':'); + cb = xaccSplitGetCorrAccountFullName(sb, ':'); + retval = safe_strcmp(ca, cb); + g_free(ca); + g_free(cb); + return retval; +} + +int +xaccSplitCompareOtherAccountCodes(Split *sa, Split *sb) +{ + const char *ca, *cb; + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + ca = xaccSplitGetCorrAccountCode(sa); + cb = xaccSplitGetCorrAccountCode(sb); + return safe_strcmp(ca, cb); +} +/********************************************************************\ +\********************************************************************/ + +enum { TDATE_POSTED, TDATE_ENTERED }; + +static void +xaccTransSetDateInternal(Transaction *trans, int which, time_t secs, + long int nsecs) +{ + Timespec *dadate = 0; + if(!trans) return; + check_open(trans); + + PINFO ("addr=%p set %d date to %lu %li %s \n", + trans, which, secs, nsecs, ctime (&secs)); + + dadate = ((which == TDATE_POSTED) + ? &trans->date_posted + : &trans->date_entered); + dadate->tv_sec = secs; + dadate->tv_nsec = nsecs; + + mark_trans(trans); + /* Because the date has changed, we need to make sure that each of + * the splits is properly ordered in each of their accounts. We + * could do that here, simply by reinserting each split into its + * account. However, in some ways this is bad behaviour, and it + * seems much better/nicer to defer that until the commit phase, + * i.e. until the user has called the xaccTransCommitEdit() + * routine. So, for now, we are done. */ +} + +void +xaccTransSetDateSecs (Transaction *trans, time_t secs) +{ + xaccTransSetDateInternal(trans, TDATE_POSTED, secs, 0); +} + +void +xaccTransSetDateEnteredSecs (Transaction *trans, time_t secs) +{ + xaccTransSetDateInternal(trans, TDATE_ENTERED, secs, 0); +} + +void +xaccTransSetDatePostedTS (Transaction *trans, const Timespec *ts) +{ + if (!ts) return; + xaccTransSetDateInternal(trans, TDATE_POSTED, ts->tv_sec, ts->tv_nsec); +} + +void +xaccTransSetDateEnteredTS (Transaction *trans, const Timespec *ts) +{ + if (!ts) return; + xaccTransSetDateInternal(trans, TDATE_ENTERED, ts->tv_sec, ts->tv_nsec); +} + +void +xaccTransSetDate (Transaction *trans, int day, int mon, int year) +{ + Timespec ts = gnc_dmy2timespec(day, mon, year); + xaccTransSetDateInternal(trans, TDATE_POSTED, ts.tv_sec, ts.tv_nsec); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccTransSetNum (Transaction *trans, const char *xnum) +{ + char * tmp; + if (!trans || !xnum) return; + check_open (trans); + + tmp = g_cache_insert(gnc_engine_get_string_cache(), (gpointer) xnum); + g_cache_remove(gnc_engine_get_string_cache(), trans->num); + trans->num = tmp; + mark_trans (trans); +} + +void +xaccTransSetDescription (Transaction *trans, const char *desc) +{ + char * tmp; + if (!trans || !desc) return; + check_open (trans); + + tmp = g_cache_insert(gnc_engine_get_string_cache(), (gpointer) desc); + g_cache_remove(gnc_engine_get_string_cache(), trans->description); + trans->description = tmp; + mark_trans (trans); +} + +void +xaccTransSetNotes (Transaction *trans, const char *notes) +{ + if (!trans || !notes) return; + check_open (trans); + + kvp_frame_set_slot_nc (trans->kvp_data, "notes", + kvp_value_new_string (notes)); + mark_trans (trans); +} + +/********************************************************************\ +\********************************************************************/ + +Split * +xaccTransGetSplit (Transaction *trans, int i) +{ + if (!trans) return NULL; + if (i < 0) return NULL; + + return g_list_nth_data (trans->splits, i); +} + +GList * +xaccTransGetSplitList (Transaction *trans) +{ + if (!trans) return NULL; + + return trans->splits; +} + +const char * +xaccTransGetNum (Transaction *trans) +{ + if (!trans) return NULL; + return (trans->num); +} + +const char * +xaccTransGetDescription (Transaction *trans) +{ + if (!trans) return NULL; + return (trans->description); +} + +const char * +xaccTransGetNotes (Transaction *trans) +{ + kvp_value *v; + + if (!trans) return NULL; + + v = kvp_frame_get_slot (xaccTransGetSlots (trans), "notes"); + if (!v) + return NULL; + + return kvp_value_get_string (v); +} + +time_t +xaccTransGetDate (Transaction *trans) +{ + if (!trans) return 0; + return (trans->date_posted.tv_sec); +} + +void +xaccTransGetDatePostedTS (Transaction *trans, Timespec *ts) +{ + if (!trans || !ts) return; + *ts = (trans->date_posted); +} + +void +xaccTransGetDateEnteredTS (Transaction *trans, Timespec *ts) +{ + if (!trans || !ts) return; + *ts = (trans->date_entered); +} + +Timespec +xaccTransRetDatePostedTS (Transaction *trans) +{ + Timespec ts; + ts.tv_sec = 0; ts.tv_nsec = 0; + if (!trans) return ts; + return (trans->date_posted); +} + +Timespec +xaccTransRetDateEnteredTS (Transaction *trans) +{ + Timespec ts; + ts.tv_sec = 0; ts.tv_nsec = 0; + if (!trans) return ts; + return (trans->date_entered); +} + +int +xaccTransCountSplits (Transaction *trans) +{ + if (!trans) return 0; + return g_list_length (trans->splits); +} + +/********************************************************************\ +\********************************************************************/ + +void +xaccSplitSetMemo (Split *split, const char *memo) +{ + char * tmp; + if (!split || !memo) return; + check_open (split->parent); + + tmp = g_cache_insert(gnc_engine_get_string_cache(), (gpointer) memo); + g_cache_remove(gnc_engine_get_string_cache(), split->memo); + split->memo = tmp; + mark_split (split); +} + +void +xaccSplitSetAction (Split *split, const char *actn) +{ + char * tmp; + if (!split || !actn) return; + check_open (split->parent); + + tmp = g_cache_insert(gnc_engine_get_string_cache(), (gpointer) actn); + g_cache_remove(gnc_engine_get_string_cache(), split->action); + split->action = tmp; + mark_split (split); +} + +void +xaccSplitSetReconcile (Split *split, char recn) +{ + if (!split) return; + check_open (split->parent); + + switch (recn) + { + case NREC: + case CREC: + case YREC: + case FREC: + break; + default: + PERR("Bad reconciled flag"); + return; + } + + split->reconciled = recn; + + xaccAccountRecomputeBalance (xaccSplitGetAccount(split)); + mark_split (split); +} + +void +xaccSplitSetDateReconciledSecs (Split *split, time_t secs) +{ + if (!split) return; + check_open (split->parent); + + split->date_reconciled.tv_sec = secs; + split->date_reconciled.tv_nsec = 0; + mark_split (split); +} + +void +xaccSplitSetDateReconciledTS (Split *split, Timespec *ts) +{ + if (!split || !ts) return; + check_open (split->parent); + + split->date_reconciled = *ts; + mark_split (split); +} + +void +xaccSplitGetDateReconciledTS (Split * split, Timespec *ts) +{ + if (!split || !ts) return; + *ts = (split->date_reconciled); +} + +Timespec +xaccSplitRetDateReconciledTS (Split * split) +{ + Timespec ts; ts.tv_sec=0; ts.tv_nsec=0; + if (!split) return ts; + return (split->date_reconciled); +} + +/********************************************************************\ +\********************************************************************/ + +/* return the parent transaction of the split */ +Transaction * +xaccSplitGetParent (Split *split) +{ + if (!split) return NULL; + return (split->parent); +} + +const char * +xaccSplitGetMemo (Split *split) +{ + if (!split) return NULL; + return (split->memo); +} + +const char * +xaccSplitGetAction (Split *split) +{ + if (!split) return NULL; + return (split->action); +} + +char +xaccSplitGetReconcile (Split *split) { + if (!split) return ' '; + return (split->reconciled); +} + +double +DxaccSplitGetShareAmount (Split * split) { + return gnc_numeric_to_double(xaccSplitGetAmount(split)); +} + +double +DxaccSplitGetValue (Split * split) { + return gnc_numeric_to_double(xaccSplitGetValue(split)); +} + +double +DxaccSplitGetSharePrice (Split * split) +{ + return gnc_numeric_to_double(xaccSplitGetSharePrice(split)); +} + +gnc_numeric +xaccSplitGetAmount (Split * split) +{ + if (!split) return gnc_numeric_zero(); + return split->amount; +} + +gnc_numeric +xaccSplitGetValue (Split * split) { + if (!split) return gnc_numeric_zero(); + return split->value; +} + +gnc_numeric +xaccSplitGetSharePrice (Split * split) { + if(!split || gnc_numeric_zero_p(split->amount)) { + return gnc_numeric_create(1, 1); + } + return gnc_numeric_div(split->value, + split->amount, + GNC_DENOM_AUTO, + GNC_DENOM_SIGFIGS(PRICE_SIGFIGS) | + GNC_RND_ROUND); +} + +/********************************************************************\ +\********************************************************************/ + +const char * +xaccSplitGetType(const Split *s) +{ + kvp_frame *frame; + kvp_value *split_type; + + if(!s) return NULL; + frame = xaccSplitGetSlots((Split *) s); + if(!frame) return NULL; + split_type = kvp_frame_get_slot(frame, "split-type"); + if(!split_type) return "normal"; + if(kvp_value_get_type(split_type) != KVP_TYPE_STRING) return NULL; + return(kvp_value_get_string(split_type)); +} + +/* reconfigure a split to be a stock split - after this, you shouldn't + mess with the value, just the amount. */ +void +xaccSplitMakeStockSplit(Split *s) +{ + check_open (s->parent); + + xaccSplitSetValue(s, gnc_numeric_zero()); + kvp_frame_set_slot_nc(s->kvp_data, + "split-type", + kvp_value_new_string("stock-split")); + mark_split(s); +} + + +/********************************************************************\ +\********************************************************************/ + +Account * +xaccGetAccountByName (Transaction *trans, const char * name) +{ + Account *acc = NULL; + GList *node; + + if (!trans) return NULL; + if (!name) return NULL; + + /* walk through the splits, looking for one, any one, that has a + * parent account */ + for (node = trans->splits; node; node = node->next) + { + Split *s = node->data; + + acc = xaccSplitGetAccount(s); + if (acc) break; + } + + if (!acc) return NULL; + + return xaccGetPeerAccountFromName (acc, name); +} + +/********************************************************************\ +\********************************************************************/ + +Account * +xaccGetAccountByFullName (Transaction *trans, const char * name, + const char separator) +{ + Account *acc = NULL; + GList *node; + + if (!trans) return NULL; + if (!name) return NULL; + + /* walk through the splits, looking for one, any one, that has a + * parent account */ + for (node = trans->splits; node; node = node->next) + { + Split *s = node->data; + + acc = xaccSplitGetAccount(s); + if (acc) break; + } + + if (!acc) return NULL; + + return xaccGetPeerAccountFromFullName (acc, name, separator); +} + +/********************************************************************\ +\********************************************************************/ + +Split * +xaccSplitGetOtherSplit (Split *split) +{ + Split *s1, *s2; + Transaction *trans; + + if (!split) return NULL; + trans = split->parent; + + if (g_list_length (trans->splits) != 2) + return NULL; + + s1 = g_list_nth_data (trans->splits, 0); + s2 = g_list_nth_data (trans->splits, 1); + + if (s1 == split) + return s2; + + return s1; +} + +/********************************************************************\ +\********************************************************************/ + +int +xaccIsPeerSplit (Split *sa, Split *sb) +{ + Transaction *ta, *tb; + if (!sa || !sb) return 0; + ta = sa->parent; + tb = sb->parent; + if (ta == tb) return 1; + return 0; +} + +/************************ END OF ************************************\ +\************************* FILE *************************************/ diff --git a/src/engine/Transaction.h b/src/engine/Transaction.h new file mode 100644 index 0000000000..af49772de8 --- /dev/null +++ b/src/engine/Transaction.h @@ -0,0 +1,476 @@ +/********************************************************************\ + * Transaction.h -- api for transactions & splits (journal entries) * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997, 1998, 1999, 2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef XACC_TRANSACTION_H +#define XACC_TRANSACTION_H + +#include "config.h" + +#include + +#include "gnc-common.h" +#include "gnc-commodity.h" +#include "gnc-numeric.h" +#include "kvp_frame.h" +#include "GNCId.h" +#include "date.h" + + +/* Values for the reconciled field in Splits */ +#define CREC 'c' /* The Split has been cleared */ +#define YREC 'y' /* The Split has been reconciled */ +#define FREC 'f' /* frozen into accounting period */ +#define NREC 'n' /* not reconciled or cleared */ + +/** STRUCTS *********************************************************/ + +typedef struct _account Account; +typedef struct _account_group AccountGroup; +typedef struct _split Split; +typedef struct _transaction Transaction; + + +/** PROTOTYPES ******************************************************/ + +/* + * The xaccConfigSetForceDoubleEntry() and xaccConfigGetForceDoubleEntry() + * set and get the "force_double_entry" flag. This flag determines how + * the splits in a transaction will be balanced. + * + * The following values have significance: + * 0 -- anything goes + * 1 -- The sum of all splits in a transaction will be + * forced to be zero, even if this requires the + * creation of additional splits. Note that a split + * whose value is zero (e.g. a stock price) can exist + * by itself. Otherwise, all splits must come in at + * least pairs. + * 2 -- splits without parents will be forced into a + * lost & found account. (Not implemented) + */ + +void xaccConfigSetForceDoubleEntry (int force); +int xaccConfigGetForceDoubleEntry (void); + +/* + * The xaccMallocTransaction() will malloc memory and initialize it. + * Once created, it is usually unsafe to merely "free" this memory; + * the xaccTransDestroy() method should be called. + */ +Transaction * xaccMallocTransaction (void); + +gboolean xaccTransEqual(const Transaction *ta, const Transaction *tb, + gboolean check_guids, + gboolean check_splits); + + +/* The xaccTransDestroy() method will remove all + * of the splits from each of their accounts, free the memory + * associated with them. This routine must be followed by either + * an xaccTransCommitEdit(), in which case the transaction + * memory will be freed, or by xaccTransRollbackEdit(), in which + * case nothing at all is freed, and everything is put back into + * original order. + */ +void xaccTransDestroy (Transaction *trans); + +/* The xaccTransBeginEdit() method must be called before any changes + * are made to a transaction or any of its component splits. If + * this is not done, errors will result. + * + * The xaccTransCommitEdit() method indicates that the changes to the + * transaction and its splits are complete and should be made + * permanent. Note this routine may result in the deletion of the + * transaction, if the transaction is "empty" (has no splits), or + * of xaccTransDestroy() was called on the transaction. + * + * The xaccTransRollbackEdit() routine rejects all edits made, and + * sets the transaction back to where it was before the editing + * started. This includes restoring any deleted splits, removing + * any added splits, and undoing the effects of xaccTransDestroy, + * as well as restoring share quantities, memos, descriptions, etc. + * + * The xaccTransIsOpen() method returns TRUE if the transaction + * is open for editing. Otherwise, it returns false. */ +void xaccTransBeginEdit (Transaction *trans); +void xaccTransCommitEdit (Transaction *trans); +void xaccTransRollbackEdit (Transaction *trans); + +gboolean xaccTransIsOpen (Transaction *trans); + +/* + * The xaccTransGetGUID() subroutine will return the + * globally unique id associated with that transaction. + * + * The xaccTransLookup() subroutine will return the + * transaction associated with the given id, or NULL + * if there is no such transaction. + */ +const GUID * xaccTransGetGUID (Transaction *trans); +Transaction * xaccTransLookup (const GUID *guid); + + +/* Transaction slots are used to store arbitrary strings, numbers, and + * structures which aren't members of the transaction struct. */ + +kvp_frame *xaccTransGetSlots(Transaction *trans); +void xaccTransSetSlots_nc(Transaction *t, kvp_frame *frm); + +/* The xaccTransSetDateSecs() method will modify the posted date + * of the transaction. (Footnote: this shouldn't matter to a user, + * but anyone modifying the engine should understand that when + * xaccTransCommitEdit() is called, the date order of each of the + * component splits will be checked, and will be restored in + * ascending date order.) + * + * The xaccTransSetDate() method does the same thing as + * xaccTransSetDateSecs(), but takes a convenient day-month-year format. + * + * The xaccTransSetDatePostedTS() method does the same thing as + * xaccTransSetDateSecs(), but takes a struct timespec64. + * + */ +void xaccTransSetDate (Transaction *trans, + int day, int mon, int year); +void xaccTransSetDateSecs (Transaction *trans, time_t time); +void xaccTransSetDatePostedTS (Transaction *trans, + const Timespec *ts); + +void xaccTransSetDateEnteredSecs (Transaction *trans, time_t time); +void xaccTransSetDateEnteredTS (Transaction *trans, + const Timespec *ts); + +/* set the Num, Description, and Notes fields */ +void xaccTransSetNum (Transaction *trans, const char *num); +void xaccTransSetDescription (Transaction *trans, const char *desc); +void xaccTransSetNotes (Transaction *trans, const char *notes); + +/* The xaccTransAppendSplit() method will append the indicated + * split to the collection of splits in this transaction. + * If the split is already a part of another transaction, + * it will be removed from that transaction first. + */ +void xaccTransAppendSplit (Transaction *trans, Split *split); + +/* The xaccSplitDestroy() method will update its parent account and + * transaction in a consistent manner, resulting in the complete + * unlinking of the split, and the freeing of its associated memory. + * The goal of this routine is to perform the removal and destruction + * of the split in an atomic fashion, with no chance of accidentally + * leaving the accounting structure out-of-balance or otherwise + * inconsistent. + * + * If the deletion of the split leaves the transaction with no + * splits, then the transaction will be marked for deletion. (It + * will not be deleted until the xaccTransCommitEdit() routine is + * called.) + */ +void xaccSplitDestroy (Split *split); + +/* ------------- gets --------------- */ +/* The xaccTransGetSplit() method returns a pointer to each of the + * splits in this transaction. Valid values for i are zero to + * (number_of__splits-1). An invalid value of i will cause NULL to + * be returned. A convenient way of cycling through all splits is + * to start at zero, and keep incrementing until a null value is returned. + */ +Split * xaccTransGetSplit (Transaction *trans, int i); + +/* The xaccTransGetSplitList() method returns a GList of the splits + * in a transaction. This list must not be modified. Do *NOT* free + * this list when you are done with it. */ +GList * xaccTransGetSplitList (Transaction *trans); + +/* These routines return the Num (or ID field), the description, + * the notes, and the date field. + */ +const char * xaccTransGetNum (Transaction *trans); +const char * xaccTransGetDescription (Transaction *trans); +const char * xaccTransGetNotes (Transaction *trans); +time_t xaccTransGetDate (Transaction *trans); + +void xaccTransGetDatePostedTS (Transaction *trans, Timespec *ts); + +void xaccTransGetDateEnteredTS (Transaction *trans, Timespec *ts); + +Timespec xaccTransRetDateEnteredTS (Transaction *trans); +Timespec xaccTransRetDatePostedTS (Transaction *trans); + +/* The xaccTransCountSplits() method returns the number of splits + * in a transaction. + */ +int xaccTransCountSplits (Transaction *trans); + +/* --------------------------------------------------------------- */ +/* Commmodity routines. Each transaction's 'currency' is by definition + * the balancing common currency for the splits in that transaction. + * */ +gnc_commodity * xaccTransGetCurrency (Transaction *trans); +void xaccTransSetCurrency (Transaction *trans, gnc_commodity *curr); + +/* The xaccTransGetImbalance() method returns the total value of the + * transaction. In a pure double-entry system, this imbalance + * should be exactly zero, and if it is not, something is broken. + * However, when double-entry semantics are not enforced, unbalanced + * transactions can sneak in, and this routine can be used to find + * out how much things are off by. The value returned is denominated + * in the currency that is returned by the xaccTransFindCommonCurrency() + * method. + */ +gnc_numeric xaccTransGetImbalance (Transaction * trans); + +/* ------------- splits --------------- */ +Split * xaccMallocSplit (void); + +gboolean xaccSplitEqual(const Split *sa, const Split *sb, + gboolean check_guids, + gboolean check_txn_splits); + +/* Split slots are used to store arbitrary strings, numbers, and + * structures which aren't members of the transaction struct. + * + * See kvp_doc.txt for reserved slot names. + */ +kvp_frame *xaccSplitGetSlots(Split *split); +void xaccSplitSetSlots_nc(Split *s, kvp_frame *frm); + +/* The xaccSplitGetGUID() subroutine will return the + * globally unique id associated with that split. + * + * The xaccSplitLookup() subroutine will return the + * split associated with the given id, or NULL + * if there is no such split. + */ +const GUID * xaccSplitGetGUID (Split *split); +Split * xaccSplitLookup (const GUID *guid); + +/* The memo is an arbitrary string associated with a split. + * Users typically type in free form text from the GUI. + */ +void xaccSplitSetMemo (Split *split, const char *memo); + +/* The Action is essentially an arbitrary string, but is + * meant to be conveniently limited to a menu of selections + * such as "Buy", "Sell", "Interest", etc. However, + * as far as the engine is concerned, its an arbitrary string. + */ +void xaccSplitSetAction (Split *split, const char *action); + +/* The Reconcile is a single byte, whose values are typically + * are "N", "C" and "R" + */ +void xaccSplitSetReconcile (Split *split, char reconciled_flag); +void xaccSplitSetDateReconciledSecs (Split *split, time_t time); +void xaccSplitSetDateReconciledTS (Split *split, Timespec *ts); +void xaccSplitGetDateReconciledTS (Split *split, Timespec *ts); +Timespec xaccSplitRetDateReconciledTS (Split *split); + +/* + * The following four functions set the prices and amounts. + * All of the routines always maintain balance: that is, + * invoking any of them will cause other splits in the transaction + * to be modified so that the net value of the transaction is zero. + * + * IMPORTANT: The split should be parented by an account before + * any of these routines are invoked! This is because the actual + * setting of amounts/values requires SCU settings from the account. + * If these are not available, then amounts/values will be set to + * -1/0, which is an invalid value. I beleive this order dependency + * is a bug, but I'm too lazy to find, fix & test at the moment ... + * + * The xaccSplitSetAmount() (formerly xaccSplitSetShareAmount) method + * sets the amount in the account's commodity that the split + * should have. + * + * The xaccSplitSetSharePrice() method sets the price of the + * split. DEPRECATED - set the value and amount instead. + * + * The xaccSplitSetValue() method adjusts the number of shares in + * the split so that the number of shares times the share price + * equals the value passed in. + * + * The xaccSplitSetSharePriceAndAmount() method will simultaneously + * update the share price and the number of shares. This + * is a utility routine that is equivalent to a xaccSplitSetSharePrice() + * followed by and xaccSplitSetAmount(), except that it incurs the + * processing overhead of balancing only once, instead of twice. */ + +void DxaccSplitSetSharePriceAndAmount (Split *split, double price, + double amount); +void DxaccSplitSetShareAmount (Split *split, double amount); +void DxaccSplitSetSharePrice (Split *split, double price); +void DxaccSplitSetValue (Split *split, double value); +void DxaccSplitSetBaseValue (Split *split, double value, + const gnc_commodity * base_currency); + +void xaccSplitSetSharePriceAndAmount (Split *split, gnc_numeric price, + gnc_numeric amount); +void xaccSplitSetAmount (Split *split, gnc_numeric amount); +void xaccSplitSetSharePrice (Split *split, gnc_numeric price); +void xaccSplitSetValue (Split *split, gnc_numeric value); +void xaccSplitSetBaseValue (Split *split, gnc_numeric value, + const gnc_commodity * base_currency); + +/* The following four subroutines return the running balance up + * to & including the indicated split. + * + * The balance is the currency-denominated balance. For accounts + * with non-unit share prices, it is correctly adjusted for + * share prices. + * + * The share-balance is the number of shares. + * Price fluctuations do not change the share balance. + * + * The cleared-balance is the currency-denominated balance + * of all transactions that have been marked as cleared or reconciled. + * It is correctly adjusted for price fluctuations. + * + * The reconciled-balance is the currency-denominated balance + * of all transactions that have been marked as reconciled. + */ + +gnc_numeric xaccSplitGetBalance (Split *split); +gnc_numeric xaccSplitGetClearedBalance (Split *split); +gnc_numeric xaccSplitGetReconciledBalance (Split *split); +gnc_numeric xaccSplitGetBaseValue (Split *split, + const gnc_commodity * base_currency); + +/* return the parent transaction of the split */ +Transaction * xaccSplitGetParent (Split *split); + +/* return the memo, action strings */ +const char * xaccSplitGetMemo (Split *split); +const char * xaccSplitGetAction (Split *split); + +/* return the value of the reconcile flag */ +char xaccSplitGetReconcile (Split *split); +double DxaccSplitGetShareAmount (Split * split); +double DxaccSplitGetSharePrice (Split * split); +double DxaccSplitGetValue (Split * split); + +gnc_numeric xaccSplitGetAmount (Split * split); +gnc_numeric xaccSplitGetSharePrice (Split * split); +gnc_numeric xaccSplitGetValue (Split * split); + +Account * xaccSplitGetAccount (Split *split); +const GUID * xaccSplitGetAccountGUID(Split *split); +void xaccSplitSetAccount(Split *s, Account *act); +void xaccSplitSetAccountGUID(Split *s, GUID id); + +/* split types: normal stock-split */ +const char *xaccSplitGetType(const Split *s); + +/* reconfgure a split to be a stock split - after this, you shouldn't + mess with the value, just the damount. */ +void xaccSplitMakeStockSplit(Split *s); + +/********************************************************************\ + * sorting comparison function + * + * The xaccTransOrder(ta,tb) method is useful for sorting. + * return a negative value if transaction ta is dated earlier than tb, + * return a positive value if transaction ta is dated later than tb, + * then compares num and description values, using the strcmp() + * c-library routine, returning what strcmp would return. + * Finally, it returns zero if all of the above match. + * Note that it does *NOT* compare its member splits. + * + * The xaccSplitDateOrder(sa,sb) method is useful for sorting. + * if sa and sb have different transactions, return their xaccTransOrder + * return a negative value if split sa has a smaller currency-value than sb, + * return a positive value if split sa has a larger currency-value than sb, + * return a negative value if split sa has a smaller share-price than sb, + * return a positive value if split sa has a larger share-price than sb, + * then compares memo and action using the strcmp() + * c-library routine, returning what strcmp would return. + * Then it compares the reconciled flags, then the reconciled dates, + * Finally, it returns zero if all of the above match. + * + */ + +int xaccTransOrder (Transaction *ta, Transaction *tb); +int xaccSplitDateOrder (Split *sa, Split *sb); + +/********************************************************************\ + * Miscellaneous utility routines. +\********************************************************************/ + +/* + * These functions compare two splits by different criteria. The *Other* + * functions attempt to find the split on the other side of a transaction + * and compare on it. They return similar to strcmp. + * + * These functions were added because converting strings to guile + * for comparisons in the transaction report is terribly inefficient. + * More may be added here in future if it turns out that other types + * of comparisons also induces guile slowdowns. + */ + +int xaccSplitCompareAccountFullNames(Split *sa, Split *sb); +int xaccSplitCompareAccountCodes(Split *sa, Split *sb); +int xaccSplitCompareOtherAccountFullNames(Split *sa, Split *sb); +int xaccSplitCompareOtherAccountCodes(Split *sa, Split *sb); + + +/* + * These functions take a split, get the corresponding split on the + * "other side" of the transaction, and extract either the name or code + * of that split, reverting to returning a constant "Split" if the + * transaction has more than one split on the "other side". These + * were added for the transaction report, and is in C because the code + * was already written in C for the above functions and duplication + * is silly. + */ + +char * xaccSplitGetCorrAccountFullName(Split *sa, char seperator); +const char * xaccSplitGetCorrAccountName(Split *sa); +const char * xaccSplitGetCorrAccountCode(Split *sa); + +/* + * The xaccGetAccountByName() is a convenience routine that + * is essentially identical to xaccGetPeerAccountFromName(), + * except that it accepts the handy transaction as root. + * + * The xaccGetAccountByFullName routine is similar, but uses + * full names using the given separator. + */ +Account * xaccGetAccountByName (Transaction *trans, const char *name); +Account * xaccGetAccountByFullName (Transaction *trans, + const char *name, + const char separator); + +/* + * The xaccSplitGetOtherSplit() is a convenience routine that returns + * the other of a pair of splits. If there are more than two + * splits, it returns NULL. + */ +Split * xaccSplitGetOtherSplit (Split *split); + +/* The xaccIsPeerSplit() is a convenience routine that returns + * a non-zero value if the two splits share a common + * parent transaction, else it returns zero. + */ +int xaccIsPeerSplit (Split *split_1, Split *split_2); + +#endif /* XACC_TRANSACTION_H */ diff --git a/src/engine/TransactionP.h b/src/engine/TransactionP.h new file mode 100644 index 0000000000..7a6c544b5a --- /dev/null +++ b/src/engine/TransactionP.h @@ -0,0 +1,236 @@ +/********************************************************************\ + * TransactionP.h -- private header for transaction & splits * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997, 1998, 1999, 2000 Linas Vepstas * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * TransactionP.h + * + * FUNCTION: + * The is the *private* transaction header file. Code outside of + * engine should *not* include this file. This is because code + * outside of the engine should *never* access any of the structure + * members directly. + * + * Note that this header file also defines prototypes for various + * routines that perform sub-atomic updates of the accounting + * structures. If these routines are not used properly, they + * can result in inconsistent, unbalanced accounting structures. + * In other words, their use is dangerous, and their use outside + * of the scope of the engine is forbidden. + * + */ + +#ifndef XACC_TRANSACTION_P_H +#define XACC_TRANSACTION_P_H + +#include +#include + +#include "config.h" +#include "kvp_frame.h" +#include "gnc-numeric.h" +#include "Transaction.h" /* for typedefs */ +#include "GNCId.h" + + +/** STRUCTS *********************************************************/ +/* + * Double-entry is forced by having at least two splits in every + * transaction. By convention, (and only by convention, not by + * any innate requirement), the first split is considered to be + * the source split or the crediting split, and the others are + * the destination, or debiting splits. The grand total of all + * of the splits must always be kept zero. + */ + +/* A split transaction is one which shows up as a credit (or debit) in + * one account, and pieces of it show up as debits (or credits) in other + * accounts. Thus, a single credit-card transaction might be split + * between "dining", "tips" and "taxes" categories. + * + * A "split" is more commonly refered to as a "entry" in a "transaction". + */ + + +struct _split +{ + GUID guid; /* globally unique id */ + + GUID acc_guid; /* the guid of the associated account */ + Account *acc; /* back-pointer to debited/credited account */ + + Transaction *parent; /* parent of split */ + + /* The memo field is an arbitrary user-assiged value. + * It is intended to hold a short (zero to forty character) string + * that is displayed by the GUI along with this split. + */ + char * memo; + + /* The action field is an arbitrary user-assigned value. + * It is meant to be a very short (one to ten cahracter) string that + * signifies the "type" of this split, such as e.g. Buy, Sell, Div, + * Withdraw, Deposit, ATM, Check, etc. The idea is that this field + * can be used to create custom reports or graphs of data. + */ + char * action; /* Buy, Sell, Div, etc. */ + + /* kvp_data is a key-value pair database for storing simple + * "extra" information in splits, transactions, and accounts. + * it's NULL until accessed. */ + kvp_frame * kvp_data; + + char reconciled; /* The reconciled field */ + Timespec date_reconciled; /* date split was reconciled */ + + /* 'value' is the amount of the transaction balancing commodity + * (i.e. currency) involved, 'amount' is the amount of the account's + * commodity (formerly known as 'security') involved. */ + gnc_numeric value; + gnc_numeric amount; + + /* -------------------------------------------------------------- */ + /* Below follow some 'temporary' fields */ + + /* The various "balances" are the sum of all of the values of + * all the splits in the account, up to and including this split. + * These balances apply to a sorting order by date posted + * (not by date entered). */ + gnc_numeric balance; + gnc_numeric cleared_balance; + gnc_numeric reconciled_balance; + + /* -------------------------------------------------------------- */ + /* Backend private expansion data */ + guint32 idata; /* used by the sql backend for kvp management */ +}; + + +struct _transaction +{ + /* guid is a globally unique identifier which can be used to + * reference the transaction. + */ + GUID guid; + + Timespec date_entered; /* date register entry was made */ + Timespec date_posted; /* date transaction was posted at bank */ + + /* The num field is a arbitrary user-assigned field. + * It is intended to store a short id number, typically the check number, + * deposit number, invoice number or other tracking number. + */ + char * num; + + /* The description field is an arbitrary user-assigned value. + * It is meant to be a short descriptive phrase. + */ + char * description; + + /* kvp_data is a key-value pair database for storing simple + * "extra" information in splits, transactions, and accounts. + * it's NULL until accessed. */ + kvp_frame * kvp_data; + + + /* The common_currency field is the balancing common currency for + * all the splits in the transaction. + * + * This field is going to replace the currency field in the account + * structures. However, right now we are in a transition period: we + * store it here an in the account, and test its value dynamically + * for correctness. If we can run for a few months without errors, + * then we'll make the conversion permanent. + * + * Alternate, better(?) name: "valuation currency": it is the + * currency in which all of the splits can be valued. */ + gnc_commodity *common_currency; + + /* version number, used for tracking multiuser updates */ + gint32 version; + guint32 version_check; /* data aging timestamp */ + + GList * splits; /* list of splits */ + + /* marker is used to track the progress of transaction traversals. + * 0 is never a legitimate marker value, so we can tell is we hit + * a new transaction in the middle of a traversal. All each new + * traversal cares about is whether or not the marker stored in + * a transaction is the same as or different than the one + * corresponding to the current traversal. */ + unsigned char marker; + + gint32 editlevel; /* nestcount of begin/end edit calls */ + gboolean do_free; /* transaction in process of being destroyed */ + + /* the orig pointer points at a copy of the original transaction, + * before editing was started. This orig copy is used to rollback + * any changes made if/when the edit is abandoned. + */ + Transaction *orig; + + /* -------------------------------------------------------------- */ + /* Backend private expansion data */ + guint32 idata; /* used by the sql backend for kvp management */ +}; + +/* Set the transaction's GUID. This should only be done when reading + * a transaction from a datafile, or some other external source. Never + * call this on an existing transaction! */ +void xaccTransSetGUID (Transaction *trans, const GUID *guid); + +/* Set the split's GUID. This should only be done when reading + * a split from a datafile, or some other external source. Never + * call this on an existing split! */ +void xaccSplitSetGUID (Split *split, const GUID *guid); + +/* The xaccFreeSplit() method simply frees all memory associated + * with the split. It does not verify that the split isn't + * referenced in some account. If the split is referenced by an + * account, then calling this method will leave the system in an + * inconsistent state. + */ +void xaccFreeSplit (Split *split); /* frees memory */ + +/* compute the value of a list of splits in the given currency, + * excluding the skip_me split. */ +gnc_numeric xaccSplitsComputeValue (GList *splits, Split * skip_me, + const gnc_commodity * base_currency); + +/* The xaccTransSet/GetVersion() routines set & get the version + * numbers on this transaction. The version number is used to manage + * multi-user updates. These routines are private because we don't + * want anyone except the backend to mess with them. + */ +void xaccTransSetVersion (Transaction*, gint32); +gint32 xaccTransGetVersion (Transaction*); + +/* The xaccTransFindCommonCurrency () method returns a gnc_commodity + * indicating a currency denomination that all of the splits in this + * transaction have in common, using the old currency/security fields + * of the split accounts. */ +gnc_commodity * xaccTransFindOldCommonCurrency (Transaction *trans); + +#endif /* XACC_TRANSACTION_P_H */ diff --git a/src/engine/date.c b/src/engine/date.c new file mode 100644 index 0000000000..db08ff0f85 --- /dev/null +++ b/src/engine/date.c @@ -0,0 +1,701 @@ +/********************************************************************\ + * date.c -- utility functions to handle the date (adjusting, get * + * current date, etc.) for xacc (X-Accountant) * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1998, 1999, 2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + * Author: Rob Clark rclark@cs.hmc.edu * + * * +\********************************************************************/ + +#define _GNU_SOURCE +#define __EXTENSIONS__ + +#include "config.h" + +#include + +#ifdef HAVE_LANGINFO_D_FMT +#include +#endif + +#include +#include +#include +#include + +#include + +#include "date.h" +#include "gnc-engine-util.h" + +#define NANOS_PER_SECOND 1000000000 + +#ifdef HAVE_LANGINFO_D_FMT +# define GNC_D_FMT (nl_langinfo (D_FMT)) +#else +# define GNC_D_FMT "%Y-%m-%d" +#endif + +/* This is now user configured through the gnome options system() */ +static DateFormat dateFormat = DATE_FORMAT_US; + +/* This static indicates the debugging module that this .o belongs to. */ +static short module = MOD_ENGINE; + + +/********************************************************************\ +\********************************************************************/ + +static void +timespec_normalize(Timespec *t) +{ + if(t->tv_nsec > NANOS_PER_SECOND) + { + t->tv_sec+= (t->tv_nsec / NANOS_PER_SECOND); + t->tv_nsec= t->tv_nsec % NANOS_PER_SECOND; + } + + if(t->tv_nsec < - NANOS_PER_SECOND) + { + t->tv_sec+= - (-t->tv_nsec / NANOS_PER_SECOND); + t->tv_nsec = - (-t->tv_nsec % NANOS_PER_SECOND); + } + + if (t->tv_sec > 0 && t->tv_nsec < 0) + { + t->tv_sec--; + t->tv_nsec = NANOS_PER_SECOND + t->tv_nsec; + } + + if (t->tv_sec < 0 && t->tv_nsec > 0) + { + t->tv_sec++; + t->tv_nsec = - NANOS_PER_SECOND + t->tv_nsec; + } + return; +} + + +gboolean +timespec_equal (const Timespec *ta, const Timespec *tb) +{ + if(ta == tb) return TRUE; + if(ta->tv_sec != tb->tv_sec) return FALSE; + if(ta->tv_nsec != tb->tv_nsec) return FALSE; + return TRUE; +} + +gint +timespec_cmp(const Timespec *ta, const Timespec *tb) +{ + if(ta == tb) return 0; + if(ta->tv_sec < tb->tv_sec) return -1; + if(ta->tv_sec > tb->tv_sec) return 1; + if(ta->tv_nsec < tb->tv_nsec) return -1; + if(ta->tv_nsec > tb->tv_nsec) return 1; + return 0; +} + +Timespec +timespec_diff(const Timespec *ta, const Timespec *tb) +{ + Timespec retval; + retval.tv_sec = ta->tv_sec - tb->tv_sec; + retval.tv_nsec = ta->tv_nsec - tb->tv_nsec; + timespec_normalize(&retval); + return retval; +} + +Timespec +timespec_abs(const Timespec *t) +{ + Timespec retval = *t; + + timespec_normalize(&retval); + if (retval.tv_sec < 0) + { + retval.tv_sec = - retval.tv_sec; + retval.tv_nsec = - retval.tv_nsec; + } + + return retval; +} + +/** + * timespecCanonicalDayTime + * given a timepair contains any time on a certain day (local time) + * converts it to be midday that day. + */ + +Timespec +timespecCanonicalDayTime(Timespec t) +{ + struct tm tm, *result; + Timespec retval; + time_t t_secs = t.tv_sec + (t.tv_nsec / NANOS_PER_SECOND); + result = localtime(&t_secs); + tm = *result; + tm.tm_sec = 0; + tm.tm_min = 0; + tm.tm_hour = 12; + tm.tm_isdst = -1; + retval.tv_sec = mktime(&tm); + retval.tv_nsec = 0; + return retval; +} + +/** + * setDateFormat + * set date format to one of US, UK, CE, OR ISO + * checks to make sure it's a legal value + * Args: DateFormat: enumeration indicating preferred format + * returns: nothing + * + * Globals: dateFormat + **/ + +void setDateFormat(DateFormat df) +{ + if(df >= DATE_FORMAT_FIRST && df <= DATE_FORMAT_LAST) + { + dateFormat = df; + } + else + { /* hack alert - is this what we should be doing here? */ + PERR("non-existent date format set"); + } + + return; +} + +/** + * printDate + * Convert a date as day / month / year integers into a localized string + * representation + * + * Args: buff - pointer to previously allocated character array; its size + * must be at lease MAX_DATE_LENTH bytes. + * day - day of the month as 1 ... 31 + * month - month of the year as 1 ... 12 + * year - year (4-digit) + * + * Return: nothing + * + * Globals: global dateFormat value + */ +void +printDate (char * buff, int day, int month, int year) +{ + if (!buff) return; + + /* Note that when printing year, we use %-4d in format string; + * this causes a one, two or three-digit year to be left-adjusted + * when printed (i.e. padded with blanks on the right). This is + * important while the user is editing the year, since erasing a + * digit can temporarily cause a three-digit year, and having the + * blank on the left is a real pain for the user. So pad on the + * right. + */ + switch(dateFormat) + { + case DATE_FORMAT_UK: + sprintf (buff, "%2d/%2d/%-4d", day, month, year); + break; + case DATE_FORMAT_CE: + sprintf (buff, "%2d.%2d.%-4d", day, month, year); + break; + case DATE_FORMAT_ISO: + sprintf (buff, "%04d-%02d-%02d", year, month, day); + break; + case DATE_FORMAT_LOCALE: + { + struct tm tm_str; + + tm_str.tm_mday = day; + tm_str.tm_mon = month - 1; /* tm_mon = 0 through 11 */ + tm_str.tm_year = year - 1900; /* this is what the standard + * says, it's not a Y2K thing */ + tm_str.tm_hour = 0; + tm_str.tm_min = 0; + tm_str.tm_sec = 0; + tm_str.tm_isdst = -1; + + strftime (buff, MAX_DATE_LENGTH, GNC_D_FMT, &tm_str); + } + break; + + case DATE_FORMAT_US: + default: + sprintf (buff, "%2d/%2d/%-4d", month, day, year); + break; + } +} + +void +printDateSecs (char * buff, time_t t) +{ + struct tm *theTime; + + if (!buff) return; + + theTime = localtime (&t); + + printDate (buff, theTime->tm_mday, + theTime->tm_mon + 1, + theTime->tm_year + 1900); +} + +char * +xaccPrintDateSecs (time_t t) +{ + char buff[100]; + printDateSecs (buff, t); + return g_strdup (buff); +} + +const char * +gnc_print_date (Timespec ts) +{ + static char buff[MAX_DATE_LENGTH]; + time_t t; + + t = ts.tv_sec + (ts.tv_nsec / 1000000000.0); + + printDateSecs (buff, t); + + return buff; +} + +/** + * scanDate + * Convert a string into day / month / year integers according to + * the current dateFormat value. + * + * Args: buff - pointer to date string + * day - will store day of the month as 1 ... 31 + * month - will store month of the year as 1 ... 12 + * year - will store the year (4-digit) + * + * Return: nothing + * + * Globals: global dateFormat value + */ +void +scanDate (const char *buff, int *day, int *month, int *year) +{ + char *dupe, *tmp, *first_field, *second_field, *third_field; + int iday, imonth, iyear; + struct tm *now; + time_t secs; + + if (!buff) return; + + dupe = g_strdup (buff); + + tmp = dupe; + first_field = NULL; + second_field = NULL; + third_field = NULL; + + /* use strtok to find delimiters */ + if (tmp) { + first_field = strtok (tmp, ".,-+/\\()"); + if (first_field) { + second_field = strtok (NULL, ".,-+/\\()"); + if (second_field) { + third_field = strtok (NULL, ".,-+/\\()"); + } + } + } + + /* if any fields appear blank, use today's date */ + time (&secs); + now = localtime (&secs); + iday = now->tm_mday; + imonth = now->tm_mon+1; + iyear = now->tm_year+1900; + + /* get numeric values */ + switch (dateFormat) + { + case DATE_FORMAT_LOCALE: + if (buff[0] != '\0') + { + struct tm thetime; + + strptime (buff, GNC_D_FMT, &thetime); + + iday = thetime.tm_mday; + imonth = thetime.tm_mon + 1; + iyear = thetime.tm_year + 1900; + } + break; + case DATE_FORMAT_UK: + case DATE_FORMAT_CE: + if (first_field) iday = atoi (first_field); + if (second_field) imonth = atoi (second_field); + if (third_field) iyear = atoi (third_field); + break; + case DATE_FORMAT_ISO: + if (first_field) iyear = atoi (first_field); + if (second_field) imonth = atoi (second_field); + if (third_field) iday = atoi (third_field); + break; + case DATE_FORMAT_US: + default: + if (first_field) imonth = atoi (first_field); + if (second_field) iday = atoi (second_field); + if (third_field) iyear = atoi (third_field); + break; + } + + g_free (dupe); + + /* if the year entered is smaller than 100, assume we mean the current + century (and are not revising some roman emperor's books) */ + if (iyear < 100) + iyear += ((int) ((now->tm_year+1950-iyear)/100)) * 100; + + if (year) *year=iyear; + if (month) *month=imonth; + if (day) *day=iday; +} + +/** + * dateSeparator + * Return the field separator for the current date format + * + * Args: none + * + * Return: date character + * + * Globals: global dateFormat value + */ +char dateSeparator () +{ + static char locale_separator = '\0'; + + switch (dateFormat) + { + case DATE_FORMAT_CE: + return '.'; + case DATE_FORMAT_ISO: + return '-'; + case DATE_FORMAT_US: + case DATE_FORMAT_UK: + default: + return '/'; + case DATE_FORMAT_LOCALE: + if (locale_separator != '\0') + return locale_separator; + else + { /* Make a guess */ + char string[256]; + struct tm *tm; + time_t secs; + char *s; + + secs = time(NULL); + tm = localtime(&secs); + strftime(string, sizeof(string), GNC_D_FMT, tm); + + for (s = string; s != '\0'; s++) + if (!isdigit(*s)) + return (locale_separator = *s); + } + } + + return '\0'; +} + +/********************************************************************\ + * iso 8601 datetimes should look like 1998-07-02 11:00:00.68-05 +\********************************************************************/ +/* hack alert -- this routine returns incorrect values for + * dates before 1970 */ + +static Timespec +gnc_iso8601_to_timespec(const char *str, int do_localtime) +{ + char buf[4]; + Timespec ts; + struct tm stm; + long int nsec =0; + + ts.tv_sec=0; + ts.tv_nsec=0; + if (!str) return ts; + + stm.tm_year = atoi(str) - 1900; + str = strchr (str, '-'); if (str) { str++; } else { return ts; } + stm.tm_mon = atoi(str) - 1; + str = strchr (str, '-'); if (str) { str++; } else { return ts; } + stm.tm_mday = atoi(str); + + str = strchr (str, ' '); if (str) { str++; } else { return ts; } + stm.tm_hour = atoi(str); + str = strchr (str, ':'); if (str) { str++; } else { return ts; } + stm.tm_min = atoi(str); + str = strchr (str, ':'); if (str) { str++; } else { return ts; } + stm.tm_sec = atoi (str); + + /* the decimal point, optionally present ... */ + /* hack alert -- this algo breaks if more than 9 decimal places present */ + if (strchr (str, '.')) + { + int decimals, i, multiplier=1000000000; + str = strchr (str, '.') +1; + decimals = strcspn (str, "+- "); + for (i=0; itz_min) { tz_min +=60; tz_hour --; } + if (60<=tz_min) { tz_min -=60; tz_hour ++; } + + /* we also have to print the sign by hand, to work around a bug + * in the glibc 2.1.3 printf (where %+02d fails to zero-pad) + */ + cyn = '-'; + if (0>tz_hour) { cyn = '+'; tz_hour = -tz_hour; } + + len = sprintf (buff, "%4d-%02d-%02d %02d:%02d:%02d.%06ld %c%02d%02d", + parsed.tm_year + 1900, + parsed.tm_mon + 1, + parsed.tm_mday, + parsed.tm_hour, + parsed.tm_min, + parsed.tm_sec, + ts.tv_nsec / 1000, + cyn, + tz_hour, + tz_min); + + /* return pointer to end of string */ + buff += len; + return buff; +} + + +/********************************************************************\ +\********************************************************************/ +/* hack alert -- this routine returns incorrect values for + * dates before 1970 */ + +time_t +xaccDMYToSec (int day, int month, int year) +{ + struct tm stm; + time_t secs; + + stm.tm_year = year - 1900; + stm.tm_mon = month - 1; + stm.tm_mday = day; + stm.tm_hour = 0; + stm.tm_min = 0; + stm.tm_sec = 0; + stm.tm_isdst = -1; + + /* compute number of seconds */ + secs = mktime (&stm); + + return secs; +} + +time_t +xaccScanDateS (const char *str) +{ + int month, day, year; + + scanDate (str, &day, &month, &year); + + return xaccDMYToSec (day,month,year); +} + +#define THIRTY_TWO_YEARS 0x3c30fc00LL + +static Timespec +gnc_dmy2timespec_internal (int day, int month, int year, gboolean start_of_day) +{ + Timespec result; + struct tm date; + long long secs = 0; + long long era = 0; + + year -= 1900; + + /* make a crude attempt to deal with dates outside the range of Dec + * 1901 to Jan 2038. Note we screw up centennial leap years here so + * hack alert */ + if ((2 > year) || (136 < year)) + { + era = year / 32; + year %= 32; + if (0 > year) { year += 32; era -= 1; } + } + + date.tm_year = year; + date.tm_mon = month - 1; + date.tm_mday = day; + + if (start_of_day) + { + date.tm_hour = 0; + date.tm_min = 0; + date.tm_sec = 0; + } + else + { + date.tm_hour = 23; + date.tm_min = 59; + date.tm_sec = 59; + } + + date.tm_isdst = -1; + + /* compute number of seconds */ + secs = mktime (&date); + + secs += era * THIRTY_TWO_YEARS; + + result.tv_sec = secs; + result.tv_nsec = 0; + + return result; +} + +Timespec +gnc_dmy2timespec (int day, int month, int year) +{ + return gnc_dmy2timespec_internal (day, month, year, TRUE); +} + +Timespec +gnc_dmy2timespec_end (int day, int month, int year) +{ + return gnc_dmy2timespec_internal (day, month, year, FALSE); +} + +/********************************************************************\ +\********************************************************************/ + +long int +gnc_timezone (struct tm *tm) +{ + g_return_val_if_fail (tm != NULL, 0); + +#ifdef HAVE_STRUCT_TM_GMTOFF + /* tm_gmtoff is seconds *east* of UTC and is + * already adjusted for daylight savings time. */ + return -(tm->tm_gmtoff); +#else + /* timezone is seconds *west* of UTC and is + * not adjusted for daylight savings time. + * In Spring, we spring forward, wheee! */ + return timezone - (tm->tm_isdst > 0 ? 60 * 60 : 0); +#endif +} + + +void +timespecFromTime_t( Timespec *ts, time_t t ) +{ + ts->tv_sec = t; + ts->tv_nsec = 0; +} + +/********************** END OF FILE *********************************\ +\********************************************************************/ diff --git a/src/engine/date.h b/src/engine/date.h new file mode 100644 index 0000000000..ce4cf6833a --- /dev/null +++ b/src/engine/date.h @@ -0,0 +1,206 @@ +/********************************************************************\ + * date.h -- utility functions to handle the date (adjusting, get * + * current date, etc.) for GnuCash * + * Copyright (C) 1997 Robin D. Clark (rclark@cs.hmc.edu) * + * Copyright (C) 1998, 1999, 2000 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * +\********************************************************************/ + +/* hack alert -- the scan and print routines should probably be moved + * to somewhere else. The engine really isn't involved with things + * like printing formats. This is needed mostly by the GUI and so on. + * If a file-io thing needs date handling, it should do it itself, + * instead of depending on the routines here. */ + +#ifndef XACC_DATE_H +#define XACC_DATE_H + +#include + + +/** Constants *******************************************************/ + +typedef enum +{ + DATE_FORMAT_US, /* United states: mm/dd/yyyy */ + DATE_FORMAT_UK, /* Britain: dd/mm/yyyy */ + DATE_FORMAT_CE, /* Continental Europe: dd.mm.yyyy */ + DATE_FORMAT_ISO, /* ISO: yyyy-mm-dd */ + DATE_FORMAT_LOCALE /* Take from locale information */ +} DateFormat; + +#define DATE_FORMAT_FIRST DATE_FORMAT_US +#define DATE_FORMAT_LAST DATE_FORMAT_LOCALE + +/* the maximum length of a string created by the date printers */ +#define MAX_DATE_LENGTH 11 + + +/** Datatypes *******************************************************/ + +/* struct timespec64 is just like timespec except that we use a 64-bit + * signed int to store the seconds. This should adequately cover + * dates in the distant future as well as the distant past, as long as + * they're not more than a couple dozen times the age of the universe. + * Note that both gcc and the IBM Toronto xlC compiler (aka CSet, + * VisualAge, etc) correctly handle long long as a 64 bit quantity, + * even on the 32-bit Intel x86 and PowerPC architectures. I'm + * assuming that all the other modern compilers are clean on this + * issue too. */ + +#ifndef SWIG /* swig 1.1p5 can't hack the long long type */ +struct timespec64 +{ + long long int tv_sec; + long int tv_nsec; +}; +#endif /* SWIG */ + +typedef struct timespec64 Timespec; + + +/** Prototypes ******************************************************/ + +/* strict equality */ +gboolean timespec_equal(const Timespec *ta, const Timespec *tb); +/* comparison: if (ta < tb) -1; else if (ta > tb) 1; else 0; */ +int timespec_cmp(const Timespec *ta, const Timespec *tb); + +/* difference between ta and tb, results are normalised + * ie tv_sec and tv_nsec of the result have the same size + * abs(result.tv_nsec) <= 1000000000 + */ + +Timespec timespec_diff(const Timespec *ta, const Timespec *tb); + +/* + * absolute value, also normalised + */ +Timespec timespec_abs(const Timespec *t); + +/* + * convert a timepair on a certain day (localtime) to + * the timepair representing midday on that day + */ + +Timespec timespecCanonicalDayTime(Timespec t); + +void setDateFormat(DateFormat df); + +/** + * printDate + * Convert a date as day / month / year integers into a localized string + * representation + * + * Args: buff - pointer to previously allocated character array; its size + * must be at lease MAX_DATE_LENTH bytes. + * day - day of the month as 1 ... 31 + * month - month of the year as 1 ... 12 + * year - year (4-digit) + * + * Return: nothing + * + * Globals: global dateFormat value + */ +void printDate (char * buff, int day, int month, int year); +void printDateSecs (char * buff, time_t secs); + +char * xaccPrintDateSecs (time_t secs); +const char * gnc_print_date(Timespec ts); + +/** + * Turns a time_t into a Timespec + **/ +void timespecFromTime_t( Timespec *ts, time_t t ); + +/** + * scanDate + * Convert a string into day / month / year integers according to + * the current dateFormat value. + * + * Args: buff - pointer to date string + * day - will store day of the month as 1 ... 31 + * month - will store month of the year as 1 ... 12 + * year - will store the year (4-digit) + * + * Return: nothing + * + * Globals: global dateFormat value + */ +void scanDate (const char *buff, int *day, int *month, int *year); + +/** + * dateSeparator + * Return the field separator for the current date format + * + * Args: none + * + * Return: date character + * + * Globals: global dateFormat value + */ +char dateSeparator(void); + +/* + * hack alert XXX FIXME -- these date routines return incorrect + * values for dates before 1970. Most of them are good only up + * till 2038. This needs fixing ... + */ + +time_t xaccDMYToSec (int day, int month, int year); +time_t xaccScanDateS (const char *buff); + +/* Convert a day, month, and year to a Timespec */ +Timespec gnc_dmy2timespec (int day, int month, int year); + +/* Same as gnc_dmy2timespec, but last second of the day */ +Timespec gnc_dmy2timespec_end (int day, int month, int year); + +/* The gnc_iso8601_to_timespec_xxx() routines converts an ISO-8601 style + * date/time string to Timespec. + * For example: 1998-07-17 11:00:00.68-05 + * is 680 milliseconds after 11 o'clock, central daylight time + * The _gmt() routine returns the time in gmt. The _local() routine + * returns the local time. + * + * The gnc_timespec_to_iso8601_buff() routine prints a Timespec + * as an ISO-8601 style string. The buffer must be long enough + * to contain the string. The string is null-terminated. This + * routine returns a pointer to the null terminator (and can + * thus be used in the 'stpcpy' metaphor of string concatenation). + */ +Timespec gnc_iso8601_to_timespec_local(const char *); +Timespec gnc_iso8601_to_timespec_gmt(const char *); +char * gnc_timespec_to_iso8601_buff (Timespec ts, char * buff); + +/* The gnc_timezone function returns the number of seconds *west* + * of UTC represented by the tm argument, adjusted for daylight + * savings time. + * + * This function requires a tm argument returned by localtime or set + * by mktime. This is a strange function! It requires that localtime + * or mktime be called before use. Subsequent calls to localtime or + * mktime *may* invalidate the result! The actual contents of tm *may* + * be used for both timezone offset and daylight savings time, or only + * daylight savings time! Timezone stuff under unix is not + * standardized and is a big mess. + */ +long int gnc_timezone (struct tm *tm); + +#endif /* XACC_DATE_H */ diff --git a/src/engine/design.txt b/src/engine/design.txt new file mode 100644 index 0000000000..41ce549ad7 --- /dev/null +++ b/src/engine/design.txt @@ -0,0 +1,397 @@ +This document is becoming obsolete. Please refer to the design +documentation in src/doc/design for a complete description of the +Engine architecture. + +Accounting Engine +----------------- +This document reviews the operation of, and various design points +pertinent to the GnuCash accounting engine. The latest version +of this document can be found in the engine source-code directory. + +Stocks, non-Currency-Denominated Assets +--------------------------------------- +The engine includes support for non-currency-denominated assets, +such as stocks, bonds, mutual funds, inventory. This is done with +two values in the Split structure: + + double share_price; + double damount; + +"damount" is the number of shares/items. It is an "immutable" quantity, +in that it cannot change except by transfer (sale/purchase). It is the +quantity that is used when computing balances. + +"share_price" is the price of the item in question. The share-price is +of course subject to fluctuation. + +The net-value of a split is the product of "damount" and "share_price". +The currency balance of an account is the sum of all "damounts" times +the latest, newest share-price. + +Currency accounts should use a share price of 1.0 for all splits. + +To maintain the double-entry consistency, one must have the following +hold true: + + 0.0 == sum of all split values. + +If all splits are in the same currency, then this becomes: + + 0.0 == sum of all ((split->damount) * (split->share_price)) + +Thus, for example, the purchase of shares can be represented as: + + source: + debit ABC Bank for $1045 (1045 dollars * dollar "price" of 1.00) + + destination: + credit PQR Stock for $1000 (100 shares at $10 per share) + credit StockBroker category $45 in fees + +If the splits are in mixed currencies and securities, then there must +be at least one common currency/security between all of them. Thus, +for example: + + source: + debit ABC Bank for $1045 (1045 dollars * dollar "price" of 1.00) + + destination: + credit VolkTrader for 2000 DM (1000 dollars at 2.0 mark per dollar) + credit Fees category $45 in fees + +If the "currency" field is set to "DM" for the VolksTrader account, +and the "security" field is set to "USD", while the currency for ABC bank is +"USD", then the balancing equation becomes: + +0.0 = 1045 * $1.00 - $1000 - 45 * $1.00 + +Note that we ignored the price when adding the second split. + +Recoding a Stock Price +---------------------- +A stock price may be recorded in a brokerage account with a single +split that has zero value: +(share price) * (zero shares) == (zero dollars) +This transaction does not violate the rules that all transactions must +have zero value. This transaction is ideal for recording prices. Its +the only transaction type that may have a single split; everything else +requires at least two splits to balance. (at least when double-entry +is enabled). + +Recording a Stock Split +----------------------- +Stock splits (i.e. when a company issues x shares of new stock for every +share already owned) may be recorded with a pair of journal entries as +follows: + +(-old num shrs) * (old price) + (new num shrs) * (new price) == 0.0 +where each journal entry credits/debits the same account. +Of course (new num shrs) == (1+x) * (old num shrs) +and the price goes inversely. + +Stock Options +------------- +Stock options are not currently supported. To support them, the +following needs to be added: + +A stock option is an option to purchase stock at a specified price. +Options have an expiration date. When you purchase an option it is +pretty much like buying stock. However, some extra information needs +to be recorded. To fully record an option purchase, you need to record +the underlying stock that the option is on, the strike price (i.e. the +price that the underlying stock can be purchases for), an expiration date, +and whether the option is a put or a call. A put option is the option +to sell stock at the strike price, and a call option is the option to +purchase stock at the strike price. Once an option is bought, it can +have one of three dispositions: it can be sold, in which case, it is +pretty much just like a stock transaction. It can expire, in which +case the option is worthless, and (IIRC) can be/is treated as a sale +at a zero price. Thirdly, it can be exercised, which is a single +transaction whereby stock is purchased at the strike price, and +the option becomes worthless. + +Another point: with standardized options one option contract represents +the ability to purchase (with a call option) or sell (with a put option) +100 shares of the underlying stock. + + +Error Reporting +--------------- +The error reporting architecture (partially implemented), uses a globally +visible subroutine to return an error. In the naivest possible implementation, +the error reporting mechanism would look like this: + + int error_num; /* global error number */ + + int xaccGetError (void) { return error_num; } + + void xaccSomeFunction (Args *various_args) { + if (bad_thing_happened) error_num = 42; + } + +Many programmers are used to a different interface, e.g. + + int xaccSomeFunction (Args *various_args) { + if (bad_thing_happened) return (42); + } + +Because of this, it is important to explain why the former design was +choosen over the latter. Let us begin by listing how the choosen design +is as good as, and in many ways can be better to the later design. + + (1) Allows programmer to check for errors asynchronously, e.g. outside + of a performance critical loop, or far away, after the return of + several subroutines. + (2) (with the right implementation) Allows reporting of multiple, complex + errors. For example, it can be used to implement a trace mechanism. + (3) (with the right implementation) Can be thread safe. + (4) Allows errors that occurred deep in the implementation to be reported + up to much higher levels without requiring baggage in the middle. + +The right implementation for (2) is to implement not a single +variable, but a stack or a ring (circular queue) on which error codes +are placed, and from which error codes can be retreived. The right +implementation for (3) is the use pthread_getspecific() to define a +per-thread global and/or ring/queue. + + +Engine Isolation +---------------- +Goals of engine isolation: + o Hide the engine behind an API so that multiple, pluggable engines + could be created, e.g. SQL or CORBA. + o Engine users are blocked from being able to put engine internal + structures in an inconsistent state. Updates are "atomic". + +Some half-finished thoughts about the engine API: + +-- The engine structures should not be accessible to any code outside + of the engine. Thus, the engine structures have been moved to + AccountP.h, TransactionP.h, etc. + The *P.h files should not be included by code outside of the engine. + +-- The down-side of hiding is that it can hurt performance. Even trivial data + accesses require a subroutine call. Maybe a smarter idea would be to leave + the structures exposed, allow direct manipulation, and then "copy-in" and + "copy-out" the structures into parallel structures when a hidden back end + needs to be invoked. + +-- the upside of hiding behind an API is that the engine can be + instrumented with extension language (perl, scheme, tcl, python) hooks + for pre/post processing of the data. To further enable such hooks, we + should probably surround all operations on structures with "begin-edit" + and "end-edit" calls. + +-- begin/end braces could potentially be useful for two-phase commit schemes. + where "end-edit" is replaced by "commit-edit" or "reject-edit". + + +Reconciliation +-------------- +The 'reconcile' state of a transaction can have one of the following values: + +/* Values for the reconciled field in Transaction: */ +#define NREC 'n' /* not reconciled or cleared */ +#define CREC 'c' /* The transaction has been cleared */ +#define YREC 'y' /* The transaction has been reconciled */ +#define FREC 'f' /* frozen into accounting period */ + +(Note that FREC is not yet used/implemented ...) + +The process of reconciliation works as follows: +1) User enters new transaction. All splits are marked 'n' for 'new' + or 'no, not yet reconciled'. +2) User beleives that the transaction has cleared the bank, + e.g. that a cheque written out has been deposited/cashed. + User clicks in the 'R' column in the register gui, + marking the split 'c' for 'cleared'. User can freely + toggle this flag from the GUI with essentially no penalty, + no complaints. This is a 'safe' operation. Note that the + register shows the 'cleared' subtotal, which is, essentially, + a guess of the banks view of the account balance. +3) When user gets the bank statement, user launches the + reconcile dialogue. This dialogue is used to match transactions + on the bank statement with which is recorded locally. + Reconciled transactions are marked with a 'y'. + Note that once a transaction has been marked 'y', and the + user has 'finished' with the reconcile dialogue, then it + should be 'hard' to change the reconcile state from + the ordinary register dialogue. It should be sort-of + 'set in stone'. (The engine does NOT enforce this, + only the gui enforces this.) +4) When the books are closed, all splits should be marked + 'frozen', and become truly un-editable. The engine should + enforce 'frozen', and just plain not allow editing of closed + books, period. The only wat to change this would have been + to re-open the book. (and the reopening of a book would + change all 'f' to 'y'.) + +About storing dates associated with reconcile: +> I think that there should be a date stamp attached to the reconciliation +> field so that as well as knowing that it has been reconciled, you also +> know *when* it was reconciled. +> +> This isn't so important for personal finances for the periodic user; I +> have in the past wanted to know when a particular transaction was +> reconciled. This is useful if you want to trace back from the +> electronic record to determine when the item actually cleared through +> the bank. +> +> This means that I can look at Cheque #428, written Jan 1/97, cashed in May +> 1997 (it sat in someone's desk for a while) in the computer system and say +> "Ah. It was marked as reconciled on June 12th/97. That was when I did the +> reconciliation of the May bank statements. Ergo, the cheque cleared in May, +> and that's the statement to go to to find a copy of the cheque..." +> +> It's not terribly important for cheques that get cashed right away; it *is* +> for things that hang around uncashed for a while. + +If the above is implemented, what date should be stored if the user +toggles the recn flag a few time? The date of the last toggle? +The very first date that it was recn'ed? + + +Automatic Backup +---------------- +The following has been implemented: + +Have (by default) xacc create a backup file +filename-timestamp.xac on every save. This will eat up some disk +space until you go back and kill the older versions, but it's +better than not realizing that the data's subtly corrupt a week +later. + +A lot of small-office/home systems do this. primarily useful as a +historical record, in case you accidentally wipe out something, and +don't spot it until later. Limited usefulness, but very nice in case +you accidentally delete an entire account. + +To a limited degree, it provides atomicity/consistency/etc at the +course-grained account-group level. + + +Transaction Processing +---------------------- +There is a rudimentary level of "TP" build in, via the routines +xaccTransBeginEdit(), xaccTransRollbackEdit(), and xaccTransCommitEdit(), +which allow changes to be made to a transaction, and then commited, +or rejected at the end. Handy for the GUI, if the user makes a bunch +of changes which they don't want to keep; also handy for an SQL back end, +where the Commit() routine triggers the actual update of the SQL database. + +Note: 'Commit' is sometimes called 'post' in other systems: The +act of 'commiting' a transaction is the same as 'posting' the +transaction to the general ledger. + +Some important implementation details to understand: the GUI currently +uses begin/end as a convenience, and thus, may hold a transaction in +the 'edit' state for a portentially long time (minutes or hours). +Thus, begin/end should not map naively to a table-lock/unlock. +Instead, an optimistic-locking scheme, as below, is prefered. + +The SQL back-end should implement posting entirely in the +'Commit()' routine. The pseudo-algorithms should look as follows: + + BeginEdit () { + // save a copy of what it was before we start editing + old_transaction = this; + } + + SetValue (float amount) { + // some example editing done here + this->value = amount; + } + + Commit () { + LockTable(); + current = QueryTransaction(); + // check ton make sure that what the sql db stores + // is identical to what we have on record as the + // 'original' tansaction. If its not, then someone + // got in there and modified it while we weren't + // looking; so we better report this back toi user. + if (current != old_transaction) Error(); Unlock(); return err; + + // otherwise, all is OK, we have successfully obtained + // the lock and validated the data, so now just update things. + StoreTransaction (new_transaction); + UnlockTable(); + old_transaction = NULL; + } + + Rollback () { + // throw away the edits + this = old_transaction; + old_transaction = NULL; + } + +The GUI should check to make sure that the Commit() routine didn't fail. +If it did fail, then the GUI should pop up a notice to the user stating +that the fundamental underlying data has changed, and that the user +should try doing the edit again. (It might be nice to indicate which +other human user might have changed the data so that they could +coordinate as needed.) + + + +Journal Logs +------------ +The following has been implemented; see TransLog.c for details. + +Transaction logs. The idea was that every time a transaction +was called that would cause a data update, the information that +was updated would be dumped out into an "append only" log file. + +This somewhat parallels what better database systems do to ensure +integrity; Oracle, for instance, has what is called an "archive log." +You have a copy of the database "synced up" as at some point in time, +and can apply "archive logs" to bring that old copy of the database +up to date should something go wrong to trash today's copy. + +In effect, you'd have things like + +=== 97/01/01 04:32:00 === Add Transaction ==== [whatever was added] ==== +=== 97/01/01 04:32:02 === Delete Transaction ==== [whatever was deleted] ==== + +It also is a useful debugging tool, as if you make sure that the +"log_transaction()" call starts by opening the log file, writes, and +then closes and syncs, you know what is going on with the data even if +horrible things happen to the "master" database file. + +Session Mgmt +------------ +To allow the user of the engine some guarantee of atomic updates, +serialized file I/O, related miscellany, the concept of a session +is supported. No file IO can be performed until a session has +been created, and file updates are not guaranteed atomic unless +performed within a SessionBegin/SessionEnd pair. + +Note that (in the current implementation) data can be manipulated +outside of the session; its just that it cannot be saved/made persistent. + +The goal of session management is to ensure that e.g. two users don't end +up editing the same file at the same time, or, e.g. that an automatic +stock-quote update daemon running under a different pid doesn't trash +data being currently edited by the user. + + +Remaining Work Items +-------------------- +To find other remaining work items in the code, grep for the string +"hack alert". + + +Ideas for engine enhancements: +----------------------------- + 1) Have (by default) gnucash immediately re-read a file after every + write, and compare the two resulting AccountGroups for equality. + + During development, this is a good idea, as it will help uncover + thinko's more quickly, instead of letting them hide for weeks or months + (as the last one did). Its not a bad self-consistency check when + monkeying with the internals, damn the performance. + + It can be removed/disabled for product versions. + + +This document is dated May 2000 diff --git a/src/engine/extensions.txt b/src/engine/extensions.txt new file mode 100644 index 0000000000..652bab01b9 --- /dev/null +++ b/src/engine/extensions.txt @@ -0,0 +1,76 @@ + +Proposed Extensions +------------------- +The following are proposals for various, as-yet-unimplemented +enhancements. The goal of this document is to understand some +of the changes that will come soon to the interfaces. + +Accounting Periods +------------------ +Acconting periods are implemented by creating an object that +describes the accounting period, and then assigning every +transaction to at least one period. + +typedef struct _accountingPeriod { + GUID guid; // id + char *name; // user-selectable name + int kind; // enum {none, week, month, quarter, year } + Timespec * start_date; // + Timespec * end_date; + AccountGroup *topgrp; // chart of accounts for this period. +} AccountingPeriod; + +The Transaction struct has to be extended with a period guid. +Every transaction can belong to at most one accounting period. + +In addition, each chart of accounts needs to be extended with +an accounting period as well. This allows 'old' accounts to be deleted +from 'open books', without having to delete that same account from old +closed books. Basically, a new chart of accounts needs to be written +out/read from the engine for each new accounting period. + + +The xaccPeriodClose() subroutine performs the following: +-- crawl over all transactions and mark them as being part of this + accounting period (actually, new transactions should by default be + getting put into the currently open period...) +-- find the equity account (what if there is more than one equity account + ?? what if equity has sub-accounts ?? ) +-- transfer all income/expense to equity (shouldn't the equity account + match the income/expense heirarchy?) +-- return a new account group with new opening balances ... + + + +Changes to Transaction Structure +-------------------------------- +Add an accounting period guid (see above). + +Changes to Journal Entry (Split) Structure +------------------------------------------ +Voucher Reference +----------------- +Add a char * voucher; This is a generic id string that somehow +identifies the piece of paper/electronic document(s) that indicate +where/how to find the paper trial for this entry. This list of id's +should probably be a list of key-value pairs, where the keys & values +can be completely configured by the user, and hold the needed data +that the user wants to store. For the SQL backend, this is a key +to a user-definable table. + +==================================================================== + +Additional Banking Info + +BankId -- routing & transit number (US) or Bankleitzan (DE) or Banque (FR) +BranchID -- Agence (FR), blank for other countries + +AcctKey -- Cle (FR), blank in others + + +Account type enumerants: +bank account types: + checking, savings, moneymarket, creditline, cma (cash amangement account) + + + diff --git a/src/engine/gnc-associate-account.c b/src/engine/gnc-associate-account.c new file mode 100644 index 0000000000..d091e2eef3 --- /dev/null +++ b/src/engine/gnc-associate-account.c @@ -0,0 +1,491 @@ +/********************************************************************\ + * gnc-associate-account.h : api for associating income and * + * expense accounts with stock/mutual fund accounts, for tracking * + * dividends, brokerage, and other stock-related expenses and * + * income so that they can be reported * + * Copyright 2000 Gnumatic Incorporated * + * Written by Robert Merkel * + * * + * WARNING WARNING WARNING: THIS STUFF IS TOTALLY UNTESTED AND * + * IS ONLY IN CVS FOR SAFEKEEPING * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * +\********************************************************************/ + +#include "config.h" + +#include "gnc-associate-account.h" +#include "gnc-engine-util.h" + +static short module = MOD_ENGINE; + +/* Maps GNCTrackingIncomeCategory to string keys. If this enum + changes, update */ + +static char * income_to_key[] = {"income-miscellaneous", + "income-interest", + "income-dividend" + "income-long-term-capital-gain", + "income-short-term-capital-gain"}; + +/* Maps GNCTrackingExpenseCategory to string keys. If this enum + changes, update */ + +static char * expense_to_key[] = {"expense-miscellaneous", + "expense-commission"}; + +static kvp_frame * +get_assoc_acc_frame(kvp_frame *account_frame) +{ + kvp_frame *assoc_acc_frame; + kvp_value *assoc_acc_frame_kvpvalue = + kvp_frame_get_slot(account_frame, "associated-accounts"); + + assoc_acc_frame = kvp_value_get_frame(assoc_acc_frame_kvpvalue); + if(!assoc_acc_frame) + { + assoc_acc_frame = kvp_frame_new(); + assoc_acc_frame_kvpvalue = kvp_value_new_frame(assoc_acc_frame); + kvp_frame_set_slot(account_frame, "associated-accounts", + assoc_acc_frame_kvpvalue); + } + + return assoc_acc_frame; +} + +static void +back_associate_expense_accounts(Account *stock_account, + GList *accounts, + GNCTrackingExpenseCategory category) +{ + kvp_frame *acc_frame; + kvp_value *val, *stock_acc_guid_kvpval, *stock_acc_category_kvpval; + const GUID *stock_acc_guid; + const GUID *existing_acc_guid; + + stock_acc_guid = xaccAccountGetGUID(stock_account); + stock_acc_guid_kvpval = kvp_value_new_guid(stock_acc_guid); + + stock_acc_category_kvpval = kvp_value_new_string(expense_to_key[category]); + + for(; accounts; accounts = g_list_next(accounts)) + { + acc_frame = xaccAccountGetSlots(accounts->data); + g_return_if_fail(val = kvp_frame_get_slot(acc_frame, + "associated-stock-account")); + g_return_if_fail(kvp_value_get_type(val) == KVP_TYPE_GUID); + existing_acc_guid = kvp_value_get_guid(val); + + g_return_if_fail(xaccGUIDType(existing_acc_guid) == GNC_ID_NONE); + + kvp_frame_set_slot_nc(acc_frame, "associated-stock-account", + stock_acc_guid_kvpval); + + kvp_frame_set_slot_nc(acc_frame, "associated-stock-account-category", + stock_acc_category_kvpval); + } + + return; +} + +static void +back_associate_income_accounts(Account *stock_account, + GList *accounts, + GNCTrackingIncomeCategory category) +{ + kvp_frame *acc_frame; + kvp_value *val, *stock_acc_guid_kvpval, *stock_acc_category_kvpval; + const GUID *stock_acc_guid; + const GUID *existing_acc_guid; + + stock_acc_guid = xaccAccountGetGUID(stock_account); + stock_acc_guid_kvpval = kvp_value_new_guid(stock_acc_guid); + + stock_acc_category_kvpval = kvp_value_new_string(income_to_key[category]); + + for(; accounts; accounts = g_list_next(accounts)) + { + acc_frame = xaccAccountGetSlots(accounts->data); + g_return_if_fail(val = kvp_frame_get_slot(acc_frame, + "associated-stock-account")); + g_return_if_fail(kvp_value_get_type(val) == KVP_TYPE_GUID); + existing_acc_guid = kvp_value_get_guid(val); + + g_return_if_fail(xaccGUIDType(existing_acc_guid) == GNC_ID_NONE); + + kvp_frame_set_slot_nc(acc_frame, "associated-stock-account", + stock_acc_guid_kvpval); + kvp_frame_set_slot_nc(acc_frame, "associated-stock-account-category", + stock_acc_category_kvpval); + } + + return; +} + +static kvp_value * +make_kvpd_on_list(GList *account_list) +{ + GList *iter; + kvp_value *retval; + kvp_value *guid_kvp; + GList *kvp_acc_list = NULL; + const GUID *acc_id; + + for(iter = account_list; iter; iter = g_list_next(iter)) + { + GNCAccountType type; + Account *current_account; + + current_account = iter->data; + type = xaccAccountGetType(current_account); + g_return_val_if_fail(type == INCOME || type == EXPENSE, NULL); + + acc_id = xaccAccountGetGUID(current_account); + guid_kvp = kvp_value_new_guid(acc_id); + kvp_acc_list = g_list_prepend(kvp_acc_list, guid_kvp); + } + + kvp_acc_list = g_list_reverse(kvp_acc_list); + + retval = kvp_value_new_glist_nc(kvp_acc_list); + return retval; +} + +static GList * +de_kvp_account_list(kvp_value *kvpd_list) +{ + GList *guid_account_list = kvp_value_get_glist(kvpd_list); + if (guid_account_list) + { + GList *expense_acc_list= NULL; + for(; guid_account_list; guid_account_list=g_list_next(guid_account_list)) + { + g_list_prepend(expense_acc_list, + xaccAccountLookup(guid_account_list->data)); + } + + expense_acc_list = g_list_reverse(expense_acc_list); + return expense_acc_list; + } + else + { + return NULL; + } +} + +/*********************************************************************\ + * gnc_tracking_associate_income_accounts * + * associate a list of income accounts with a stock account * + * * + * NOTE: Please disassociate all the accounts in account_list * + * using gnc_tracking_dissociate_accounts if necessary, BEFORE * + * calling this function * + * * + * Args: stock_account - the stock account * + * category - the type of association * + * account_list - a GList of Account *'s of the accounts * + * to associate with the stock account * + * * + * Returns : void * +\*********************************************************************/ + +void +gnc_tracking_associate_income_accounts(Account *stock_account, + GNCTrackingIncomeCategory category, + GList *account_list) +{ + kvp_frame *account_frame, *inc_account_frame; + kvp_value *kvpd_on_account_list; + GNCAccountType type; + + g_return_if_fail(stock_account); + type = xaccAccountGetType(stock_account); + g_return_if_fail(type == STOCK || type == MUTUAL); + account_frame = xaccAccountGetSlots(stock_account); + g_return_if_fail(account_frame); + g_return_if_fail(category >= 0); + g_return_if_fail(category < GNC_TR_INC_N_CATEGORIES); + + inc_account_frame = get_assoc_acc_frame(account_frame); + kvpd_on_account_list = make_kvpd_on_list(account_list); + + back_associate_income_accounts(stock_account, account_list, category); + + kvp_frame_set_slot_nc(inc_account_frame, + income_to_key[category], + kvpd_on_account_list); +} + +/*********************************************************************\ + * gnc_tracking_associate_expense_accounts * + * associate a list of expense accounts with a stock account * + * * + * NOTE: Please disassociate all the accounts in account_list * + * using gnc_tracking_dissociate_accounts if necessary, BEFORE * + * calling this function * + * * + * Args: stock_account - the stock account * + * category - the type of association * + * account_list - a GList of Account *'s of the accounts * + * to associate with the stock account * + * * + * Returns : void * +\*********************************************************************/ +void +gnc_tracking_asssociate_expense_account(Account *stock_account, + GNCTrackingExpenseCategory category, + GList *account_list) +{ + kvp_frame *account_frame, *expense_acc_frame; + kvp_value *kvpd_on_account_list; + GNCAccountType type; + + g_return_if_fail(stock_account); + type = xaccAccountGetType(stock_account); + g_return_if_fail(type == STOCK || type == MUTUAL); + account_frame = xaccAccountGetSlots(stock_account); + g_return_if_fail(account_frame); + g_return_if_fail(category >= 0); + g_return_if_fail(category < GNC_TR_EXP_N_CATEGORIES); + + expense_acc_frame = get_assoc_acc_frame(account_frame); + kvpd_on_account_list = make_kvpd_on_list(account_list); + + back_associate_expense_accounts(stock_account, account_list, category); + + kvp_frame_set_slot_nc(expense_acc_frame, + expense_to_key[category], + kvpd_on_account_list); +} + +/*********************************************************************\ + * gnc_tracking_find_expense_accounts * + * find out which accounts are associated with a particular * + * account in a particular way * + * * + * * + * Args: stock_account - the stock account * + * category - the type of association * + * * + * Returns : A GList of Account *'s listing the accounts * +\*********************************************************************/ + +GList * +gnc_tracking_find_expense_accounts(Account *stock_account, + GNCTrackingExpenseCategory category) +{ + + GNCAccountType type; + kvp_frame *account_frame, *expense_acc_frame; + kvp_value *kvpd_on_account_list; + + type = xaccAccountGetType(stock_account); + g_return_val_if_fail(category >= 0 && category < GNC_TR_EXP_N_CATEGORIES, + NULL); + g_return_val_if_fail(type == STOCK || type == MUTUAL, NULL); + + account_frame = xaccAccountGetSlots(stock_account); + g_return_val_if_fail(account_frame, NULL); + + expense_acc_frame = get_assoc_acc_frame(account_frame); + kvpd_on_account_list = kvp_frame_get_slot(account_frame, + expense_to_key[category]); + + return de_kvp_account_list(kvpd_on_account_list); +} + +/*********************************************************************\ + * gnc_tracking_find_income_accounts * + * find out which accounts are associated with a particular * + * account in a particular way * + * * + * * + * Args: stock_account - the stock account * + * category - the type of association * + * * + * Returns : A GList of Account *'s listing the accounts * +\*********************************************************************/ +GList * +gnc_tracking_find_income_accounts(Account *stock_account, + GNCTrackingIncomeCategory category) +{ + GNCAccountType type; + kvp_frame *account_frame, *income_acc_frame; + kvp_value *kvpd_on_account_list; + + type = xaccAccountGetType(stock_account); + g_return_val_if_fail(category >= 0 && category < GNC_TR_INC_N_CATEGORIES, + NULL); + g_return_val_if_fail(type == STOCK || type == MUTUAL, NULL); + + account_frame = xaccAccountGetSlots(stock_account); + g_return_val_if_fail(account_frame, NULL); + + income_acc_frame = get_assoc_acc_frame(account_frame); + kvpd_on_account_list = kvp_frame_get_slot(income_acc_frame, + income_to_key[category]); + + return de_kvp_account_list(kvpd_on_account_list); +} + +/*********************************************************************\ + * gnc_tracking_find_all_expense_accounts * + * find all expense accounts associated with a stock account * + * * + * Args: stock_account - the stock account * + * * + * Returns : A GList of Account *'s listing the accounts * +\*********************************************************************/ + +GList * +gnc_tracking_find_all_expense_accounts(Account *stock_account) +{ + GList *complete_list=NULL; + int i; + + for(i = 0; i < GNC_TR_EXP_N_CATEGORIES; i++) + { + complete_list = + g_list_concat(complete_list, + gnc_tracking_find_expense_accounts(stock_account, i)); + } + + return complete_list; +} + +/*********************************************************************\ + * gnc_tracking_find_all_income_accounts * + * find all income accounts associated with a stock account * + * * + * Args: stock_account - the stock account * + * * + * Returns : A GList of Account *'s listing the accounts * +\*********************************************************************/ + +GList * +gnc_tracking_find_all_income_accounts(Account *stock_account) +{ + GList *complete_list= NULL; + int i; + + for(i = 0; i < GNC_TR_EXP_N_CATEGORIES; i++) + { + g_list_concat(complete_list, + gnc_tracking_find_expense_accounts(stock_account, + i)); + } + return complete_list; +} + +/*********************************************************************\ + * gnc_tracking_find_stock_account * + * find the stock account associated with this expense/income * + * account. If there is no association, return NULL * + * * + * Args: inc_or_expense_acc - the expense/income account * + * * + * * + * Returns : The associated stock account * +\*********************************************************************/ + +Account * +gnc_tracking_find_stock_account(Account *inc_or_expense_acc) +{ + return NULL; +} + +/*********************************************************************\ + * gnc_tracking_dissociate_account * + * remove any association between this income/expense account * + * and any stock account it is presently associated with * + * account. * + * * + * Args: inc_or_expense_acc - the expense/income account * + * * + * * + * Returns : void * +\*********************************************************************/ + +void +gnc_tracking_dissociate_account(Account *inc_or_expense_account) +{ + GNCAccountType type; + kvp_frame *stock_account_kvpframe, *assoc_acc_kvpframe; + kvp_frame *current_account_kvpframe; + kvp_value *stock_account_kvpval, *acc_list_kvpval, *category_kvpval; + const GUID *stock_account_guid, *inc_or_expense_account_guid, *current_guid; + Account *stock_account; + char *category_name; + GList *assoc_acc_list, *assoc_acc_list_start; + + type = xaccAccountGetType(inc_or_expense_account); + + g_return_if_fail(type == INCOME || type == EXPENSE); + + current_account_kvpframe = xaccAccountGetSlots(inc_or_expense_account); + + stock_account_kvpval = kvp_frame_get_slot(current_account_kvpframe, + "associated-stock-account"); + + stock_account_guid = kvp_value_get_guid(stock_account_kvpval); + if(xaccGUIDType(stock_account_guid) == GNC_ID_NULL) + return; + + category_kvpval = kvp_frame_get_slot(current_account_kvpframe, + "associated-stock-account-category"); + category_name = kvp_value_get_string(category_kvpval); + + + inc_or_expense_account_guid = xaccAccountGetGUID(inc_or_expense_account); + stock_account = xaccAccountLookup(stock_account_guid); + + stock_account_kvpframe = xaccAccountGetSlots(stock_account); + + g_return_if_fail(stock_account_kvpval = + kvp_frame_get_slot(stock_account_kvpframe, + "associated-accounts")); + + assoc_acc_kvpframe = kvp_value_get_frame(stock_account_kvpval); + + g_return_if_fail(acc_list_kvpval = kvp_frame_get_slot(assoc_acc_kvpframe, + category_name)); + g_return_if_fail(assoc_acc_list_start = + kvp_value_get_glist(acc_list_kvpval)); + + for(assoc_acc_list = assoc_acc_list_start; + assoc_acc_list; + assoc_acc_list = g_list_next(assoc_acc_list)) + { + g_return_if_fail(current_guid = kvp_value_get_guid(assoc_acc_list->data)); + if(guid_equal(current_guid, inc_or_expense_account_guid)) + { + assoc_acc_list_start = + g_list_remove_link(assoc_acc_list_start, assoc_acc_list); + g_list_free_1(assoc_acc_list); + acc_list_kvpval = kvp_value_new_glist_nc(assoc_acc_list); + kvp_frame_set_slot_nc(assoc_acc_kvpframe, + category_name, + acc_list_kvpval); + return; + } + } + + /* should never happen */ + PERR("Income/Expense account and stock account disagree on association"); +} + +/* ========================== END OF FILE ===================== */ diff --git a/src/engine/gnc-associate-account.h b/src/engine/gnc-associate-account.h new file mode 100644 index 0000000000..09335475dc --- /dev/null +++ b/src/engine/gnc-associate-account.h @@ -0,0 +1,89 @@ +/********************************************************************\ + * gnc-associate-account.h : api for associating income and * + * expense accounts with stock/mutual fund accounts, for tracking * + * dividends, brokerage, and other stock-related expenses and * + * income so that they can be reported * + * Copyright 2000 Gnumatic Incorporated * + * Written by Robert Merkel * + * + * WARNING WARNING WARNING: THIS CODE IS TOTALLY UNTESTED. * + * THE ONLY REASON IT'S IN CVS IS FOR SAFEKEEPING * + * * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * +\********************************************************************/ + +#include +#include + +/* + * account_list is a list of account *'s, all of which much be expense + * accounts + */ +typedef enum {GNC_TR_INC_MISC, + GNC_TR_INC_INTEREST, + GNC_TR_INC__DIVIDEND, + GNC_TR_INC_LT_CG, + GNC_TR_INC_ST_CG, + GNC_TR_INC_N_CATEGORIES} GNCTrackingIncomeCategory; + +typedef enum {GNC_TR_EXP_MISC, + GNC_TR_EXP_COMMISSION, + GNC_TR_EXP_N_CATEGORIES} GNCTrackingExpenseCategory; + + +/* + * account_list is a list of account *'s, all of which much be expense + * accounts. You can clear associations by setting account_list to NULL + */ + +void gnc_tracking_associate_income_accounts(Account *stock_account, + GNCTrackingIncomeCategory category, + GList *account_list); + + +void gnc_tracking_asssociate_expense_account(Account *stock_account, + GNCTrackingExpenseCategory category, + GList *account_list); + +/* + * returns a list of account *'s, + * returns null if no association specified + */ + +GList *gnc_tracking_find_expense_accounts(Account *stock_account, + GNCTrackingExpenseCategory category); + +GList *gnc_tracking_find_income_accounts(Account *stock_account, + GNCTrackingIncomeCategory category); + +/* for ROI purposes we don't care about categories, these are "grab +all" for that purpose */ + +GList *gnc_tracking_find_all_expense_accounts(Account *stock_account); + +GList *gnc_tracking_find_all_income_accounts(Account *stock_account); + + +/* + * reverse lookup - obviously returns a stock account (or NULL if none + * associated), and argument must be an income or expense account + */ +Account *gnc_tracking_find_stock_account(Account *inc_or_expense_acc); + +void gnc_tracking_dissociate_account(Account *inc_or_expense_account); diff --git a/src/engine/gnc-book-p.h b/src/engine/gnc-book-p.h new file mode 100644 index 0000000000..691df316d0 --- /dev/null +++ b/src/engine/gnc-book-p.h @@ -0,0 +1,102 @@ +/********************************************************************\ + * gnc-book-p.h -- private functions for gnc books. * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * HISTORY: + * Created 2001 by Rob Browning + * Copyright (c) 2001 Rob Browning + */ + +#ifndef GNC_BOOK_P_H +#define GNC_BOOK_P_H + +#include "Group.h" +#include "Backend.h" +#include "BackendP.h" +#include "gnc-pricedb.h" +#include "TransLog.h" +#include "gnc-engine-util.h" +#include "gnc-pricedb-p.h" +#include "DateUtils.h" + +#include "gnc-engine.h" +#include "gnc-engine-util.h" +#include "gnc-book.h" +#include "gnc-pricedb.h" +#include "Group.h" + +#define GNC_BACKEND_INTERFACE 0 + +struct gnc_book_struct +{ + AccountGroup *topgroup; + GNCPriceDB *pricedb; + GList *sched_xactions; + AccountGroup *template_group; + + /* + * should be set true if sched_xactions is changed + * before saving + */ + + gboolean sx_notsaved; + + /* the requested book id, in the form or a URI, such as + * file:/some/where, or sql:server.host.com:555 + */ + char *book_id; + + /* if any book subroutine failed, this records the failure reason + * (file not found, etc). + * This is a 'stack' that is one deep. + * FIXME: This is a hack. I'm trying to move us away from static + * global vars. This may be a temp fix if we decide to integrate + * FileIO errors into GNCBook errors. + */ + GNCBackendError last_err; + char *error_message; + + char *fullpath; + + /* ---------------------------------------------------- */ + /* This struct member applies for network, rpc and SQL i/o */ + /* It is not currently used for file i/o, but it should be. */ + Backend *backend; +}; + + +void gnc_book_set_group(GNCBook *book, AccountGroup *grp); +void gnc_book_set_pricedb(GNCBook *book, GNCPriceDB *db); + +/* + * used by backends to mark the notsaved as FALSE just after + * loading. Do not use otherwise! + */ + + +void gnc_book_mark_saved(GNCBook *book); + +void gnc_book_push_error (GNCBook *book, GNCBackendError err, char *message); + +Backend* gncBackendInit_file(const char *book_id, void *data); + +#endif /* GNC_BOOK_P_H */ diff --git a/src/engine/gnc-book.c b/src/engine/gnc-book.c new file mode 100644 index 0000000000..f219450cd4 --- /dev/null +++ b/src/engine/gnc-book.c @@ -0,0 +1,1026 @@ +/********************************************************************\ + * gnc-book.c -- dataset access (set of accounting books) * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * +\********************************************************************/ + +/* + * FILE: + * gnc-book.c + * + * FUNCTION: + * Encapsulate all the information about a gnucash dataset, including + * the methods used to read and write them to datastores. + * + * HISTORY: + * Created by Linas Vepstas December 1998 + * Copyright (c) 1998-2001 Linas Vepstas + * Copyright (c) 2000 Dave Peticolas + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "Backend.h" +#include "BackendP.h" +#include "Group.h" +#include "SchedXaction.h" +#include "TransLog.h" +#include "gnc-engine-util.h" +#include "gnc-pricedb-p.h" +#include "DateUtils.h" +#include "gnc-book.h" +#include "gnc-book-p.h" +#include "gnc-engine.h" +#include "gnc-engine-util.h" +#include "gnc-module.h" + +static short module = MOD_IO; + +/* ---------------------------------------------------------------------- */ + +static void +gnc_book_clear_error (GNCBook *book) +{ + book->last_err = ERR_BACKEND_NO_ERR; + if(book->error_message) + { + g_free(book->error_message); + book->error_message = NULL; + } +} + +void +gnc_book_push_error (GNCBook *book, GNCBackendError err, char *message) +{ + book->last_err = err; + book->error_message = message; +} + +GNCBackendError +gnc_book_get_error (GNCBook * book) +{ + if (!book) return ERR_BACKEND_NO_BACKEND; + return book->last_err; +} + +static const char * +get_default_error_message(GNCBackendError err) +{ + return ""; +} + +const char * +gnc_book_get_error_message(GNCBook *book) +{ + if(!book) return ""; + if(!book->error_message) return get_default_error_message(book->last_err); + return book->error_message; +} + +GNCBackendError +gnc_book_pop_error (GNCBook * book) +{ + GNCBackendError err; + if (!book) return ERR_BACKEND_NO_BACKEND; + err = book->last_err; + gnc_book_clear_error(book); + return err; +} + +/* ---------------------------------------------------------------------- */ + +const char *TEMPLATE_ACCOUNT_NAME = "__account for template transactions__"; + +static void +gnc_book_init (GNCBook *book) +{ + Account *template_acct; + + if(!book) return; + + book->topgroup = xaccMallocAccountGroup(); + book->pricedb = gnc_pricedb_create(); + + book->sched_xactions = NULL; + book->sx_notsaved = FALSE; + book->template_group = xaccMallocAccountGroup(); + + book->book_id = NULL; + gnc_book_clear_error (book); + book->fullpath = NULL; + book->backend = NULL; +} + +GNCBook * +gnc_book_new (void) +{ + GNCBook *book = g_new0(GNCBook, 1); + gnc_book_init(book); + return book; +} + +/* ---------------------------------------------------------------------- */ + +gnc_commodity_table* +gnc_book_get_commodity_table(GNCBook *book) +{ + return gnc_engine_commodities(); +} + +AccountGroup * +gnc_book_get_group (GNCBook *book) +{ + if (!book) return NULL; + return book->topgroup; +} + +void +gnc_book_set_group (GNCBook *book, AccountGroup *grp) +{ + if(!book) return; + + /* Do not free the old topgroup here unless you also fix + * all the other uses of gnc_book_set_group! */ + + book->topgroup = grp; +} + +/* ---------------------------------------------------------------------- */ + +static int +counter_thunk(Transaction *t, void *data) +{ + (*((guint*)data))++; + return 0; +} + +guint +gnc_book_count_transactions(GNCBook *book) +{ + guint count = 0; + xaccGroupForEachTransaction(gnc_book_get_group(book), + counter_thunk, (void*)&count); + return count; +} + +/* ---------------------------------------------------------------------- */ + +GNCPriceDB * +gnc_book_get_pricedb(GNCBook *book) +{ + if(!book) return NULL; + return book->pricedb; +} + +void +gnc_book_set_pricedb(GNCBook *book, GNCPriceDB *db) +{ + if(!book) return; + book->pricedb = db; +} + +/* ---------------------------------------------------------------------- */ + +GList * +gnc_book_get_schedxactions( GNCBook *book ) +{ + if ( book == NULL ) return NULL; + return book->sched_xactions; +} + +void +gnc_book_set_schedxactions( GNCBook *book, GList *newList ) +{ + if ( book == NULL ) return; + book->sched_xactions = newList; + book->sx_notsaved = TRUE; + return; +} + +AccountGroup * +gnc_book_get_template_group( GNCBook *book ) +{ + if ( book == NULL ) return NULL; + return book->template_group; +} + +void +gnc_book_set_template_group( GNCBook *book, AccountGroup *templateGroup ) +{ + if ( book == NULL ) return; + book->template_group = templateGroup; +} + +/* ---------------------------------------------------------------------- */ + +Backend * +xaccGNCBookGetBackend (GNCBook *book) +{ + if (!book) return NULL; + return book->backend; +} + +/* ---------------------------------------------------------------------- */ + +const char * +gnc_book_get_file_path (GNCBook *book) +{ + if (!book) return NULL; + return book->fullpath; +} + +/* ---------------------------------------------------------------------- */ + +const char * +gnc_book_get_url (GNCBook *book) +{ + if (!book) return NULL; + return book->book_id; +} + +/* ---------------------------------------------------------------------- */ + +static void +mark_sx_clean(gpointer data, gpointer user_data) +{ + SchedXaction *sx = (SchedXaction *) data; + xaccSchedXactionSetDirtyness(sx, FALSE); + return; +} + +static void +book_sxns_mark_saved(GNCBook *book) +{ + book->sx_notsaved = FALSE; + g_list_foreach(gnc_book_get_schedxactions(book), + mark_sx_clean, + NULL); + return; +} +void +gnc_book_mark_saved(GNCBook *book) +{ + /* FIXME: is this the right behaviour if book == NULL? */ + g_return_if_fail(book); + xaccGroupMarkSaved(gnc_book_get_group(book)); + gnc_pricedb_mark_clean(gnc_book_get_pricedb(book)); + + xaccGroupMarkSaved(gnc_book_get_template_group(book)); + book_sxns_mark_saved(book); + + return; +} + + +/* ---------------------------------------------------------------------- */ + +static void +gnc_book_int_backend_load_error(GNCBook *book, char *message, char *dll_err) +{ + PWARN (message, dll_err ? dll_err : ""); + g_free(book->fullpath); + book->fullpath = NULL; + g_free(book->book_id); + book->book_id = NULL; + gnc_book_push_error (book, ERR_BACKEND_NO_BACKEND, NULL); +} + + +/* FIXME : reinstate better error messages with gnc_module errors */ +static void +gnc_book_load_backend(GNCBook * book, char * backend_name) +{ + GNCModule mod; + Backend *(* be_new_func)(void); + char * mod_name = g_strdup_printf("gnucash/backend/%s", backend_name); + + mod = gnc_module_load(mod_name, GNC_BACKEND_INTERFACE); + if(mod) + { + be_new_func = gnc_module_lookup(mod, "gnc_backend_new"); + if(be_new_func) + { + book->backend = be_new_func(); + } + else + { + gnc_book_int_backend_load_error(book, " can't find backend_new ", + ""); + } + } + else + { + gnc_book_int_backend_load_error(book, " failed to load '%s' backend", + backend_name); + } + g_free(mod_name); +} + +gboolean +gnc_book_begin (GNCBook *book, const char * book_id, + gboolean ignore_lock, gboolean create_if_nonexistent) +{ + int rc; + + if (!book) return FALSE; + ENTER (" ignore_lock=%d, book-id=%s", ignore_lock, + book_id ? book_id : "(null)"); + + /* clear the error condition of previous errors */ + gnc_book_clear_error (book); + + /* check to see if this session is already open */ + if (gnc_book_get_url(book)) + { + gnc_book_push_error (book, ERR_BACKEND_LOCKED, NULL); + LEAVE("bad book url"); + return FALSE; + } + + /* seriously invalid */ + if (!book_id) + { + gnc_book_push_error (book, ERR_BACKEND_NO_BACKEND, NULL); + LEAVE("bad book_id"); + return FALSE; + } + /* Store the sessionid URL */ + book->book_id = g_strdup (book_id); + + book->fullpath = xaccResolveFilePath(book_id); + if (!book->fullpath) + { + gnc_book_push_error (book, ERR_FILEIO_FILE_NOT_FOUND, NULL); + LEAVE("bad fullpath"); + return FALSE; /* ouch */ + } + PINFO ("filepath=%s", book->fullpath ? book->fullpath : "(null)"); + + /* check to see if this is a type we know how to handle */ + if (!g_strncasecmp(book_id, "file:", 5) || + *book_id == '/') + { + gnc_book_load_backend(book, "file" ); + } +#if 0 + /* load different backend based on URL. We should probably + * dynamically load these based on some config file ... */ + else if ((!g_strncasecmp(book_id, "http://", 7)) || + (!g_strncasecmp(book_id, "https://", 8))) + { + /* create the backend */ + book->backend = xmlendNew(); + } +#endif + else if (!g_strncasecmp(book_id, "postgres://", 11)) + { + gnc_book_load_backend(book, "postgres"); + } + else if (!g_strncasecmp(book_id, "rpc://", 6)) + { + gnc_book_load_backend(book, "rpc"); + } + + /* if there's a begin method, call that. */ + if (book->backend && book->backend->book_begin) + { + int err; + (book->backend->book_begin)(book->backend, book, + gnc_book_get_url(book), ignore_lock, + create_if_nonexistent); + PINFO("Run book_begin on backend"); + err = xaccBackendGetError(book->backend); + if (err != ERR_BACKEND_NO_ERR) + { + g_free(book->fullpath); + book->fullpath = NULL; + g_free(book->book_id); + book->book_id = NULL; + gnc_book_push_error (book, err, NULL); + LEAVE("backend error"); + return FALSE; + } + } + LEAVE(" "); + return TRUE; +} + +/* ---------------------------------------------------------------------- */ + +gboolean +gnc_book_load (GNCBook *book) +{ + GNCBackendError backend_err; + Backend *be; + + if (!book) return FALSE; + if (!gnc_book_get_url(book)) return FALSE; + + ENTER ("book_id=%s", gnc_book_get_url(book) + ? gnc_book_get_url(book) : "(null)"); + + /* At this point, we should are supposed to have a valid book + * id and a lock on the file. */ + + xaccLogDisable(); + xaccGroupMarkDoFree (book->topgroup); + xaccFreeAccountGroup (book->topgroup); + book->topgroup = NULL; + gnc_pricedb_destroy(book->pricedb); + book->pricedb = NULL; + + xaccLogSetBaseName(book->fullpath); + xaccLogEnable(); + + gnc_book_clear_error (book); + + /* This code should be sufficient to initialize *any* backend, + * whether http, postgres, or anything else that might come along. + * Basically, the idea is that by now, a backend has already been + * created & set up. At this point, we only need to get the + * top-level account group out of the backend, and that is a + * generic, backend-independent operation. + */ + be = book->backend; + + /* Starting the session should result in a bunch of accounts + * and currencies being downloaded, but probably no transactions; + * The GUI will need to do a query for that. + */ + if (be) + { + xaccLogDisable(); + if(be->book_load) + { + xaccLogSetBaseName(book->fullpath); + + book->topgroup = (be->book_load) (be); + xaccGroupSetBackend (book->topgroup, be); + gnc_book_push_error(book, xaccBackendGetError(be), NULL); + } + + if (be->price_load) + { + book->pricedb = (be->price_load) (be); + + /* we just got done loading, it can't possibly be dirty !! */ + gnc_book_mark_saved(book); + + xaccPriceDBSetBackend (book->pricedb, be); + gnc_book_push_error(book, xaccBackendGetError(be), NULL); + } + xaccLogEnable(); + } + + if (!book->topgroup) + { + LEAVE("topgroup NULL"); + return FALSE; + } + + if (!book->pricedb) + { + LEAVE("pricedb NULL"); + return FALSE; + } + + if (gnc_book_get_error(book) != ERR_BACKEND_NO_ERR) + { + LEAVE("error from backend %d", gnc_book_get_error(book)); + return FALSE; + } + + LEAVE("book_id=%s", gnc_book_get_url(book) + ? gnc_book_get_url(book) : "(null)"); + return TRUE; +} + + + +static gboolean +book_sxlist_notsaved(GNCBook *book) +{ + GList *sxlist; + SchedXaction *sx; + if(book->sx_notsaved + || + xaccGroupNotSaved(book->template_group)) return TRUE; + + for(sxlist = book->sched_xactions; + sxlist != NULL; + sxlist = g_list_next(sxlist)) + { + sx = (SchedXaction *) (sxlist->data); + if (xaccSchedXactionIsDirty( sx )) + return TRUE; + } + + return FALSE; +} + +/* ---------------------------------------------------------------------- */ + +gboolean +gnc_book_not_saved(GNCBook *book) +{ + if(!book) return FALSE; + + return(xaccGroupNotSaved(book->topgroup) + || + gnc_pricedb_dirty(book->pricedb) + || + book_sxlist_notsaved(book)); +} + +/* ---------------------------------------------------------------------- */ + +gboolean +gnc_book_save_may_clobber_data (GNCBook *book) +{ + /* FIXME: Make sure this doesn't need more sophisticated semantics + * in the face of special file, devices, pipes, symlinks, etc. */ + + struct stat statbuf; + + if (!book) return FALSE; + if (!book->fullpath) return FALSE; + if (stat(book->fullpath, &statbuf) == 0) return TRUE; + + return FALSE; +} + +/* ---------------------------------------------------------------------- */ + +static gboolean +save_error_handler(Backend *be, GNCBook *book) +{ + int err; + err = xaccBackendGetError(be); + + if (ERR_BACKEND_NO_ERR != err) + { + gnc_book_push_error (book, err, NULL); + + /* we close the backend here ... isn't this a bit harsh ??? */ + if (be->book_end) + { + (be->book_end)(be); + } + return TRUE; + } + return FALSE; +} + +void +gnc_book_save (GNCBook *book) +{ + Backend *be; + + if (!book) return; + + ENTER ("book_id=%s", gnc_book_get_url(book) + ? gnc_book_get_url(book) : "(null)"); + + /* if there is a backend, and the backend is reachablele + * (i.e. we can communicate with it), then synchronize with + * the backend. If we cannot contact the backend (e.g. + * because we've gone offline, the network has crashed, etc.) + * then give the user the option to save to disk. + */ + be = book->backend; + if (be) { + + /* if invoked as SaveAs(), then backend not yet set */ + xaccGroupSetBackend (book->topgroup, be); + xaccPriceDBSetBackend (book->pricedb, be); + + if(be->all_sync) + { + (be->all_sync)(be, book->topgroup, book->pricedb); + if(save_error_handler(be, book)) + return; + } + else + { + if (be->sync && book->topgroup) { + (be->sync)(be, book->topgroup); + if(save_error_handler(be, book)) + return; + } + + if (be->sync_price && book->pricedb) { + (be->sync_price)(be, book->pricedb); + if(save_error_handler(be, book)) + return; + } + } + return; + } + + /* if the fullpath doesn't exist, either the user failed to initialize, + * or the lockfile was never obtained. Either way, we can't write. */ + gnc_book_clear_error (book); + + if (!book->fullpath) + { + gnc_book_push_error (book, ERR_BACKEND_MISC, NULL); + return; + } + + LEAVE(" "); +} + +/* ---------------------------------------------------------------------- */ + +void +gnc_book_end (GNCBook *book) +{ + if (!book) return; + + ENTER ("book_id=%s", gnc_book_get_url(book) + ? gnc_book_get_url(book) : "(null)"); + + /* close down the backend first */ + if (book->backend && book->backend->book_end) + { + (book->backend->book_end)(book->backend); + } + + gnc_book_clear_error (book); + + LEAVE(" "); +} + +void +gnc_book_destroy (GNCBook *book) +{ + if (!book) return; + + ENTER ("book_id=%s", gnc_book_get_url(book) + ? gnc_book_get_url(book) : "(null)"); + + xaccLogDisable(); + gnc_book_end (book); + + /* destroy the backend */ + if (book->backend && book->backend->destroy_backend) + { + book->backend->destroy_backend(book->backend); + } + else + { + g_free(book->backend); + } + + xaccGroupSetBackend (book->topgroup, NULL); + xaccPriceDBSetBackend (book->pricedb, NULL); + + /* mark the accounts as being freed + * to avoid tons of balance recomputations. */ + xaccGroupMarkDoFree (book->topgroup); + + xaccFreeAccountGroup (book->topgroup); + book->topgroup = NULL; + + gnc_pricedb_destroy (book->pricedb); + book->pricedb = NULL; + + xaccLogEnable(); + + g_free (book); + LEAVE(" "); +} + +gboolean +gnc_book_events_pending (GNCBook *book) +{ + if (!book) return FALSE; + if (!book->backend) return FALSE; + if (!book->backend->events_pending) return FALSE; + + return book->backend->events_pending (book->backend); +} + +gboolean +gnc_book_process_events (GNCBook *book) +{ + if (!book) return FALSE; + if (!book->backend) return FALSE; + if (!book->backend->process_events) return FALSE; + + return book->backend->process_events (book->backend); +} + +/* ---------------------------------------------------------------------- */ +/* + * If $HOME/.gnucash/data directory doesn't exist, then create it. + */ + +static void +MakeHomeDir (void) +{ + int rc; + struct stat statbuf; + char *home; + char *path; + char *data; + + /* Punt. Can't figure out where home is. */ + home = getenv ("HOME"); + if (!home) return; + + path = g_strconcat(home, "/.gnucash", NULL); + + rc = stat (path, &statbuf); + if (rc) + { + /* assume that the stat failed only because the dir is absent, + * and not because its read-protected or other error. + * Go ahead and make it. Don't bother much with checking mkdir + * for errors; seems pointless. */ + mkdir (path, S_IRWXU); /* perms = S_IRWXU = 0700 */ + } + + data = g_strconcat (path, "/data", NULL); + rc = stat (data, &statbuf); + if (rc) + mkdir (data, S_IRWXU); + + g_free (path); + g_free (data); +} + +/* ---------------------------------------------------------------------- */ +/* XXX hack alert -- we should be yanking this out of some config file */ +static char * searchpaths[] = +{ + "/usr/share/gnucash/data/", + "/usr/local/share/gnucash/data/", + "/usr/share/gnucash/accounts/", + "/usr/local/share/gnucash/accounts/", + NULL, +}; + +typedef gboolean (*pathGenerator)(char *pathbuf, int which); + +static gboolean +xaccAddEndPath(char *pathbuf, const char *ending, int len) +{ + if(len + strlen(pathbuf) >= PATH_MAX) + return FALSE; + + strcat (pathbuf, ending); + return TRUE; +} + +static gboolean +xaccCmdPathGenerator(char *pathbuf, int which) +{ + if(which != 0) + { + return FALSE; + } + else + { + /* try to find a file by this name in the cwd ... */ + if (getcwd (pathbuf, PATH_MAX) == NULL) + return FALSE; + + strcat (pathbuf, "/"); + return TRUE; + } +} + +static gboolean +xaccDataPathGenerator(char *pathbuf, int which) +{ + char *path; + + if(which != 0) + { + return FALSE; + } + else + { + path = getenv ("HOME"); + if (!path) + return FALSE; + + if (PATH_MAX <= (strlen (path) + 20)) + return FALSE; + + strcpy (pathbuf, path); + strcat (pathbuf, "/.gnucash/data/"); + return TRUE; + } +} + +static gboolean +xaccUserPathPathGenerator(char *pathbuf, int which) +{ + char *path = NULL; + + if(searchpaths[which] == NULL) + { + return FALSE; + } + else + { + path = searchpaths[which]; + + if (PATH_MAX <= strlen(path)) + return FALSE; + + strcpy (pathbuf, path); + return TRUE; + } +} + +char * +xaccResolveFilePath (const char * filefrag_tmp) +{ + struct stat statbuf; + char pathbuf[PATH_MAX]; + char *filefrag; + pathGenerator gens[4]; + int namelen; + int i; + + /* seriously invalid */ + if (!filefrag_tmp) + { + PERR("filefrag is NULL"); + return NULL; + } + + ENTER ("filefrag=%s", filefrag_tmp); + + /* ---------------------------------------------------- */ + /* OK, now we try to find or build an absolute file path */ + + /* check for an absolute file path */ + if (*filefrag_tmp == '/') + return g_strdup (filefrag_tmp); + + if (!g_strncasecmp(filefrag_tmp, "file:", 5)) + { + char *ret = g_new(char, strlen(filefrag_tmp) - 5 + 1); + strcpy(ret, filefrag_tmp + 5); + return ret; + } + + { + char *p; + + filefrag = g_strdup (filefrag_tmp); + p = strchr (filefrag, '/'); + while (p) { + *p = ','; + p = strchr (filefrag, '/'); + } + } + + /* get conservative on the length so that sprintf(getpid()) works ... */ + /* strlen ("/.LCK") + sprintf (%x%d) */ + namelen = strlen (filefrag) + 25; + + gens[0] = xaccCmdPathGenerator; + gens[1] = xaccDataPathGenerator; + gens[2] = xaccUserPathPathGenerator; + gens[3] = NULL; + + for (i = 0; gens[i] != NULL; i++) + { + int j; + for(j = 0; gens[i](pathbuf, j) ; j++) + { + if(xaccAddEndPath(pathbuf, filefrag, namelen)) + { + int rc = stat (pathbuf, &statbuf); + if ((!rc) && (S_ISREG(statbuf.st_mode))) + { + g_free(filefrag); + return (g_strdup (pathbuf)); + } + } + } + } + /* OK, we didn't find the file. */ + + /* make sure that the gnucash home dir exists. */ + MakeHomeDir(); + + /* If the user specified a simple filename (i.e. no slashes in it) + * then create the file. But if it has slashes in it, then creating + * a bunch of directories seems like a bad idea; more likely, the user + * specified a bad filename. So return with error. */ + if (strchr (filefrag, '/')) + return NULL; + + /* Lets try creating a new file in $HOME/.gnucash/data */ + if (xaccDataPathGenerator(pathbuf, 0)) + { + if(xaccAddEndPath(pathbuf, filefrag, namelen)) + return (g_strdup (pathbuf)); + } + + /* OK, we still didn't find the file */ + /* Lets try creating a new file in the cwd */ + if (xaccCmdPathGenerator(pathbuf, 0)) + { + if(xaccAddEndPath(pathbuf, filefrag, namelen)) + return (g_strdup (pathbuf)); + } + + return NULL; +} + +/* ---------------------------------------------------------------------- */ + +char * +xaccResolveURL (const char * pathfrag) +{ + /* seriously invalid */ + if (!pathfrag) return NULL; + + /* At this stage of checking, URL's are always, by definition, + * resolved. If there's an error connecting, we'll find out later. + * + * FIXME -- we should probably use ghttp_uri_validate + * to make sure hte uri is in good form... + */ + + if (!g_strncasecmp (pathfrag, "http://", 7) || + !g_strncasecmp (pathfrag, "https://", 8) || + !g_strncasecmp (pathfrag, "postgres://", 11) || + !g_strncasecmp (pathfrag, "rpc://", 6)) + { + return g_strdup(pathfrag); + } + + if (!g_strncasecmp (pathfrag, "file:", 5)) { + return (xaccResolveFilePath (pathfrag+5)); + } + + return (xaccResolveFilePath (pathfrag)); +} + +/* ---------------------------------------------------------------------- */ + +/* this should go in a separate binary to create a rpc server */ + +void +gnc_run_rpc_server (void) +{ + char * dll_err; + void * dll_handle; + int (*rpc_run)(short); + int ret; + + /* open and resolve all symbols now (we don't want mystery + * failure later) */ + dll_handle = dlopen ("libgnc_rpc.so", RTLD_NOW); + if (! dll_handle) + { + dll_err = dlerror(); + PWARN (" can't load library: %s\n", dll_err ? dll_err : ""); + return; + } + + rpc_run = dlsym (dll_handle, "rpc_server_run"); + dll_err = dlerror(); + if (dll_err) + { + dll_err = dlerror(); + PWARN (" can't find symbol: %s\n", dll_err ? dll_err : ""); + return; + } + + ret = (*rpc_run)(0); + + /* XXX How do we force an exit? */ +} diff --git a/src/engine/gnc-book.h b/src/engine/gnc-book.h new file mode 100644 index 0000000000..686c40ab52 --- /dev/null +++ b/src/engine/gnc-book.h @@ -0,0 +1,210 @@ +/********************************************************************\ + * gnc-book.h -- dataset access (set of accounting books) * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: + * gnc-book.h + * + * FUNCTION: + * Encapsulate all the information about a gnucash dataset, including + * the methods used to read and write them to datastores. + * + * This class provides several important services: + * + * 1) Prevents multiple users from editing the same file at the same + * time, thus avoiding lost data due to race conditions. Thus + * an open session implies that the associated file is locked. + * + * 2) Provides a search path for the file to be edited. This should + * simplify install & maintenance problems for naive users who + * may not have a good grasp on what a file system is, or where + * they want to keep their data files. + * + * The current implementations assumes the use of files and file + * locks; however, the API was designed to be general enough to + * allow the use of generic URL's, and/or implementation on top + * of SQL or other database/persistant object technology. + * + * HISTORY: + * Created by Linas Vepstas December 1998 + * Copyright (c) 1998, 1999 Linas Vepstas + * Copyright (c) 2000 Dave Peticolas + */ + +#ifndef GNC_BOOK_H +#define GNC_BOOK_H + +#include "gnc-pricedb.h" +#include "Backend.h" +#include "Group.h" + +/** TYPES **********************************************************/ + +struct gnc_book_struct; + +typedef struct gnc_book_struct GNCBook; + +/** PROTOTYPES ******************************************************/ + +GNCBook * gnc_book_new (void); +void gnc_book_destroy (GNCBook *book); + +/* The gnc_book_begin () method begins a new book. It takes as an argument + * the book id. The book id must be a string in the form of a URI/URL. + * In the current implementation, the following URL's are supported + * -- File URI of the form + * "file:/home/somewhere/somedir/file.xac" + * The path part must be a valid path. The file-part must be + * a valid old-style-xacc or new-style-gnucash-format file. Paths + * may be relative or absolute. If the path is relative; that is, + * if the argument is "file:somefile.xac" then a sequence of + * search paths are checked for a file of this name. + * + * -- Postgres URI of the form + * "postgres://hostname.com/dbname" + * See the sql subdirectory for more info. + * + * The 'ignore_lock' argument, if set to TRUE, will cause this routine + * to ignore any file locks that it finds. If set to FALSE, then + * file locks will be tested and obeyed. + * + * If the file exists, can be opened and read, and a lock can be obtained + * then a lock will be obtained and the function returns TRUE. + * + * If the file/database doesn't exist, and the create_if_nonexistent + * flag is set to TRUE, then the database is created. + * + * Otherwise the function returns FALSE. + */ +gboolean gnc_book_begin (GNCBook *book, const char * book_id, + gboolean ignore_lock, gboolean create_if_nonexistent); + + +/* The gnc_book_load() method loads the data associated with the book. + * The function returns TRUE on success. + */ +gboolean gnc_book_load (GNCBook *book); + +/* The gnc_book_get_error() routine can be used to obtain the reason + * for any failure. Calling this routine returns the current error. + * + * The gnc_book_pop_error() routine can be used to obtain the reason + * for any failure. Calling this routine resets the error value. + * + * This routine allows an implementation of multiple error values, + * e.g. in a stack, where this routine pops the top value. The current + * implementation has a stack that is one-deep. + * + * See Backend.h for a listing of returned errors. + */ +GNCBackendError gnc_book_get_error (GNCBook *book); +const char * gnc_book_get_error_message(GNCBook *book); +GNCBackendError gnc_book_pop_error (GNCBook *book); + + +AccountGroup *gnc_book_get_group (GNCBook *book); +void gnc_book_set_group(GNCBook *book, AccountGroup *group); +GNCPriceDB *gnc_book_get_pricedb (GNCBook *book); + +guint gnc_book_count_transactions(GNCBook *book); + +/* + * gnc_book_get_commodity_table returns the commodity table associated with + * the BOOK. At the moment this just returns the global commodity table, + * but if we get everything using this we can make it a non-global table :) + */ +gnc_commodity_table* gnc_book_get_commodity_table(GNCBook *book); + +/** + * Returns the list of scheduled transactions. + **/ +GList * gnc_book_get_schedxactions( GNCBook *book ); +void gnc_book_set_schedxactions( GNCBook *book, GList *newList ); + +AccountGroup *gnc_book_get_template_group( GNCBook *book ); +void gnc_book_set_template_group( GNCBook *book, AccountGroup *templateGroup ); + +/* The gnc_book_get_file_path() routine returns the fully-qualified file + * path for the book. That is, if a relative or partial filename + * was for the book, then it had to have been fully resolved to + * open the book. This routine returns the result of this resolution. + * The path is always guarenteed to reside in the local file system, + * even if the book itself was opened as a URL. (currently, the + * filepath is derived from the url by substituting commas for + * slashes). + * + * The gnc_book_get_url() routine returns the url that was opened. + * URL's for local files take the form of + * file:/some/where/some/file.gml + */ +const char * gnc_book_get_file_path (GNCBook *book); +const char * gnc_book_get_url (GNCBook *book); + +/* + * The gnc_book_not_saved() subroutine will return TRUE + * if any data in the book hasn't been saved to long-term storage. + */ +gboolean gnc_book_not_saved(GNCBook *book); + +/* FIXME: This isn't as thorough as we might want it to be... */ +gboolean gnc_book_save_may_clobber_data (GNCBook *book); + +/* The gnc_book_save() method will commit all changes that have been + * made to the book. In the current implementation, this is nothing + * more than a write to the file of the current AccountGroup of the + * book. + * + * The gnc_book_end() method will release the session lock. It will *not* + * save the account group to a file. Thus, this method acts as an "abort" + * or "rollback" primitive. + */ +void gnc_book_save (GNCBook *book); +void gnc_book_end (GNCBook *book); + +/* The gnc_book_events_pending() method will return TRUE if the backend + * has pending events which must be processed to bring the engine + * up to date with the backend. + * + * The gnc_book_process_events() method will process any events indicated + * by the gnc_book_events_pending() method. It returns TRUE if the + * engine was modified while engine events were suspended. + */ +gboolean gnc_book_events_pending (GNCBook *book); +gboolean gnc_book_process_events (GNCBook *book); + +/* The xaccResolveFilePath() routine is a utility that will accept + * a fragmentary filename as input, and resolve it into a fully + * qualified path in the file system, i.e. a path that begins with + * a leading slash. First, the current working directory is + * searched for the file. Next, the directory $HOME/.gnucash/data, + * and finally, a list of other (configurable) paths. If the file + * is not found, then the path $HOME/.gnucash/data is used. If + * $HOME is not defined, then the current working directory is + * used. + */ +char * xaccResolveFilePath (const char * filefrag); +char * xaccResolveURL (const char * pathfrag); + +/* Run the RPC Server */ +void gnc_run_rpc_server (void); + +#endif /* GNC_BOOK_H */ diff --git a/src/engine/gnc-commodity.c b/src/engine/gnc-commodity.c new file mode 100644 index 0000000000..b3ffedd2f1 --- /dev/null +++ b/src/engine/gnc-commodity.c @@ -0,0 +1,755 @@ +/******************************************************************** + * gnc-commodity.c -- api for tradable commodities (incl. currency) * + * Copyright (C) 2000 Bill Gribble * + * Copyright (C) 2001 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + *******************************************************************/ + +#define _GNU_SOURCE + +#include "config.h" + +#include +#include +#include +#include + +#include "gnc-commodity.h" +#include "gnc-engine-util.h" + + +/* parts per unit is nominal, i.e. number of 'partname' units in + * a 'unitname' unit. fraction is transactional, i.e. how many + * of the smallest-transactional-units of the currency are there + * in a 'unitname' unit. */ + +struct _gnc_commodity { + char * fullname; + char * namespace; + char * mnemonic; + char * printname; + char * exchange_code; /* CUSIP or other identifying code */ + int fraction; + char * unique_name; + gint16 mark; /* user-defined mark, handy for traversals */ +}; + +struct _gnc_commodity_namespace { + GHashTable * table; +}; + +struct _gnc_commodity_table { + GHashTable * table; +}; + +typedef struct _gnc_commodity_namespace gnc_commodity_namespace; + +/******************************************************************** + * gnc_commodity_new + ********************************************************************/ + +static void +reset_printname(gnc_commodity *com) +{ + g_free(com->printname); + com->printname = g_strdup_printf("%s (%s)", + com->mnemonic ? com->mnemonic : "", + com->fullname ? com->fullname : ""); +} + +static void +reset_unique_name(gnc_commodity *com) +{ + g_free(com->unique_name); + com->unique_name = g_strdup_printf("%s::%s", + com->namespace ? com->namespace : "", + com->mnemonic ? com->mnemonic : ""); +} + +gnc_commodity * +gnc_commodity_new(const char * fullname, + const char * namespace, const char * mnemonic, + const char * exchange_code, + int fraction) +{ + gnc_commodity * retval = g_new0(gnc_commodity, 1); + + retval->fullname = g_strdup(fullname); + retval->namespace = g_strdup(namespace); + retval->mnemonic = g_strdup(mnemonic); + retval->exchange_code = g_strdup(exchange_code); + retval->fraction = fraction; + retval->mark = 0; + + reset_printname(retval); + reset_unique_name(retval); + + return retval; +} + + +/******************************************************************** + * gnc_commodity_destroy + ********************************************************************/ + +void +gnc_commodity_destroy(gnc_commodity * cm) { + if(!cm) return; + g_free(cm->fullname); + g_free(cm->printname); + g_free(cm->namespace); + g_free(cm->exchange_code); + g_free(cm->mnemonic); + g_free(cm->unique_name); + cm->mark = 0; + g_free(cm); +} + + +/******************************************************************** + * gnc_commodity_get_mnemonic + ********************************************************************/ + +const char * +gnc_commodity_get_mnemonic(const gnc_commodity * cm) { + if(!cm) return NULL; + return cm->mnemonic; +} + +/******************************************************************** + * gnc_commodity_get_printname + ********************************************************************/ + +const char * +gnc_commodity_get_printname(const gnc_commodity * cm) { + if(!cm) return NULL; + return cm->printname; +} + + +/******************************************************************** + * gnc_commodity_get_namespace + ********************************************************************/ + +const char * +gnc_commodity_get_namespace(const gnc_commodity * cm) { + if(!cm) return NULL; + return cm->namespace; +} + + +/******************************************************************** + * gnc_commodity_get_fullname + ********************************************************************/ + +const char * +gnc_commodity_get_fullname(const gnc_commodity * cm) { + if(!cm) return NULL; + return cm->fullname; +} + + +/******************************************************************** + * gnc_commodity_get_unique_name + ********************************************************************/ + +const char * +gnc_commodity_get_unique_name(const gnc_commodity * cm) { + if(!cm) return NULL; + return cm->unique_name; +} + + +/******************************************************************** + * gnc_commodity_get_exchange_code + ********************************************************************/ + +const char * +gnc_commodity_get_exchange_code(const gnc_commodity * cm) { + if(!cm) return NULL; + return cm->exchange_code; +} + +/******************************************************************** + * gnc_commodity_get_fraction + ********************************************************************/ + +int +gnc_commodity_get_fraction(const gnc_commodity * cm) { + if(!cm) return 0; + return cm->fraction; +} + +/******************************************************************** + * gnc_commodity_get_mark + ********************************************************************/ + +gint16 +gnc_commodity_get_mark(const gnc_commodity * cm) { + if(!cm) return 0; + return cm->mark; +} + +/******************************************************************** + * gnc_commodity_set_mnemonic + ********************************************************************/ + +void +gnc_commodity_set_mnemonic(gnc_commodity * cm, const char * mnemonic) { + if(!cm) return; + if(cm->mnemonic == mnemonic) return; + + g_free(cm->mnemonic); + cm->mnemonic = g_strdup(mnemonic); + + reset_printname(cm); + reset_unique_name(cm); +} + +/******************************************************************** + * gnc_commodity_set_namespace + ********************************************************************/ + +void +gnc_commodity_set_namespace(gnc_commodity * cm, const char * namespace) { + if(!cm) return; + if(cm->namespace == namespace) return; + + g_free(cm->namespace); + cm->namespace = g_strdup(namespace); + + reset_printname(cm); + reset_unique_name(cm); +} + +/******************************************************************** + * gnc_commodity_set_fullname + ********************************************************************/ + +void +gnc_commodity_set_fullname(gnc_commodity * cm, const char * fullname) { + if(!cm) return; + if(cm->fullname == fullname) return; + + g_free(cm->fullname); + cm->fullname = g_strdup(fullname); + + reset_printname(cm); +} + +/******************************************************************** + * gnc_commodity_set_exchange_code + ********************************************************************/ + +void +gnc_commodity_set_exchange_code(gnc_commodity * cm, + const char * exchange_code) { + if(!cm) return; + if(cm->exchange_code == exchange_code) return; + + g_free(cm->exchange_code); + cm->exchange_code = g_strdup(exchange_code); +} + +/******************************************************************** + * gnc_commodity_set_fraction + ********************************************************************/ + +void +gnc_commodity_set_fraction(gnc_commodity * cm, int fraction) { + if(!cm) return; + cm->fraction = fraction; +} + +/******************************************************************** + * gnc_commodity_get_mark + ********************************************************************/ + +void +gnc_commodity_set_mark(gnc_commodity * cm, gint16 mark) { + if(!cm) return; + cm->mark = mark; +} + +/******************************************************************** + * gnc_commodity_equiv + * are two commodities the same? + ********************************************************************/ + +gboolean +gnc_commodity_equiv(const gnc_commodity * a, const gnc_commodity * b) { + if(a == b) return TRUE; + if(!a || !b) return FALSE; + if(safe_strcmp(a->namespace, b->namespace) != 0) return FALSE; + if(safe_strcmp(a->mnemonic, b->mnemonic) != 0) return FALSE; + return TRUE; +} + + +/******************************************************************** + * gnc_commodity_table_new + * make a new commodity table + ********************************************************************/ + +gnc_commodity_table * +gnc_commodity_table_new(void) { + gnc_commodity_table * retval = g_new0(gnc_commodity_table, 1); + retval->table = g_hash_table_new(&g_str_hash, &g_str_equal); + return retval; +} + +/******************************************************************** + * gnc_commodity_get_size + * get the size of the commodity table + ********************************************************************/ + +guint +gnc_commodity_table_get_number_of_namespaces(gnc_commodity_table* tbl) +{ + g_return_val_if_fail(tbl, 0); + g_return_val_if_fail(tbl->table, 0); + return g_hash_table_size(tbl->table); +} + +static void +count_coms(gpointer key, gpointer value, gpointer user_data) +{ + GHashTable *tbl = ((gnc_commodity_namespace*)value)->table; + guint *count = (guint*)user_data; + + if(safe_strcmp((char*)key, GNC_COMMODITY_NS_ISO) == 0) + { + /* don't count default commodities */ + return; + } + + if(!value) return; + + *count += g_hash_table_size(tbl); +} + +guint +gnc_commodity_table_get_size(gnc_commodity_table* tbl) +{ + guint count = 0; + g_return_val_if_fail(tbl, 0); + g_return_val_if_fail(tbl->table, 0); + + g_hash_table_foreach(tbl->table, count_coms, (gpointer)&count); + + return count; +} + +/******************************************************************** + * gnc_commodity_table_lookup + * locate a commodity by namespace and mnemonic. + ********************************************************************/ + +gnc_commodity * +gnc_commodity_table_lookup(const gnc_commodity_table * table, + const char * namespace, const char * mnemonic) { + gnc_commodity_namespace * nsp = NULL; + + if (!table || !namespace || !mnemonic) return NULL; + + nsp = g_hash_table_lookup(table->table, (gpointer)namespace); + + if(nsp) { + return g_hash_table_lookup(nsp->table, (gpointer)mnemonic); + } + else { + return NULL; + } +} + +/******************************************************************** + * gnc_commodity_table_lookup + * locate a commodity by unique name. + ********************************************************************/ + +gnc_commodity * +gnc_commodity_table_lookup_unique(const gnc_commodity_table *table, + const char * unique_name) +{ + char *namespace; + char *mnemonic; + gnc_commodity *commodity; + + if (!table || !unique_name) return NULL; + + namespace = g_strdup (unique_name); + mnemonic = strstr (namespace, "::"); + if (!mnemonic) + { + g_free (namespace); + return NULL; + } + + *mnemonic = '\0'; + mnemonic += 2; + + commodity = gnc_commodity_table_lookup (table, namespace, mnemonic); + + g_free (namespace); + + return commodity; +} + +/******************************************************************** + * gnc_commodity_table_find_full + * locate a commodity by namespace and printable name + ********************************************************************/ + +gnc_commodity * +gnc_commodity_table_find_full(const gnc_commodity_table * table, + const char * namespace, + const char * fullname) { + gnc_commodity * retval=NULL; + GList * all; + GList * iterator; + + if (!fullname || (fullname[0] == '\0')) + return NULL; + + all = gnc_commodity_table_get_commodities(table, namespace); + + for(iterator = all; iterator; iterator=iterator->next) { + if(!strcmp(fullname, + gnc_commodity_get_printname(iterator->data))) { + retval = iterator->data; + break; + } + } + + g_list_free (all); + + return retval; +} + + +/******************************************************************** + * gnc_commodity_table_insert + * add a commodity to the table. + ********************************************************************/ + +gnc_commodity * +gnc_commodity_table_insert(gnc_commodity_table * table, + gnc_commodity * comm) { + gnc_commodity_namespace * nsp = NULL; + gnc_commodity *c; + + if (!table) return NULL; + if (!comm) return NULL; + + c = gnc_commodity_table_lookup (table, comm->namespace, comm->mnemonic); + + if (c) { + if (c == comm) + return c; + + gnc_commodity_set_fullname (c, gnc_commodity_get_fullname (comm)); + gnc_commodity_set_fraction (c, gnc_commodity_get_fraction (comm)); + gnc_commodity_set_exchange_code (c, + gnc_commodity_get_exchange_code (comm)); + + gnc_commodity_destroy (comm); + + return c; + } + + nsp = g_hash_table_lookup(table->table, (gpointer)(comm->namespace)); + + if(!nsp) { + nsp = g_new0(gnc_commodity_namespace, 1); + nsp->table = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(table->table, + g_strdup(comm->namespace), + (gpointer)nsp); + } + + g_hash_table_insert(nsp->table, + (gpointer)g_strdup(comm->mnemonic), + (gpointer)comm); + + return comm; +} + +/******************************************************************** + * gnc_commodity_table_remove + * remove a commodity from the table. + ********************************************************************/ + +void +gnc_commodity_table_remove(gnc_commodity_table * table, + gnc_commodity * comm) +{ + gnc_commodity_namespace * nsp; + gnc_commodity *c; + + if (!table) return; + if (!comm) return; + + c = gnc_commodity_table_lookup (table, comm->namespace, comm->mnemonic); + if (c != comm) return; + + nsp = g_hash_table_lookup (table->table, comm->namespace); + if (!nsp) return; + + g_hash_table_remove (nsp->table, comm->mnemonic); +} + +/******************************************************************** + * gnc_commodity_table_has_namespace + * see if any commodities in the namespace exist + ********************************************************************/ + +int +gnc_commodity_table_has_namespace(const gnc_commodity_table * table, + const char * namespace) { + gnc_commodity_namespace * nsp = NULL; + + if(!table || !namespace) { return 0; } + + nsp = g_hash_table_lookup(table->table, (gpointer)namespace); + if(nsp) { + return 1; + } + else { + return 0; + } +} + +static void +hash_keys_helper(gpointer key, gpointer value, gpointer data) { + GList ** l = data; + *l = g_list_prepend(*l, key); +} + +static GList * +g_hash_table_keys(GHashTable * table) { + GList * l = NULL; + g_hash_table_foreach(table, &hash_keys_helper, (gpointer) &l); + return l; +} + +static void +hash_values_helper(gpointer key, gpointer value, gpointer data) { + GList ** l = data; + *l = g_list_prepend(*l, value); +} + +static GList * +g_hash_table_values(GHashTable * table) { + GList * l = NULL; + g_hash_table_foreach(table, &hash_values_helper, (gpointer) &l); + return l; +} + +/******************************************************************** + * gnc_commodity_table_get_namespaces + * see if any commodities in the namespace exist + ********************************************************************/ + +GList * +gnc_commodity_table_get_namespaces(const gnc_commodity_table * table) { + if (!table) + return NULL; + + return g_hash_table_keys(table->table); +} + + +/******************************************************************** + * gnc_commodity_table_get_commodities + * list commodities in a give namespace + ********************************************************************/ + +GList * +gnc_commodity_table_get_commodities(const gnc_commodity_table * table, + const char * namespace) { + gnc_commodity_namespace * ns = NULL; + + if(table) { + ns = g_hash_table_lookup(table->table, (gpointer)namespace); + } + + if(ns) { + return g_hash_table_values(ns->table); + } + else { + return NULL; + } +} + +/******************************************************************** + * gnc_commodity_table_add_namespace + * add an empty namespace if it does not exist + ********************************************************************/ + +void +gnc_commodity_table_add_namespace(gnc_commodity_table * table, + const char * namespace) { + gnc_commodity_namespace * ns = NULL; + + if(table) { + ns = g_hash_table_lookup(table->table, (gpointer)namespace); + } + + if(!ns) { + ns = g_new0(gnc_commodity_namespace, 1); + ns->table = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(table->table, + (gpointer) g_strdup(namespace), + (gpointer) ns); + } +} + + +/******************************************************************** + * gnc_commodity_table_delete_namespace + * delete a namespace + ********************************************************************/ + +static int +ns_helper(gpointer key, gpointer value, gpointer user_data) { + gnc_commodity * c = value; + gnc_commodity_destroy(c); + g_free(key); + return TRUE; +} + +void +gnc_commodity_table_delete_namespace(gnc_commodity_table * table, + const char * namespace) { + gpointer orig_key; + gnc_commodity_namespace * value; + + if(table) { + if(g_hash_table_lookup_extended(table->table, + (gpointer) namespace, + &orig_key, + (gpointer)&value)) { + g_hash_table_remove(table->table, namespace); + + g_hash_table_foreach_remove(value->table, ns_helper, NULL); + g_hash_table_destroy(value->table); + g_free(value); + + g_free(orig_key); + } + } +} + +void +gnc_commodity_table_remove_non_iso (gnc_commodity_table *t) +{ + GList *namespaces; + GList *node; + + if (!t) return; + + namespaces = gnc_commodity_table_get_namespaces (t); + + for (node = namespaces; node; node = node->next) + { + char *ns = node->data; + + if (safe_strcmp (ns, GNC_COMMODITY_NS_ISO) == 0) + continue; + + gnc_commodity_table_delete_namespace (t, ns); + } + + g_list_free (namespaces); +} + +/******************************************************************** + * gnc_commodity_table_foreach_commodity + * call user-defined function once for every commodity in every + * namespace + ********************************************************************/ + +typedef struct { + gboolean ok; + gboolean (*func)(gnc_commodity *, gpointer); + gpointer user_data; +} IterData; + +static void +iter_commodity (gpointer key, gpointer value, gpointer user_data) +{ + IterData *iter_data = (IterData *) user_data; + gnc_commodity *cm = (gnc_commodity *) value; + + if (iter_data->ok) + { + iter_data->ok = (iter_data->func)(cm, iter_data->user_data); + } +} + +static void +iter_namespace (gpointer key, gpointer value, gpointer user_data) +{ + GHashTable *namespace_hash = ((gnc_commodity_namespace *) value)->table; + g_hash_table_foreach (namespace_hash, iter_commodity, user_data); +} + +gboolean +gnc_commodity_table_foreach_commodity (gnc_commodity_table * tbl, + gboolean (*f)(gnc_commodity *, gpointer), + gpointer user_data) +{ + IterData iter_data; + + iter_data.ok = TRUE; + iter_data.func = f; + iter_data.user_data = user_data; + + g_hash_table_foreach(tbl->table, iter_namespace, (gpointer)&iter_data); + + return iter_data.ok; +} + +/******************************************************************** + * gnc_commodity_table_destroy + * cleanup and free. + ********************************************************************/ + +static int +ct_helper(gpointer key, gpointer value, gpointer data) { + gnc_commodity_namespace * ns = value; + g_hash_table_foreach_remove(ns->table, ns_helper, NULL); + g_hash_table_destroy(ns->table); + ns->table = NULL; + g_free(ns); + g_free(key); + return TRUE; +} + +void +gnc_commodity_table_destroy(gnc_commodity_table * t) { + if (!t) return; + + g_hash_table_foreach_remove(t->table, ct_helper, t); + g_hash_table_destroy(t->table); + g_free(t); +} + +/* ========================= END OF FILE ============================== */ diff --git a/src/engine/gnc-commodity.h b/src/engine/gnc-commodity.h new file mode 100644 index 0000000000..3bace2a44a --- /dev/null +++ b/src/engine/gnc-commodity.h @@ -0,0 +1,116 @@ +/******************************************************************** + * gnc-commodity.h -- api for tradable commodities (incl. currency) * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + *******************************************************************/ + +#ifndef GNC_COMMODITY_H +#define GNC_COMMODITY_H + +#include + +typedef struct _gnc_commodity gnc_commodity; +typedef struct _gnc_commodity_table gnc_commodity_table; + +#define GNC_COMMODITY_NS_LEGACY "GNC_LEGACY_CURRENCIES" +#define GNC_COMMODITY_NS_ISO "ISO4217" +#define GNC_COMMODITY_NS_NASDAQ "NASDAQ" +#define GNC_COMMODITY_NS_NYSE "NYSE" +#define GNC_COMMODITY_NS_EUREX "EUREX" +#define GNC_COMMODITY_NS_MUTUAL "FUND" +#define GNC_COMMODITY_NS_AMEX "AMEX" +#define GNC_COMMODITY_NS_ASX "ASX" + +/* gnc_commodity functions */ +gnc_commodity * gnc_commodity_new(const char * fullname, + const char * namespace, + const char * mnemonic, + const char * exchange_code, + int fraction); + +void gnc_commodity_destroy(gnc_commodity * cm); + +const char * gnc_commodity_get_mnemonic(const gnc_commodity * cm); +const char * gnc_commodity_get_namespace(const gnc_commodity * cm); +const char * gnc_commodity_get_fullname(const gnc_commodity * cm); +const char * gnc_commodity_get_printname(const gnc_commodity * cm); +const char * gnc_commodity_get_exchange_code(const gnc_commodity * cm); +const char * gnc_commodity_get_unique_name(const gnc_commodity * cm); +int gnc_commodity_get_fraction(const gnc_commodity * cm); +gint16 gnc_commodity_get_mark(const gnc_commodity * cm); + +void gnc_commodity_set_mnemonic(gnc_commodity * cm, const char * mnemonic); +void gnc_commodity_set_namespace(gnc_commodity * cm, const char * namespace); +void gnc_commodity_set_fullname(gnc_commodity * cm, const char * fullname); +void gnc_commodity_set_exchange_code(gnc_commodity * cm, + const char * exchange_code); +void gnc_commodity_set_fraction(gnc_commodity * cm, int smallest_fraction); +void gnc_commodity_set_mark(gnc_commodity * cm, gint16 mark); + +gboolean gnc_commodity_equiv(const gnc_commodity * a, const gnc_commodity * b); + + +/* gnc_commodity_table functions : operate on a database of commodity + * info */ + +gnc_commodity_table * gnc_commodity_table_new(void); +void gnc_commodity_table_destroy(gnc_commodity_table * table); +gnc_commodity * gnc_commodity_table_lookup(const gnc_commodity_table * table, + const char * namespace, + const char * mnemonic); +gnc_commodity * +gnc_commodity_table_lookup_unique(const gnc_commodity_table *table, + const char * unique_name); +gnc_commodity * gnc_commodity_table_find_full(const gnc_commodity_table * t, + const char * namespace, + const char * fullname); +gnc_commodity * gnc_commodity_table_insert(gnc_commodity_table * table, + gnc_commodity * comm); +void gnc_commodity_table_remove(gnc_commodity_table * table, + gnc_commodity * comm); + +int gnc_commodity_table_has_namespace(const gnc_commodity_table * t, + const char * namespace); + +guint gnc_commodity_table_get_size(gnc_commodity_table* tbl); +guint gnc_commodity_table_get_number_of_namespaces(gnc_commodity_table* tbl); + +/* The next two functions return newly allocated lists which should + * be freed with g_list_free. */ +GList * gnc_commodity_table_get_namespaces(const gnc_commodity_table * t); +GList * gnc_commodity_table_get_commodities(const gnc_commodity_table * t, + const char * namespace); + +void gnc_commodity_table_add_namespace(gnc_commodity_table * table, + const char * namespace); +void gnc_commodity_table_delete_namespace(gnc_commodity_table * t, + const char * namespace); + +void gnc_commodity_table_remove_non_iso (gnc_commodity_table *t); + +/* gnc_commodity_table_foreach_commodity - call f once for each commodity in + * table, until and unless f returns FALSE. + */ +gboolean gnc_commodity_table_foreach_commodity(gnc_commodity_table * table, + gboolean (*f)(gnc_commodity *cm, + gpointer user_data), + gpointer user_data); + +#endif diff --git a/src/engine/gnc-common.h b/src/engine/gnc-common.h new file mode 100644 index 0000000000..d4469eaf45 --- /dev/null +++ b/src/engine/gnc-common.h @@ -0,0 +1,31 @@ +/********************************************************************\ + * gnc-common.h -- define platform independent items * + * * + * Copyright (C) 1999, 2000 Rob Browning * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef GNC_COMMON_H +#define GNC_COMMON_H + +#include "config.h" +#include + +#endif diff --git a/src/engine/gnc-engine-util.c b/src/engine/gnc-engine-util.c new file mode 100644 index 0000000000..0b438353a6 --- /dev/null +++ b/src/engine/gnc-engine-util.c @@ -0,0 +1,392 @@ +/********************************************************************\ + * gnc-engine-util.c -- GnuCash engine utility functions * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1997-2001 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + * Author: Rob Clark (rclark@cs.hmc.edu) * + * Author: Linas Vepstas (linas@linas.org) * +\********************************************************************/ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gnc-engine-util.h" +#include "gnc-common.h" +#include "gnc-engine.h" + +/** GLOBALS *********************************************************/ + +/* This static indicates the debugging module that this .o belongs to. */ +/* static short module = MOD_ENGINE; */ + +static gncLogLevel loglevel[MOD_LAST + 1] = +{ + GNC_LOG_FATAL, /* DUMMY */ + GNC_LOG_WARNING, /* ENGINE */ + GNC_LOG_WARNING, /* IO */ + GNC_LOG_WARNING, /* REGISTER */ + GNC_LOG_WARNING, /* LEDGER */ + GNC_LOG_WARNING, /* HTML */ + GNC_LOG_WARNING, /* GUI */ + GNC_LOG_WARNING, /* SCRUB */ + GNC_LOG_WARNING, /* GTK_REG */ + GNC_LOG_WARNING, /* GUILE */ + GNC_LOG_WARNING, /* BACKEND */ + GNC_LOG_WARNING, /* QUERY */ + GNC_LOG_WARNING, /* PRICE */ + GNC_LOG_WARNING, /* SQL EVENT */ + GNC_LOG_WARNING, /* SQL TXN */ + GNC_LOG_WARNING, /* KVP */ + GNC_LOG_DEBUG, /* SX */ +}; + + +/* Set the logging level of the given module. */ +void +gnc_set_log_level(gncModuleType module, gncLogLevel level) +{ + if ((module < 0) || (module > MOD_LAST)) + return; + + loglevel[module] = level; +} + +/* Set the logging level for all modules. */ +void +gnc_set_log_level_global(gncLogLevel level) +{ + gncModuleType module; + + for (module = 0; module <= MOD_LAST; module++) + loglevel[module] = level; +} + +/* prettify() cleans up subroutine names. AIX/xlC has the habit of + * printing signatures not names; clean this up. On other operating + * systems, truncate name to 30 chars. Note this routine is not thread + * safe. Note we wouldn't need this routine if AIX did something more + * reasonable. Hope thread safety doesn't poke us in eye. */ +static const char * +prettify (const char *name) +{ + static char bf[128]; + char *p; + + if (!name) + return ""; + + strncpy (bf, name, 29); bf[28] = 0; + p = strchr (bf, '('); + + if (p) + { + *(p+1) = ')'; + *(p+2) = 0x0; + } + else + strcpy (&bf[26], "...()"); + + return bf; +} + +gboolean +gnc_should_log (gncModuleType module, gncLogLevel log_level) +{ + if (module < 0 || module > MOD_LAST) + { + PERR ("Bad module: %d", module); + return FALSE; + } + + if (log_level > loglevel[module]) + return FALSE; + + return TRUE; +} + +void +gnc_log (gncModuleType module, gncLogLevel log_level, const char *prefix, + const char *function_name, const char *format, ...) +{ + va_list ap; + + if (!gnc_should_log (module, log_level)) + return; + + fprintf (stderr, "%s: %s: ", + prefix ? prefix : "(null)", + prettify (function_name)); + + va_start (ap, format); + + vfprintf (stderr, format, ap); + + va_end (ap); + + fprintf (stderr, "\n"); +} + + +/********************************************************************\ +\********************************************************************/ + +#define NUM_CLOCKS 10 + +static +struct timeval gnc_clock[NUM_CLOCKS] = { + {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, + {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, +}; + +void +gnc_start_clock (int clockno, gncModuleType module, gncLogLevel log_level, + const char *function_name, const char *format, ...) +{ + struct timezone tz; + va_list ap; + + if ((0>clockno) || (NUM_CLOCKS <= clockno)) return; + gettimeofday (&gnc_clock[clockno], &tz); + + fprintf (stderr, "Clock %d Start: %s: ", + clockno, prettify (function_name)); + + va_start (ap, format); + + vfprintf (stderr, format, ap); + + va_end (ap); + + fprintf (stderr, "\n"); +} + +void +gnc_report_clock (int clockno, gncModuleType module, gncLogLevel log_level, + const char *function_name, const char *format, ...) +{ + struct timezone tz; + struct timeval now; + va_list ap; + + if ((0>clockno) || (NUM_CLOCKS <= clockno)) return; + gettimeofday (&now, &tz); + + /* need to borrow to make differnce */ + if (now.tv_usec < gnc_clock[clockno].tv_usec) + { + now.tv_sec --; + now.tv_usec += 1000000; + } + now.tv_sec -= gnc_clock[clockno].tv_sec; + now.tv_usec -= gnc_clock[clockno].tv_usec; + + fprintf (stderr, "Clock %d Elapsed: %ld.%06ld %s: ", + clockno, now.tv_sec, now.tv_usec, prettify (function_name)); + + va_start (ap, format); + + vfprintf (stderr, format, ap); + + va_end (ap); + + fprintf (stderr, "\n"); +} + +/********************************************************************\ +\********************************************************************/ + +/* Search for str2 in first nchar chars of str1, ignore case.. Return + * pointer to first match, or null. */ +char * +strncasestr(const char *str1, const char *str2, size_t len) +{ + while (*str1 && len--) + { + if (toupper(*str1) == toupper(*str2)) + { + if (strncasecmp(str1,str2,strlen(str2)) == 0) + { + return (char *) str1; + } + } + str1++; + } + return NULL; +} + +/* Search for str2 in str1, ignore case. Return pointer to first + * match, or null. */ +char * +strcasestr(const char *str1, const char *str2) +{ + size_t len = strlen (str1); + char * retval = strncasestr (str1, str2, len); + return retval; +} + +/********************************************************************\ +\********************************************************************/ + +int +safe_strcmp (const char * da, const char * db) +{ + SAFE_STRCMP (da, db); + return 0; +} + +int +null_strcmp (const char * da, const char * db) +{ + if (da && db) return strcmp (da, db); + if (!da && db && 0==db[0]) return 0; + if (!db && da && 0==da[0]) return 0; + if (!da && db) return -1; + if (da && !db) return +1; + return 0; +} + +/********************************************************************\ +\********************************************************************/ + +#define MAX_DIGITS 50 + +/* inverse of strtoul */ +char * +ultostr (unsigned long val, int base) +{ + char buf[MAX_DIGITS]; + unsigned long broke[MAX_DIGITS]; + int i; + unsigned long places=0, reval; + + if ((2>base) || (36=0; i--) { + reval += broke[i+1]; + reval *= base; + broke[i] -= reval; + } + + /* print */ + for (i=0; ibroke[i]) { + buf[places-1-i] = 0x30+broke[i]; /* ascii digit zero */ + } else { + buf[places-1-i] = 0x41-10+broke[i]; /* ascii capital A */ + } + } + buf[places] = 0x0; + + return g_strdup (buf); +} + +/********************************************************************\ + * returns TRUE if the string is a number, possibly with whitespace +\********************************************************************/ + +gboolean +gnc_strisnum(const char *s) +{ + if (s == NULL) return FALSE; + if (*s == 0) return FALSE; + + while (*s && isspace(*s)) + s++; + + if (*s == 0) return FALSE; + if (!isdigit(*s)) return FALSE; + + while (*s && isdigit(*s)) + s++; + + if (*s == 0) return TRUE; + + while (*s && isspace(*s)) + s++; + + if (*s == 0) return TRUE; + + return FALSE; +} + +/********************************************************************\ + * our own version of stpcpy +\********************************************************************/ + +char * +gnc_stpcpy (char *dest, const char *src) +{ + strcpy (dest, src); + return (dest + strlen (src)); +} + +/********************************************************************\ + See header for docs. +\********************************************************************/ + +static void +kv_pair_helper(gpointer key, gpointer val, gpointer user_data) +{ + GSList **result = (GSList **) user_data; + GHashTableKVPair *kvp = g_new(GHashTableKVPair, 1); + + kvp->key = key; + kvp->value = val; + *result = g_slist_prepend(*result, kvp); +} + +GSList * +g_hash_table_key_value_pairs(GHashTable *table) +{ + GSList *result_list = NULL; + g_hash_table_foreach(table, kv_pair_helper, &result_list); + return result_list; +} + +void +g_hash_table_kv_pair_free_gfunc(gpointer data, gpointer user_data) +{ + GHashTableKVPair *kvp = (GHashTableKVPair *) data; + g_free(kvp); +} + + +/************************* END OF FILE ******************************\ +\********************************************************************/ diff --git a/src/engine/gnc-engine-util.h b/src/engine/gnc-engine-util.h new file mode 100644 index 0000000000..3efa464d8c --- /dev/null +++ b/src/engine/gnc-engine-util.h @@ -0,0 +1,269 @@ +/********************************************************************\ + * gnc-engine-util.h -- GnuCash engine utility functions * + * Copyright (C) 1997 Robin D. Clark * + * Copyright (C) 1998-2001 Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + * Author: Rob Clark (rclark@cs.hmc.edu) * + * Author: Linas Vepstas (linas@linas.org) * +\********************************************************************/ + +#ifndef GNC_ENGINE_UTIL_H +#define GNC_ENGINE_UTIL_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include + +#include "gnc-common.h" +#include "gnc-commodity.h" +#include "gnc-numeric.h" + +/** DEBUGGING MACROS ************************************************/ +/* The debuging macros enable the setting of trace messages */ + +/** If you modify this, modify the loglevel table in the .c file. */ +typedef enum +{ + MOD_DUMMY = 0, + MOD_ENGINE = 1, + MOD_IO = 2, + MOD_REGISTER= 3, + MOD_LEDGER = 4, + MOD_HTML = 5, + MOD_GUI = 6, + MOD_SCRUB = 7, + MOD_GTK_REG = 8, + MOD_GUILE = 9, + MOD_BACKEND = 10, + MOD_QUERY = 11, + MOD_PRICE = 12, + MOD_EVENT = 13, + MOD_TXN = 14, + MOD_KVP = 15, + MOD_SX = 16, + MOD_LAST = 16 +} gncModuleType; + +typedef enum +{ + GNC_LOG_FATAL = 0, + GNC_LOG_ERROR = 1, + GNC_LOG_WARNING = 2, + GNC_LOG_INFO = 3, + GNC_LOG_DEBUG = 4, + GNC_LOG_DETAIL = 5, + GNC_LOG_TRACE = 6, +} gncLogLevel; + +/* FIXME: these logging functions should proably get replaced by + * the glib.h g_error(), etc functions. That way, we would have + * unified logging mechanism, instead of having some messages + * work one way, and other a different way ... + */ +gboolean gnc_should_log (gncModuleType module, gncLogLevel log_level); +void gnc_log (gncModuleType module, gncLogLevel log_level, + const char *prefix, const char *function_name, + const char *format, ...) G_GNUC_PRINTF(5,6); + +#define FATAL(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_FATAL)) \ + gnc_log (module, GNC_LOG_FATAL, "Fatal Error", \ + __FUNCTION__, format, ## args); \ +} + +#define PERR(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_ERROR)) \ + gnc_log (module, GNC_LOG_ERROR, "Error", \ + __FUNCTION__, format, ##args); \ +} + +#define PWARN(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_WARNING)) \ + gnc_log (module, GNC_LOG_WARNING, "Warning", \ + __FUNCTION__, format, ## args); \ +} + +#define PINFO(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_INFO)) \ + gnc_log (module, GNC_LOG_INFO, "Info", \ + __FUNCTION__, format, ## args); \ +} + +#define DEBUG(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_DEBUG)) \ + gnc_log (module, GNC_LOG_DEBUG, "Debug", \ + __FUNCTION__, format, ## args); \ +} + +#define ENTER(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_DEBUG)) \ + gnc_log (module, GNC_LOG_DEBUG, "Enter", \ + __FUNCTION__, format, ## args); \ +} + +#define LEAVE(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_DEBUG)) \ + gnc_log (module, GNC_LOG_DEBUG, "Leave", \ + __FUNCTION__, format, ## args); \ +} + +#define DETAIL(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_DETAIL)) \ + gnc_log (module, GNC_LOG_DETAIL, "Detail", \ + __FUNCTION__, format, ## args); \ +} + + +#define DEBUGCMD(x) { if (gnc_should_log (module, GNC_LOG_DEBUG)) { x; }} + +#define ERROR() fprintf(stderr,"%s: Line %d, error = %s\n", \ + __FILE__, __LINE__, strerror(errno)); + +#define TRACE(format, args...) { \ + if (gnc_should_log (module, GNC_LOG_TRACE)) \ + gnc_log (module, GNC_LOG_TRACE, "Trace", \ + __FUNCTION__, format, ## args); \ +} + +void gnc_start_clock (int clockno, gncModuleType module, gncLogLevel log_level, + const char *function_name, const char *format, ...); + +void gnc_report_clock (int clockno, gncModuleType module, gncLogLevel log_level, + const char *function_name, const char *format, ...); + + +#define START_CLOCK(clockno,format, args...) { \ + if (gnc_should_log (module, GNC_LOG_INFO)) \ + gnc_start_clock (clockno, module, GNC_LOG_INFO,\ + __FUNCTION__, format, ## args); \ +} + +#define REPORT_CLOCK(clockno,format, args...) { \ + if (gnc_should_log (module, GNC_LOG_INFO)) \ + gnc_report_clock (clockno, module, GNC_LOG_INFO,\ + __FUNCTION__, format, ## args); \ +} + + +/* Set the logging level of the given module. */ +void gnc_set_log_level(gncModuleType module, gncLogLevel level); + +/* Set the logging level for all modules. */ +void gnc_set_log_level_global(gncLogLevel level); + + +/** Macros *****************************************************/ +#define EPS (1.0e-6) +#define DEQEPS(x,y,eps) (((((x)+(eps))>(y)) ? 1 : 0) && ((((x)-(eps))<(y)) ? 1 : 0)) +#define DEQ(x,y) DEQEPS(x,y,EPS) + + +#define SAFE_STRCMP(da,db) { \ + if ((da) && (db)) { \ + int retval = strcmp ((da), (db)); \ + /* if strings differ, return */ \ + if (retval) return retval; \ + } else \ + if ((!(da)) && (db)) { \ + return -1; \ + } else \ + if ((da) && (!(db))) { \ + return +1; \ + } \ +} + +/* Define the long long int conversion for scanf */ +#if HAVE_SCANF_LLD +# define GNC_SCANF_LLD "%lld" +#else +# define GNC_SCANF_LLD "%qd" +#endif + + +/** Prototypes *************************************************/ + +/* The safe_strcmp compares strings a and b the same way that strcmp() + * does, except that either may be null. This routine assumes that + * a non-null string is always greater than a null string. + */ +int safe_strcmp (const char * da, const char * db); + +/* The null_strcmp compares strings a and b the same way that strcmp() + * does, except that either may be null. This routine assumes that + * a null string is equal to the empty string. + */ +int null_strcmp (const char * da, const char * db); + +/* Search for str2 in first nchar chars of str1, ignore case. Return + * pointer to first match, or null. These are just like that strnstr + * and the strstr functions, except that they ignore the case. */ +extern char *strncasestr(const char *str1, const char *str2, size_t len); +extern char *strcasestr(const char *str1, const char *str2); + +/* The ultostr() subroutine is the inverse of strtoul(). It accepts a + * number and prints it in the indicated base. The returned string + * should be g_freed when done. */ +char * ultostr (unsigned long val, int base); + +/* Returns true if string s is a number, possibly surrounded by + * whitespace. */ +gboolean gnc_strisnum(const char *s); + +/* Define a gnucash stpcpy */ +char * gnc_stpcpy (char *dest, const char *src); + +#ifndef HAVE_STPCPY +#define stpcpy gnc_stpcpy +#endif + + +/***********************************************************************\ + + g_hash_table_key_value_pairs(hash): Returns a GSList* of all the + keys and values in a given hash table. Data elements of lists are + actual hash elements, so be careful, and deallocation of the + GHashTableKVPairs in the result list are the caller's + responsibility. A typical sequence might look like this: + + GSList *kvps = g_hash_table_key_value_pairs(hash); + ... use kvps->data->key and kvps->data->val, etc. here ... + g_slist_foreach(kvps, g_hash_table_kv_pair_free_gfunc, NULL); + g_slist_free(kvps); + +*/ + +typedef struct { + gpointer key; + gpointer value; +} GHashTableKVPair; + +GSList *g_hash_table_key_value_pairs(GHashTable *table); +void g_hash_table_kv_pair_free_gfunc(gpointer data, gpointer user_data); + +/***********************************************************************/ + +#endif diff --git a/src/engine/gnc-engine.c b/src/engine/gnc-engine.c new file mode 100644 index 0000000000..f566390fbd --- /dev/null +++ b/src/engine/gnc-engine.c @@ -0,0 +1,117 @@ +/******************************************************************** + * gnc-engine.c -- top-level initialization for Gnucash Engine * + * Copyright 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + ********************************************************************/ + +#include "config.h" + +#include +#include + +#include "GNCIdP.h" +#include "gnc-engine.h" + +static GList * engine_init_hooks = NULL; +static gnc_commodity_table * known_commodities = NULL; +static int engine_is_initialized = 0; +GCache * gnc_string_cache = NULL; + +/******************************************************************** + * gnc_engine_init + * initialize backend, load any necessary databases, etc. + ********************************************************************/ + +void +gnc_engine_init(int argc, char ** argv) { + gnc_engine_init_hook_t hook; + GList * cur; + + if (1 == engine_is_initialized) return; + engine_is_initialized = 1; + + /* initialize the string cache */ + gnc_engine_get_string_cache(); + + xaccGUIDInit (); + + /* initialize the commodity table (it starts empty) */ + known_commodities = gnc_commodity_table_new(); + + /* call any engine hooks */ + for(cur = engine_init_hooks; cur; cur = cur->next) { + hook = (gnc_engine_init_hook_t)cur->data; + if(hook) { + (*hook)(argc, argv); + } + } +} + +GCache* +gnc_engine_get_string_cache(void) +{ + if(!gnc_string_cache) + { + gnc_string_cache = g_cache_new( + (GCacheNewFunc) g_strdup, g_free, + (GCacheDupFunc) g_strdup, g_free, g_str_hash, + g_str_hash, g_str_equal); + } + return gnc_string_cache; +} + +/******************************************************************** + * gnc_engine_shutdown + * shutdown backend, destroy any global data, etc. + ********************************************************************/ + +void +gnc_engine_shutdown (void) +{ + g_cache_destroy (gnc_string_cache); + gnc_string_cache = NULL; + + xaccGUIDShutdown (); + + gnc_commodity_table_destroy (known_commodities); + known_commodities = NULL; +} + +/******************************************************************** + * gnc_engine_add_init_hook + * add a startup hook + ********************************************************************/ + +void +gnc_engine_add_init_hook(gnc_engine_init_hook_t h) { + engine_init_hooks = g_list_append(engine_init_hooks, (gpointer)h); +} + + +/******************************************************************** + * gnc_engine_commodities() + * get the global gnc_engine commodity table + ********************************************************************/ + +gnc_commodity_table * +gnc_engine_commodities(void) { + assert(engine_is_initialized); + return known_commodities; +} diff --git a/src/engine/gnc-engine.h b/src/engine/gnc-engine.h new file mode 100644 index 0000000000..52b8ab5a86 --- /dev/null +++ b/src/engine/gnc-engine.h @@ -0,0 +1,69 @@ +/******************************************************************** + * gnc-engine.h -- top-level include file for Gnucash Engine * + * Copyright 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + ********************************************************************/ + +#ifndef GNC_ENGINE_H +#define GNC_ENGINE_H + +#include "gnc-commodity.h" + +typedef void (* gnc_engine_init_hook_t)(int, char **); + +/** PROTOTYPES ******************************************************/ + +/* gnc_engine_init MUST be called before gnc engine functions can + * be used. */ +void gnc_engine_init(int argc, char ** argv); + +/* called to shutdown the engine */ +void gnc_engine_shutdown (void); + +/* pass a function pointer to gnc_engine_add_init_hook and + * it will be called during the evaluation of gnc_engine_init */ +void gnc_engine_add_init_hook(gnc_engine_init_hook_t hook); + +/* this is a global table of known commodity types. */ +gnc_commodity_table * gnc_engine_commodities(void); + +/* Many strings used throughout the engine are likely to be duplicated. + * So we provide a reference counted cache system for the strings, which + * shares strings whenever possible. + * + * Use g_cache_insert to insert a string into the cache (it will return a + * pointer to the cached string). + * Basically you should use this instead of g_strdup. + * + * Use g_cache_remove (giving it a pointer to a cached string) if the string + * is unused. If this is the last reference to the string it will be + * removed from the cache, otherwise it will just decrement the + * reference count. + * Basically you should use this instead of g_free. + * + * Note that all the work is done when inserting or removing. Once + * cached the strings are just plain C strings. + */ + +/* get the gnc_string_cache. Create it if it doesn't exist already */ +GCache* gnc_engine_get_string_cache(void); + +#endif + diff --git a/src/engine/gnc-event-p.h b/src/engine/gnc-event-p.h new file mode 100644 index 0000000000..19a1431e44 --- /dev/null +++ b/src/engine/gnc-event-p.h @@ -0,0 +1,45 @@ +/******************************************************************** + * gnc-event-p.h -- private engine event handling interface * + * Copyright 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + ********************************************************************/ + +#ifndef GNC_EVENT_P_H +#define GNC_EVENT_P_H + +#include "gnc-event.h" + +/* gnc_engine_generate_event + * Invoke all registered event handlers using the given arguments. + * + * GNC_EVENT_CREATE events should be generated after the object + * has been created and registered in the engine entity table. + * GNC_EVENT_MODIFY events should be generated whenever any data + * member or submember (i.e., splits) is changed. + * GNC_EVENT_DESTROY events should be called before the object + * has been destroyed or removed from the entity table. + * + * entity: the GUID of the entity generating the event + * event_type: the type of event -- this should be one of the + * single-bit GNCEngineEventType values, not a combination. + */ +void gnc_engine_generate_event (GUID *entity, GNCEngineEventType event_type); + +#endif diff --git a/src/engine/gnc-event.c b/src/engine/gnc-event.c new file mode 100644 index 0000000000..bca005e2c1 --- /dev/null +++ b/src/engine/gnc-event.c @@ -0,0 +1,185 @@ +/******************************************************************** + * gnc-event.c -- engine event handling implementation * + * Copyright 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + ********************************************************************/ + +#include "config.h" + +#include "gnc-engine-util.h" +#include "gnc-event-p.h" + + +/** Declarations ****************************************************/ + +typedef struct +{ + GNCEngineEventHandler handler; + gpointer user_data; + + gint handler_id; +} HandlerInfo; + + +/** Static Variables ************************************************/ +static guint suspend_counter = 0; +static gint next_handler_id = 0; +static GList *handlers = NULL; + +/* This static indicates the debugging module that this .o belongs to. */ +static short module = MOD_ENGINE; + + +/** Implementations *************************************************/ + +gint +gnc_engine_register_event_handler (GNCEngineEventHandler handler, + gpointer user_data) +{ + HandlerInfo *hi; + gint handler_id; + GList *node; + + /* sanity check */ + if (!handler) + { + PERR ("no handler specified"); + return 0; + } + + /* look for a free handler id */ + handler_id = next_handler_id; + node = handlers; + + while (node) + { + hi = node->data; + + if (hi->handler_id == handler_id) + { + handler_id++; + node = handlers; + continue; + } + + node = node->next; + } + + /* found one, add the handler */ + hi = g_new0 (HandlerInfo, 1); + + hi->handler = handler; + hi->user_data = user_data; + hi->handler_id = handler_id; + + handlers = g_list_prepend (handlers, hi); + + /* update id for next registration */ + next_handler_id = handler_id + 1; + + return handler_id; +} + +void +gnc_engine_unregister_event_handler (gint handler_id) +{ + GList *node; + + for (node = handlers; node; node = node->next) + { + HandlerInfo *hi = node->data; + + if (hi->handler_id != handler_id) + continue; + + /* found it */ + + /* take out of list */ + handlers = g_list_remove_link (handlers, node); + + /* safety */ + hi->handler = NULL; + + g_list_free_1 (node); + g_free (hi); + + return; + } + + PERR ("no such handler: %d", handler_id); +} + +void +gnc_engine_suspend_events (void) +{ + suspend_counter++; + + if (suspend_counter == 0) + { + PERR ("suspend counter overflow"); + } +} + +void +gnc_engine_resume_events (void) +{ + if (suspend_counter == 0) + { + PERR ("suspend counter underflow"); + return; + } + + suspend_counter--; +} + +void +gnc_engine_generate_event (GUID *entity, GNCEngineEventType event_type) +{ + GList *node; + + if (!entity) + return; + + if (suspend_counter) + return; + + switch (event_type) + { + case GNC_EVENT_NONE: + return; + + case GNC_EVENT_CREATE: + case GNC_EVENT_MODIFY: + case GNC_EVENT_DESTROY: + break; + + default: + PERR ("bad event type %d", event_type); + return; + } + + for (node = handlers; node; node = node->next) + { + HandlerInfo *hi = node->data; + + if (hi->handler) + hi->handler (entity, event_type, hi->user_data); + } +} diff --git a/src/engine/gnc-event.h b/src/engine/gnc-event.h new file mode 100644 index 0000000000..bdf51a3b71 --- /dev/null +++ b/src/engine/gnc-event.h @@ -0,0 +1,84 @@ +/******************************************************************** + * gnc-event.h -- engine event handling interface * + * Copyright 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + ********************************************************************/ + +#ifndef GNC_EVENT_H +#define GNC_EVENT_H + +#include + +#include "guid.h" + + +typedef enum +{ + GNC_EVENT_NONE = 0, + GNC_EVENT_CREATE = 1 << 0, + GNC_EVENT_MODIFY = 1 << 1, + GNC_EVENT_DESTROY = 1 << 2, + GNC_EVENT_ALL = 0xff +} GNCEngineEventType; + + +/* GNCEngineEventHandler + * Handler invoked when an engine event occurs. + * + * entity: GUID of entity generating event + * event_type: one of the single-bit GNCEngineEventTypes, not a combination + * user_data: user_data supplied when handler was registered. + */ +typedef void (*GNCEngineEventHandler) (GUID *entity, + GNCEngineEventType event_type, + gpointer user_data); + +/* gnc_engine_register_event_handler + * Register a handler for engine events. + * + * handler: handler to register + * user_data: data provided when handler is invoked + * + * Returns: id identifying handler + */ +gint gnc_engine_register_event_handler (GNCEngineEventHandler handler, + gpointer user_data); + +/* gnc_engine_unregister_event_handler + * Unregister an engine event handler. + * + * handler_id: the id of the handler to unregister + */ +void gnc_engine_unregister_event_handler (gint handler_id); + +/* gnc_engine_suspend_events + * Suspend all engine events. This function may be + * called multiple times. To resume event generation, + * an equal number of calls to gnc_engine_resume_events + * must be made. + */ +void gnc_engine_suspend_events (void); + +/* gnc_engine_resume_events + * Resume engine event generation. + */ +void gnc_engine_resume_events (void); + +#endif diff --git a/src/engine/gnc-numeric.c b/src/engine/gnc-numeric.c new file mode 100644 index 0000000000..93227d65e1 --- /dev/null +++ b/src/engine/gnc-numeric.c @@ -0,0 +1,1268 @@ +/******************************************************************** + * gnc-numeric.c -- an exact-number library for gnucash. * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + *******************************************************************/ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +#include "gnc-engine-util.h" +#include "gnc-numeric.h" + +/* TODO + * - use longer intermediate values to make operations + * 64-bit-overflow-proof + */ + +/* static short module = MOD_ENGINE; */ + +static const char * _numeric_error_strings[] = +{ + "No error", + "Argument is not a valid number", + "Intermediate result overflow", + "Argument denominators differ in GNC_DENOM_FIXED operation", + "Remainder part in GNC_RND_NEVER operation" +}; + +static gint64 gnc_numeric_lcd(gnc_numeric a, gnc_numeric b); + +/******************************************************************** + * gnc_numeric_zero_p + ********************************************************************/ + +int +gnc_numeric_zero_p(gnc_numeric a) { + if(gnc_numeric_check(a)) { + return 0; + } + else { + if((a.num == 0) && (a.denom != 0)) { + return 1; + } + else { + return 0; + } + } +} + +/******************************************************************** + * gnc_numeric_negative_p + ********************************************************************/ + +int +gnc_numeric_negative_p(gnc_numeric a) { + if(gnc_numeric_check(a)) { + return 0; + } + else { + if((a.num < 0) && (a.denom != 0)) { + return 1; + } + else { + return 0; + } + } +} + +/******************************************************************** + * gnc_numeric_positive_p + ********************************************************************/ + +int +gnc_numeric_positive_p(gnc_numeric a) { + if(gnc_numeric_check(a)) { + return 0; + } + else { + if((a.num > 0) && (a.denom != 0)) { + return 1; + } + else { + return 0; + } + } +} + + +/******************************************************************** + * gnc_numeric_compare + * returns 1 if a>b, -1 if b>a, 0 if a == b + ********************************************************************/ + +int +gnc_numeric_compare(gnc_numeric a, gnc_numeric b) { + gint64 ab, ba; + + if(gnc_numeric_check(a) || gnc_numeric_check(b)) { + return 0; + } + ab = a.num * b.denom; + ba = b.num * a.denom; + + if(ab == ba) { + return 0; + } + else if(ab > ba) { + return 1; + } + else { + return -1; + } +} + + +/******************************************************************** + * gnc_numeric_eq + ********************************************************************/ + +int +gnc_numeric_eq(gnc_numeric a, gnc_numeric b) { + return ((a.num == b.num) && (a.denom == b.denom)); +} + + +/******************************************************************** + * gnc_numeric_equal + ********************************************************************/ + +int +gnc_numeric_equal(gnc_numeric a, gnc_numeric b) { + if(((a.denom > 0) && (b.denom > 0)) || + ((a.denom < 0) && (b.denom < 0))) { + return ((a.num * b.denom) == (a.denom * b.num)); + } + else { + return 0; + } +} + + +/******************************************************************** + * gnc_numeric_same + * would a and b be equal() if they were both converted to the same + * denominator? + ********************************************************************/ + +int +gnc_numeric_same(gnc_numeric a, gnc_numeric b, gint64 denom, + gint how) { + gnc_numeric aconv, bconv; + + aconv = gnc_numeric_convert(a, denom, how); + bconv = gnc_numeric_convert(b, denom, how); + + return(gnc_numeric_equal(aconv, bconv)); +} + + + +/******************************************************************** + * gnc_numeric_add + ********************************************************************/ + +gnc_numeric +gnc_numeric_add(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how) { + gnc_numeric sum; + + if(gnc_numeric_check(a) || gnc_numeric_check(b)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + + if((denom == GNC_DENOM_AUTO) && + (how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_FIXED) { + if(a.denom == b.denom) { + denom = a.denom; + } + else if(b.num == 0) { + denom = a.denom; + } + else if(a.num == 0) { + denom = b.denom; + } + else { + return gnc_numeric_error(GNC_ERROR_DENOM_DIFF); + } + } + + if(a.denom < 0) { + a.num *= a.denom; + a.denom = 1; + } + + if(b.denom < 0) { + b.num *= b.denom; + b.denom = 1; + } + + /* get an exact answer.. same denominator is the common case. */ + if(a.denom == b.denom) { + sum.num = a.num + b.num; + sum.denom = a.denom; + } + else { + sum.num = a.num*b.denom + b.num*a.denom; + sum.denom = a.denom*b.denom; + } + + if((denom == GNC_DENOM_AUTO) && + ((how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_LCD)) { + denom = gnc_numeric_lcd(a, b); + how = how & GNC_NUMERIC_RND_MASK; + } + + return gnc_numeric_convert(sum, denom, how); +} + + +/******************************************************************** + * gnc_numeric_add_fixed + ********************************************************************/ + +gnc_numeric +gnc_numeric_add_fixed(gnc_numeric a, gnc_numeric b) { + return gnc_numeric_add(a, b, GNC_DENOM_AUTO, + GNC_DENOM_FIXED | GNC_RND_NEVER); +} + + +/******************************************************************** + * gnc_numeric_sub + ********************************************************************/ + +gnc_numeric +gnc_numeric_sub(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how) { + gnc_numeric diff; + + if(gnc_numeric_check(a) || gnc_numeric_check(b)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + + if((denom == GNC_DENOM_AUTO) && + (how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_FIXED) { + if(a.denom == b.denom) { + denom = a.denom; + } + else if(b.num == 0) { + denom = a.denom; + } + else if(a.num == 0) { + denom = b.denom; + } + else { + return gnc_numeric_error(GNC_ERROR_DENOM_DIFF); + } + } + + if(a.denom < 0) { + a.num *= a.denom; + a.denom = 1; + } + + if(b.denom < 0) { + b.num *= b.denom; + b.denom = 1; + } + + /* get an exact answer.. same denominator is the common case. */ + if(a.denom == b.denom) { + diff.num = a.num - b.num; + diff.denom = a.denom; + } + else { + diff.num = a.num*b.denom - b.num*a.denom; + diff.denom = a.denom*b.denom; + } + + if((denom == GNC_DENOM_AUTO) && + ((how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_LCD)) { + denom = gnc_numeric_lcd(a, b); + how = how & GNC_NUMERIC_RND_MASK; + } + return gnc_numeric_convert(diff, denom, how); +} + + +/******************************************************************** + * gnc_numeric_sub_fixed + ********************************************************************/ + +gnc_numeric +gnc_numeric_sub_fixed(gnc_numeric a, gnc_numeric b) { + return gnc_numeric_sub(a, b, GNC_DENOM_AUTO, + GNC_DENOM_FIXED | GNC_RND_NEVER); +} + + +/******************************************************************** + * gnc_numeric_mul + ********************************************************************/ + +gnc_numeric +gnc_numeric_mul(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how) { + gnc_numeric product; + + if(gnc_numeric_check(a) || gnc_numeric_check(b)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + + if((denom == GNC_DENOM_AUTO) && + (how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_FIXED) { + if(a.denom == b.denom) { + denom = a.denom; + } + else if(b.num == 0) { + denom = a.denom; + } + else if(a.num == 0) { + denom = b.denom; + } + else { + return gnc_numeric_error(GNC_ERROR_DENOM_DIFF); + } + } + + if(a.denom < 0) { + a.num *= a.denom; + a.denom = 1; + } + + if(b.denom < 0) { + b.num *= b.denom; + b.denom = 1; + } + + product.num = a.num*b.num; + product.denom = a.denom*b.denom; + + if(product.denom < 0) { + product.num = -product.num; + product.denom = -product.denom; + } + + if((denom == GNC_DENOM_AUTO) && + ((how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_LCD)) { + denom = gnc_numeric_lcd(a, b); + how = how & GNC_NUMERIC_RND_MASK; + } + + return gnc_numeric_convert(product, denom, how); +} + + +/******************************************************************** + * gnc_numeric_div + ********************************************************************/ + +gnc_numeric +gnc_numeric_div(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how) { + gnc_numeric quotient; + + if(gnc_numeric_check(a) || gnc_numeric_check(b)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + + if((denom == GNC_DENOM_AUTO) && + (how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_FIXED) { + if(a.denom == b.denom) { + denom = a.denom; + } + else if(a.denom == 0) { + denom = b.denom; + } + else { + return gnc_numeric_error(GNC_ERROR_DENOM_DIFF); + } + } + + + if(a.denom < 0) { + a.num *= a.denom; + a.denom = 1; + } + + if(b.denom < 0) { + b.num *= b.denom; + b.denom = 1; + } + + if(a.denom == b.denom) { + quotient.num = a.num; + quotient.denom = b.num; + } + else { + quotient.num = a.num*b.denom; + quotient.denom = a.denom*b.num; + } + + if(quotient.denom < 0) { + quotient.num = -quotient.num; + quotient.denom = -quotient.denom; + } + + if((denom == GNC_DENOM_AUTO) && + ((how & GNC_NUMERIC_DENOM_MASK) == GNC_DENOM_LCD)) { + denom = gnc_numeric_lcd(a, b); + how = how & GNC_NUMERIC_RND_MASK; + } + + return gnc_numeric_convert(quotient, denom, how); +} + +/******************************************************************** + * gnc_numeric_neg + * negate the argument + ********************************************************************/ + +gnc_numeric +gnc_numeric_neg(gnc_numeric a) { + if(gnc_numeric_check(a)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + return gnc_numeric_create(- a.num, a.denom); +} + +/******************************************************************** + * gnc_numeric_neg + * return the absolute value of the argument + ********************************************************************/ + +gnc_numeric +gnc_numeric_abs(gnc_numeric a) { + if(gnc_numeric_check(a)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + return gnc_numeric_create(ABS(a.num), a.denom); +} + +/******************************************************************** + * gnc_numeric_convert + ********************************************************************/ + +gnc_numeric +gnc_numeric_convert(gnc_numeric in, gint64 denom, gint how) { + gnc_numeric out; + gnc_numeric temp; + gint64 temp_bc; + gint64 temp_a; + gint64 remainder; + gint64 sign; + gint denom_neg=0; + double ratio, logratio; + double sigfigs; + + if(gnc_numeric_check(in)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + + if(denom == GNC_DENOM_AUTO) { + switch(how & GNC_NUMERIC_DENOM_MASK) { + case GNC_DENOM_EXACT: + return in; + break; + + case GNC_DENOM_REDUCE: + /* reduce the input to a relatively-prime fraction */ + return gnc_numeric_reduce(in); + break; + + case GNC_DENOM_FIXED: + if(in.denom != denom) { + return gnc_numeric_error(GNC_ERROR_DENOM_DIFF); + } + else { + return in; + } + break; + + case GNC_DENOM_SIGFIG: + ratio = fabs(gnc_numeric_to_double(in)); + if(ratio < 10e-20) { + logratio = 0; + } + else { + logratio = log10(ratio); + logratio = ((logratio > 0.0) ? + (floor(logratio)+1.0) : (ceil(logratio))); + } + sigfigs = GNC_NUMERIC_GET_SIGFIGS(how); + + if(sigfigs-logratio >= 0) { + denom = (gint64)(pow(10, sigfigs-logratio)); + } + else { + denom = -((gint64)(pow(10, logratio-sigfigs))); + } + + how = how & ~GNC_DENOM_SIGFIG & ~GNC_NUMERIC_SIGFIGS_MASK; + break; + + case GNC_DENOM_LCD: + /* this is a no-op. */ + default: + break; + } + } + + /* make sure we need to do the work */ + if(in.denom == denom) { + return in; + } + + /* if the denominator of the input value is negative, get rid of that. */ + if(in.denom < 0) { + in.num = in.num * (- in.denom); + in.denom = 1; + } + + sign = (in.num < 0) ? -1 : 1; + + /* if the denominator is less than zero, we are to interpret it as + * the reciprocal of its magnitude. */ + if(denom < 0) { + denom = - denom; + denom_neg = 1; + temp_a = (in.num < 0) ? -in.num : in.num; + temp_bc = in.denom * denom; + remainder = in.num % temp_bc; + out.num = in.num / temp_bc; + out.denom = - denom; + } + else { + /* do all the modulo and int division on positive values to make + * things a little clearer. Reduce the fraction denom/in.denom to + * help with range errors (FIXME : need bigger intermediate rep) */ + temp.num = denom; + temp.denom = in.denom; + temp = gnc_numeric_reduce(temp); + + out.num = in.num * temp.num; + out.num = (out.num < 0) ? -out.num : out.num; + remainder = out.num % temp.denom; + out.num = out.num / temp.denom; + out.denom = denom; + } + + if(remainder > 0) { + switch(how) { + case GNC_RND_FLOOR: + if(sign < 0) { + out.num = out.num + 1; + } + break; + + case GNC_RND_CEIL: + if(sign > 0) { + out.num = out.num + 1; + } + break; + + case GNC_RND_TRUNC: + break; + + case GNC_RND_PROMOTE: + out.num = out.num + 1; + break; + + case GNC_RND_ROUND_HALF_DOWN: + if(denom_neg) { + if((2 * remainder) > in.denom*denom) { + out.num = out.num + 1; + } + } + else if((2 * remainder) > temp.denom) { + out.num = out.num + 1; + } + break; + + case GNC_RND_ROUND_HALF_UP: + if(denom_neg) { + if((2 * remainder) >= in.denom*denom) { + out.num = out.num + 1; + } + } + else if((2 * remainder ) >= temp.denom) { + out.num = out.num + 1; + } + break; + + case GNC_RND_ROUND: + if(denom_neg) { + if((2 * remainder) > in.denom*denom) { + out.num = out.num + 1; + } + else if((2 * remainder) == in.denom*denom) { + if(out.num % 2) { + out.num = out.num + 1; + } + } + } + else { + if((2 * remainder ) > temp.denom) { + out.num = out.num + 1; + } + else if((2 * remainder) == temp.denom) { + if(out.num % 2) { + out.num = out.num + 1; + } + } + } + break; + + case GNC_RND_NEVER: + return gnc_numeric_error(GNC_ERROR_REMAINDER); + break; + } + } + + out.num = (sign > 0) ? out.num : (-out.num); + + return out; +} + + +/******************************************************************** + * gnc_numeric_lcd + * Find the least common multiple of the denominators of + * a and b + ********************************************************************/ + +gint64 +gnc_numeric_lcd(gnc_numeric a, gnc_numeric b) { + gint64 current_divisor = 2; + gint64 max_square; + gint64 three_count = 0; + gint64 small_denom; + gint64 big_denom; + + if(gnc_numeric_check(a) || gnc_numeric_check(b)) { + return GNC_ERROR_ARG; + } + + if(b.denom < a.denom) { + small_denom = b.denom; + big_denom = a.denom; + } + else { + small_denom = a.denom; + big_denom = b.denom; + } + + /* special case: smaller divides smoothly into larger */ + if((big_denom % small_denom) == 0) { + return big_denom; + } + + max_square = small_denom; + + /* the LCM algorithm : take the union of the prime factors of the + * two args and multiply them together. To do this, we find the + * successive prime factors of the smaller denominator and eliminate + * them from the larger denominator, then multiply the smaller by + * the remains of the larger. */ + while(current_divisor * current_divisor <= max_square) { + if(((small_denom % current_divisor) == 0) && + ((big_denom % current_divisor) == 0)) { + big_denom = big_denom / current_divisor; + } + else { + if(current_divisor == 2) { + current_divisor++; + } + else if(three_count == 3) { + current_divisor += 4; + three_count = 1; + } + else { + current_divisor += 2; + three_count++; + } + } + + if((current_divisor > small_denom) || + (current_divisor > big_denom)) { + break; + } + } + + return small_denom * big_denom; + +} + + +/******************************************************************** + * gnc_numeric_reduce + * reduce a fraction by GCF elimination. This is NOT done as a + * part of the arithmetic API unless GNC_DENOM_REDUCE is specified + * as the output denominator. + ********************************************************************/ + +gnc_numeric +gnc_numeric_reduce(gnc_numeric in) { + + gint64 current_divisor = 2; + gint64 max_square; + gint64 num = (in.num < 0) ? (- in.num) : in.num ; + gint64 denom = in.denom; + int three_count = 0; + gnc_numeric out; + + if(gnc_numeric_check(in)) { + return gnc_numeric_error(GNC_ERROR_ARG); + } + + /* the strategy is to eliminate common factors from + * 2 up to 'max', where max is the smaller of the smaller + * part of the fraction and the sqrt of the larger part of + * the fraction. There's also the special case of the + * smaller of fraction parts being a common factor. + * + * we test for 2 and 3 first, and thereafter skip all even numbers + * and all odd multiples of 3 (that's every third odd number, + * i.e. 9, 15, 21), thus the three_count stuff. */ + + /* special case: one side divides evenly by the other */ + if (num == 0) { + denom = 1; + } + else if((num > denom) && (num % denom == 0)) { + num = num / denom; + denom = 1; + } + else if ((num <= denom) && (denom % num == 0)) { + denom = denom / num; + num = 1; + } + + max_square = (num > denom) ? denom : num; + + /* normal case: test 2, then 3, 5, 7, 11, etc. + * (skip multiples of 2 and 3) */ + while(current_divisor * current_divisor <= max_square) { + if((num % current_divisor == 0) && + (denom % current_divisor == 0)) { + num = num / current_divisor; + denom = denom / current_divisor; + } + else { + if(current_divisor == 2) { + current_divisor++; + } + else if(three_count == 3) { + current_divisor += 4; + three_count = 1; + } + else { + current_divisor += 2; + three_count++; + } + } + + if((current_divisor > num) || + (current_divisor > denom)) { + break; + } + } + + /* all calculations are done on positive num, since it's not + * well defined what % does for negative values */ + out.num = (in.num < 0) ? (- num) : num; + out.denom = denom; + return out; +} + +/******************************************************************** + * double_to_gnc_numeric + ********************************************************************/ + +gnc_numeric +double_to_gnc_numeric(double in, gint64 denom, gint how) { + gnc_numeric out; + gint64 int_part=0; + double frac_part; + gint64 frac_int=0; + double logval; + double sigfigs; + + if((denom == GNC_DENOM_AUTO) && (how & GNC_DENOM_SIGFIG)) { + if(fabs(in) < 10e-20) { + logval = 0; + } + else { + logval = log10(fabs(in)); + logval = ((logval > 0.0) ? + (floor(logval)+1.0) : (ceil(logval))); + } + sigfigs = GNC_NUMERIC_GET_SIGFIGS(how); + if(sigfigs-logval >= 0) { + denom = (gint64)(pow(10, sigfigs-logval)); + } + else { + denom = -((gint64)(pow(10, logval-sigfigs))); + } + + how = how & ~GNC_DENOM_SIGFIG & ~GNC_NUMERIC_SIGFIGS_MASK; + } + + int_part = (gint64)(floor(fabs(in))); + frac_part = in - (double)int_part; + + int_part = int_part * denom; + frac_part = frac_part * (double)denom; + + switch(how & GNC_NUMERIC_RND_MASK) { + case GNC_RND_FLOOR: + frac_int = (gint64)floor(frac_part); + break; + + case GNC_RND_CEIL: + frac_int = (gint64)ceil(frac_part); + break; + + case GNC_RND_TRUNC: + frac_int = (gint64)frac_part; + break; + + case GNC_RND_ROUND: + case GNC_RND_ROUND_HALF_UP: + frac_int = (gint64)rint(frac_part); + break; + + case GNC_RND_NEVER: + frac_int = (gint64)floor(frac_part); + if(frac_part != (double) frac_int) { + /* signal an error */ + } + break; + } + + out.num = int_part + frac_int; + out.denom = denom; + return out; +} + +/******************************************************************** + * gnc_numeric_to_double + ********************************************************************/ + +double +gnc_numeric_to_double(gnc_numeric in) { + if(in.denom >= 0) { + return (double)in.num/(double)in.denom; + } + else { + return (double)(in.num * in.denom); + } +} + + +/******************************************************************** + * gnc_numeric_create + ********************************************************************/ + +gnc_numeric +gnc_numeric_create(gint64 num, gint64 denom) { + gnc_numeric out; + out.num = num; + out.denom = denom; + return out; +} + + +/******************************************************************** + * gnc_numeric_error + ********************************************************************/ + +gnc_numeric +gnc_numeric_error(int error_code) { + if(abs(error_code) < 5) { + /* PERR("%s", _numeric_error_strings[ - error_code]); */ + } + return gnc_numeric_create(error_code, 0LL); +} + + +/******************************************************************** + * gnc_numeric_zero + ********************************************************************/ + +gnc_numeric +gnc_numeric_zero(void) { + return gnc_numeric_create(0LL, 1LL); +} + + +/******************************************************************** + * gnc_numeric_num + ********************************************************************/ + +gint64 +gnc_numeric_num(gnc_numeric a) { + return a.num; +} + + +/******************************************************************** + * gnc_numeric_denom + ********************************************************************/ + +gint64 +gnc_numeric_denom(gnc_numeric a) { + return a.denom; +} + + +/******************************************************************** + * gnc_numeric_add_with_error + ********************************************************************/ + +gnc_numeric +gnc_numeric_add_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error) { + + gnc_numeric sum = gnc_numeric_add(a, b, denom, how); + gnc_numeric exact = gnc_numeric_add(a, b, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + gnc_numeric err = gnc_numeric_sub(sum, exact, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + + if(error) { + *error = err; + } + return sum; +} + +/******************************************************************** + * gnc_numeric_sub_with_error + ********************************************************************/ + +gnc_numeric +gnc_numeric_sub_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error) { + + gnc_numeric diff = gnc_numeric_sub(a, b, denom, how); + gnc_numeric exact = gnc_numeric_sub(a, b, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + gnc_numeric err = gnc_numeric_sub(diff, exact, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + if(error) { + *error = err; + } + return diff; +} + + +/******************************************************************** + * gnc_numeric_mul_with_error + ********************************************************************/ + +gnc_numeric +gnc_numeric_mul_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error) { + + gnc_numeric prod = gnc_numeric_mul(a, b, denom, how); + gnc_numeric exact = gnc_numeric_mul(a, b, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + gnc_numeric err = gnc_numeric_sub(prod, exact, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + if(error) { + *error = err; + } + return prod; +} + + +/******************************************************************** + * gnc_numeric_div_with_error + ********************************************************************/ + +gnc_numeric +gnc_numeric_div_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error) { + + gnc_numeric quot = gnc_numeric_div(a, b, denom, how); + gnc_numeric exact = gnc_numeric_div(a, b, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE); + gnc_numeric err = gnc_numeric_sub(quot, exact, + GNC_DENOM_AUTO, GNC_DENOM_REDUCE); + if(error) { + *error = err; + } + return quot; +} + +int +gnc_numeric_check(gnc_numeric in) { + if(in.denom != 0) { + return GNC_ERROR_OK; + } + else if(in.num) { + return in.num; + } + else { + return GNC_ERROR_ARG; + } +} + +/******************************************************************** + * gnc_numeric text IO + ********************************************************************/ + +gchar * +gnc_numeric_to_string(gnc_numeric n) { + gchar *result; + long long int tmpnum = n.num; + long long int tmpdenom = n.denom; + + result = g_strdup_printf("%lld/%lld", tmpnum, tmpdenom); + + return result; +} + +const gchar * +string_to_gnc_numeric(const gchar* str, gnc_numeric *n) { + int num_read; + long long int tmpnum; + long long int tmpdenom; + + if(!str) return NULL; + + /* must use "<" here because %n's effects aren't well defined */ + if(sscanf(str, " " GNC_SCANF_LLD "/" GNC_SCANF_LLD "%n", + &tmpnum, &tmpdenom, &num_read) < 2) { + return(NULL); + } + n->num = tmpnum; + n->denom = tmpdenom; + return(str + num_read); +} + +#ifdef _GNC_NUMERIC_TEST + +static char * +gnc_numeric_print(gnc_numeric in) { + char * retval; + if(gnc_numeric_check(in)) { + retval = g_strdup_printf(" [%lld / %lld]", + (long long int) in.num, + (long long int) in.denom); + } + else { + retval = g_strdup_printf("[%lld / %lld]", + (long long int) in.num, + (long long int) in.denom); + } + return retval; +} + +int +main(int argc, char ** argv) { + gnc_numeric a = gnc_numeric_create(1, 3); + gnc_numeric b = gnc_numeric_create(1, 4); + gnc_numeric c; + gnc_numeric d = gnc_numeric_create(1, 2); + + gnc_numeric err; + int i; + + printf("add exact : %s + %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_add(a, b, + GNC_DENOM_AUTO, + GNC_DENOM_EXACT))); + + + printf("add least : %s + %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_add(a, b, + GNC_DENOM_AUTO, + GNC_DENOM_REDUCE))); + + printf("add 100ths (banker's): %s + %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_add(a, b, 100, + GNC_RND_ROUND))); + + c = gnc_numeric_add_with_error(a, b, 100, GNC_RND_ROUND, &err); + printf("add 100ths/error : %s + %s = %s + (error) %s\n\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(c), + gnc_numeric_print(err)); + + printf("sub exact : %s - %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_sub(a, b, GNC_DENOM_AUTO, + GNC_DENOM_EXACT))); + + printf("sub least : %s - %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_sub(a, b, + GNC_DENOM_AUTO, + GNC_DENOM_REDUCE))); + + printf("sub 100ths : %s - %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_sub(a, b, 100, + GNC_RND_ROUND))); + + c = gnc_numeric_sub_with_error(a, b, 100, GNC_RND_FLOOR, &err); + printf("sub 100ths/error : %s - %s = %s + (error) %s\n\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(c), + gnc_numeric_print(err)); + + printf("mul exact : %s * %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_mul(a, b, GNC_DENOM_AUTO, + GNC_DENOM_EXACT))); + + printf("mul least : %s * %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_mul(a, b, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE))); + + printf("mul 100ths : %s * %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_mul(a, b, 100, + GNC_RND_ROUND))); + + c = gnc_numeric_mul_with_error(a, b, 100, GNC_RND_ROUND, &err); + printf("mul 100ths/error : %s * %s = %s + (error) %s\n\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(c), + gnc_numeric_print(err)); + + printf("div exact : %s / %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_div(a, b, GNC_DENOM_AUTO, + GNC_DENOM_EXACT))); + + printf("div least : %s / %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_div(a, b, GNC_DENOM_AUTO, + GNC_DENOM_REDUCE))); + + printf("div 100ths : %s / %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_div(a, b, 100, + GNC_RND_ROUND))); + + c = gnc_numeric_div_with_error(a, b, 100, GNC_RND_ROUND, &err); + printf("div 100ths/error : %s / %s = %s + (error) %s\n\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(c), + gnc_numeric_print(err)); + + printf("7/16 as float: %e\n", + gnc_numeric_to_double(gnc_numeric_create(7, 16))); + + printf("7/16 as 100ths (floor): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(7, 16), + 100, GNC_RND_FLOOR))); + printf("7/16 as 100ths (ceil): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(7, 16), + 100, GNC_RND_CEIL))); + printf("7/16 as 100ths (trunc): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(7, 16), + 100, GNC_RND_TRUNC))); + printf("7/16 as 100ths (round): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(7, 16), + 100, GNC_RND_ROUND))); + + printf("1511/1000 as 1/100 (round): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(1511, 1000), + 100, GNC_RND_ROUND))); + printf("1516/1000 as 1/100 (round): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(1516, 1000), + 100, GNC_RND_ROUND))); + printf("1515/1000 as 1/100 (round): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(1515, 1000), + 100, GNC_RND_ROUND))); + printf("1525/1000 as 1/100 (round): %s\n", + gnc_numeric_print(gnc_numeric_convert(gnc_numeric_create(1525, 1000), + 100, GNC_RND_ROUND))); + + printf("100023234 / 334216654 reduced: %s\n", + gnc_numeric_print(gnc_numeric_reduce(gnc_numeric_create(10023234LL, + 334216654LL)))); + printf("2^10*3^10*17^2 / 2^8*3^12 reduced: %s\n", + gnc_numeric_print + (gnc_numeric_reduce(gnc_numeric_create(17474724864LL, + 136048896LL)))); + printf("1024 / 1024^4 reduced: %s\n", + gnc_numeric_print + (gnc_numeric_reduce(gnc_numeric_create(1024LL, + 1099511627776LL)))); + printf("reducing 100,000 times:\n\n"); + for(i = 0; i < 100000; i++) { + gnc_numeric_reduce(gnc_numeric_create(17474724864LL, + 136048896LL)); + } + + printf("add LCM: %s + %s = %s\n", + gnc_numeric_print(b), gnc_numeric_print(d), + gnc_numeric_print(gnc_numeric_add(b, d, GNC_DENOM_AUTO, + GNC_DENOM_LCD))); + + printf("float to 6 sigfigs: %s\n", + gnc_numeric_print(double_to_gnc_numeric(1.1234567890123, + GNC_DENOM_AUTO, + GNC_DENOM_SIGFIGS(6) | + GNC_RND_ROUND))); + printf("float to 6 sigfigs: %s\n", + gnc_numeric_print(double_to_gnc_numeric(.011234567890123, + GNC_DENOM_AUTO, + GNC_DENOM_SIGFIGS(6) | + GNC_RND_ROUND))); + printf("float to 6 sigfigs: %s\n", + gnc_numeric_print(double_to_gnc_numeric(1123.4567890123, + GNC_DENOM_AUTO, + GNC_DENOM_SIGFIGS(6) | + GNC_RND_ROUND))); + printf("float to 6 sigfigs: %s\n", + gnc_numeric_print(double_to_gnc_numeric(1.1234567890123e-5, + GNC_DENOM_AUTO, + GNC_DENOM_SIGFIGS(6) | + GNC_RND_ROUND))); + printf("add to 4 sigfigs: %s + %s = %s\n", + gnc_numeric_print(a), gnc_numeric_print(b), + gnc_numeric_print(gnc_numeric_add(a, b, + GNC_DENOM_AUTO, + GNC_DENOM_SIGFIGS(4) | + GNC_RND_ROUND))); + + + return 0; +} +#endif diff --git a/src/engine/gnc-numeric.h b/src/engine/gnc-numeric.h new file mode 100644 index 0000000000..313a506aad --- /dev/null +++ b/src/engine/gnc-numeric.h @@ -0,0 +1,167 @@ +/******************************************************************** + * gnc-numeric.h -- an exact-number library for gnucash. * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + *******************************************************************/ + +#ifndef GNC_NUMERIC_H +#define GNC_NUMERIC_H + +#include + +struct _gnc_numeric { + gint64 num; + gint64 denom; +}; + +typedef struct _gnc_numeric gnc_numeric; + +/* bitmasks for HOW flags */ +#define GNC_NUMERIC_RND_MASK 0x0000000f +#define GNC_NUMERIC_DENOM_MASK 0x000000f0 +#define GNC_NUMERIC_SIGFIGS_MASK 0x0000ff00 + +/* rounding/truncation modes for operations */ +enum { + GNC_RND_FLOOR = 0x01, + GNC_RND_CEIL = 0x02, + GNC_RND_TRUNC = 0x03, + GNC_RND_PROMOTE = 0x04, + GNC_RND_ROUND_HALF_DOWN = 0x05, + GNC_RND_ROUND_HALF_UP = 0x06, + GNC_RND_ROUND = 0x07, + GNC_RND_NEVER = 0x08 +}; + +/* auto-denominator types */ +enum { + GNC_DENOM_EXACT = 0x10, + GNC_DENOM_REDUCE = 0x20, + GNC_DENOM_LCD = 0x30, + GNC_DENOM_FIXED = 0x40, + GNC_DENOM_SIGFIG = 0x50 +}; + +/* bits 8-15 of 'how' are reserved for the number of significant + * digits to use in the output with GNC_DENOM_SIGFIG */ + +/* errors */ +enum { + GNC_ERROR_OK = 0, + GNC_ERROR_ARG = -1, + GNC_ERROR_OVERFLOW = -2, + GNC_ERROR_DENOM_DIFF = -3, + GNC_ERROR_REMAINDER = -4 +}; + +#define GNC_DENOM_AUTO 0 + +#define GNC_DENOM_RECIPROCAL( a ) (- ( a )) +#define GNC_DENOM_SIGFIGS( a ) ( ((( a ) & 0xff) << 8) | GNC_DENOM_SIGFIG) +#define GNC_NUMERIC_GET_SIGFIGS( a ) ( (( a ) & 0xff00 ) >> 8) + +/* make a gnc_numeric from numerator and denominator */ +gnc_numeric gnc_numeric_create(gint64 num, gint64 denom); + +/* create a zero-value gnc_numeric */ +gnc_numeric gnc_numeric_zero(void); + +/* make a special error-signalling gnc_numeric */ +gnc_numeric gnc_numeric_error(int error_code); + +/* check for error signal in value */ +int gnc_numeric_check(gnc_numeric a); + +/* get parts */ +gint64 gnc_numeric_num(gnc_numeric a); +gint64 gnc_numeric_denom(gnc_numeric a); + +/* tests */ +int gnc_numeric_zero_p(gnc_numeric a); /* 1 if 0, 0 else */ +int gnc_numeric_compare(gnc_numeric a, gnc_numeric b); +int gnc_numeric_negative_p(gnc_numeric a); +int gnc_numeric_positive_p(gnc_numeric a); + +/* equivalence predicates : + * eq : a and b are exactly the same (same numerator and denominator) + * equal : a and b represent exactly the same number (ratio of numerator + * to denominator is exactly equal) + * same : after both are converted to DENOM using method HOW, a and b + * are equal(). + */ +int gnc_numeric_eq(gnc_numeric a, gnc_numeric b); +int gnc_numeric_equal(gnc_numeric a, gnc_numeric b); +int gnc_numeric_same(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how); + +/* arithmetic operations */ +gnc_numeric gnc_numeric_add(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how); +gnc_numeric gnc_numeric_sub(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how); +gnc_numeric gnc_numeric_mul(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how); +gnc_numeric gnc_numeric_div(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how); +gnc_numeric gnc_numeric_neg(gnc_numeric a); +gnc_numeric gnc_numeric_abs(gnc_numeric a); + +/* some shortcuts for common operations */ +gnc_numeric gnc_numeric_add_fixed(gnc_numeric a, gnc_numeric b); +gnc_numeric gnc_numeric_sub_fixed(gnc_numeric a, gnc_numeric b); + +/* arithmetic functions with exact error returns */ +gnc_numeric gnc_numeric_add_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error); +gnc_numeric gnc_numeric_sub_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error); +gnc_numeric gnc_numeric_mul_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error); +gnc_numeric gnc_numeric_div_with_error(gnc_numeric a, gnc_numeric b, + gint64 denom, gint how, + gnc_numeric * error); + +/* change the denominator of a gnc_numeric value */ +gnc_numeric gnc_numeric_convert(gnc_numeric in, gint64 denom, + gint how); + +gnc_numeric gnc_numeric_convert_with_error(gnc_numeric in, gint64 denom, + gint how, + gnc_numeric * error); + +/* reduce by GCF elimination */ +gnc_numeric gnc_numeric_reduce(gnc_numeric in); + +/* convert to and from floating-point values */ +gnc_numeric double_to_gnc_numeric(double in, gint64 denom, + gint how); +double gnc_numeric_to_double(gnc_numeric in); + +gchar *gnc_numeric_to_string(gnc_numeric n); + +/* Read a gnc_numeric from str, skipping any leading whitespace, and + returning a pointer to just past the last byte read. Return NULL + on error. */ +const gchar *string_to_gnc_numeric(const gchar* str, gnc_numeric *n); + +#endif diff --git a/src/engine/gnc-pricedb-p.h b/src/engine/gnc-pricedb-p.h new file mode 100644 index 0000000000..f4f0063727 --- /dev/null +++ b/src/engine/gnc-pricedb-p.h @@ -0,0 +1,89 @@ +/******************************************************************** + * gnc-pricedb-p.h -- a simple price database for gnucash. * + * Copyright (C) 2001 Rob Browning * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + *******************************************************************/ + +#ifndef GNC_PRICEDB_P_H +#define GNC_PRICEDB_P_H + +#include + +#include "gnc-book.h" +#include "BackendP.h" +#include "gnc-pricedb.h" + +struct _GNCPrice { + /* 'public' data fields */ + GUID guid; /* globally unique price id */ + GNCPriceDB *db; + gnc_commodity *commodity; + gnc_commodity *currency; + Timespec time; + char *source; + char *type; + gnc_numeric value; + gint32 version; /* version number, for syncing with backend */ + guint32 version_check; /* data aging timestamp */ + + /* 'private' object management fields */ + guint32 refcount; /* garbage collection reference count */ + gint32 editlevel; /* nesting level of begin/end edit calls */ + gboolean not_saved; /* price edit saved flag */ + gboolean do_free; /* price is going to be destroyed soon */ +}; + + + +struct _GNCPriceDB { + GHashTable *commodity_hash; + Backend *backend; + gboolean dirty; +}; + +/* These structs define the kind of price lookup being done + * so that it can be passed to the backend. This is a rather + * cheesy, low-brow interface. It could stand improvement. + */ +typedef enum { + LOOKUP_LATEST = 1, + LOOKUP_ALL, + LOOKUP_AT_TIME, + LOOKUP_NEAREST_IN_TIME, + LOOKUP_LATEST_BEFORE, + LOOKUP_EARLIEST_AFTER +} PriceLookupType; + + +struct _GNCPriceLookup { + PriceLookupType type; + GNCPriceDB *prdb; + gnc_commodity *commodity; + gnc_commodity *currency; + Timespec date; +}; + +void gnc_pricedb_mark_clean(GNCPriceDB *db); +void gnc_pricedb_substitute_commodity(GNCPriceDB *db, + gnc_commodity *old_c, + gnc_commodity *new_c); +void gnc_price_set_guid (GNCPrice *p, const GUID *guid); + +#endif diff --git a/src/engine/gnc-pricedb.c b/src/engine/gnc-pricedb.c new file mode 100644 index 0000000000..dc4016a95e --- /dev/null +++ b/src/engine/gnc-pricedb.c @@ -0,0 +1,1257 @@ +/******************************************************************** + * gnc-pricedb.c -- a simple price database for gnucash. * + * Copyright (C) 2001 Rob Browning, Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + *******************************************************************/ + +#include "config.h" + +#include +#include + +#include "Backend.h" +#include "GNCId.h" +#include "GNCIdP.h" +#include "gnc-engine.h" +#include "gnc-engine-util.h" +#include "gnc-event.h" +#include "gnc-event-p.h" +#include "gnc-pricedb.h" +#include "gnc-pricedb-p.h" +#include "guid.h" + +/* This static indicates the debugging module that this .o belongs to. */ +static short module = MOD_PRICE; + +static gboolean add_price(GNCPriceDB *db, GNCPrice *p); +static gboolean remove_price(GNCPriceDB *db, GNCPrice *p, gboolean cleanup); + +/* ==================================================================== */ +/* GNCPrice functions + */ + +/* allocation */ +GNCPrice * +gnc_price_create(void) +{ + GNCPrice *p = g_new0(GNCPrice, 1); + ENTER(" "); + p->refcount = 1; + p->editlevel = 0; + p->not_saved = FALSE; + p->do_free = FALSE; + p->version = 0; + p->version_check = 0; + xaccGUIDNew (&p->guid); + xaccStoreEntity(p, &p->guid, GNC_ID_PRICE); + gnc_engine_generate_event (&p->guid, GNC_EVENT_CREATE); + return p; +} + +static void +gnc_price_destroy (GNCPrice *p) +{ + ENTER(" "); + gnc_engine_generate_event (&p->guid, GNC_EVENT_DESTROY); + xaccRemoveEntity(&p->guid); + + if(p->type) g_cache_remove(gnc_engine_get_string_cache(), p->type); + if(p->source) g_cache_remove(gnc_engine_get_string_cache(), p->source); + + memset(p, 0, sizeof(GNCPrice)); + g_free(p); +} + +void +gnc_price_ref(GNCPrice *p) +{ + if(!p) return; + p->refcount++; +} + +void +gnc_price_unref(GNCPrice *p) +{ + if(!p) return; + ENTER("pr=%p refcount=%d", p, p->refcount); + if(p->refcount == 0) { + PERR("refcount == 0 !!!!"); + assert(p->refcount != 0); + } + + p->refcount--; + + if(p->refcount <= 0) { + if (NULL != p->db) { + PERR("last unref while price in database"); + } + gnc_price_destroy (p); + } +} + +/* ==================================================================== */ + +GNCPrice * +gnc_price_clone(GNCPrice* p) +{ + /* the clone doesn't belong to a PriceDB */ + GNCPrice *new_p; + + ENTER ("pr=%p", p); + if(!p) return NULL; + new_p = gnc_price_create(); + if(!new_p) return NULL; + + new_p->version = p->version; + + gnc_price_begin_edit(new_p); + /* never ever clone guid's */ + gnc_price_set_commodity(new_p, gnc_price_get_commodity(p)); + gnc_price_set_time(new_p, gnc_price_get_time(p)); + gnc_price_set_source(new_p, gnc_price_get_source(p)); + gnc_price_set_type(new_p, gnc_price_get_type(p)); + gnc_price_set_value(new_p, gnc_price_get_value(p)); + gnc_price_set_currency(new_p, gnc_price_get_currency(p)); + gnc_price_commit_edit(new_p); + + return(new_p); +} + +/* ==================================================================== */ + +void +gnc_price_begin_edit (GNCPrice *p) +{ + if (!p) return; + p->editlevel++; + if (1 < p->editlevel) return; + ENTER ("pr=%p, not-saved=%d do-free=%d", p, p->not_saved, p->do_free); + + if (0 >= p->editlevel) + { + PERR ("unbalanced call - resetting (was %d)", p->editlevel); + p->editlevel = 1; + } + + /* See if there's a backend. If there is, invoke it. */ + /* We may not be able to find the backend, so make not of that .. */ + if (p->db) { + Backend *be; + be = xaccPriceDBGetBackend (p->db); + if (be && be->price_begin_edit) { + (be->price_begin_edit) (be, p); + } + p->not_saved = FALSE; + } else { + p->not_saved = TRUE; + } + + LEAVE ("pr=%p, not-saved=%d do-free=%d", p, p->not_saved, p->do_free); +} + +void +gnc_price_commit_edit (GNCPrice *p) +{ + if (!p) return; + + p->editlevel--; + if (0 < p->editlevel) return; + + ENTER ("pr=%p, not-saved=%d do-free=%d", p, p->not_saved, p->do_free); + if (0 > p->editlevel) + { + PERR ("unbalanced call - resetting (was %d)", p->editlevel); + p->editlevel = 0; + } + + /* See if there's a backend. If there is, invoke it. */ + /* We may not be able to find the backend, so make not of that .. */ + if (p->db) { + Backend *be; + be = xaccPriceDBGetBackend (p->db); + if (be && be->price_commit_edit) { + GNCBackendError errcode; + + /* clear errors */ + do { + errcode = xaccBackendGetError (be); + } while (ERR_BACKEND_NO_ERR != errcode); + + /* if we haven't been able to call begin edit before, call it now */ + if (TRUE == p->not_saved) { + if (be->price_begin_edit) { + (be->price_begin_edit) (be, p); + } + } + + (be->price_commit_edit) (be, p); + errcode = xaccBackendGetError (be); + if (ERR_BACKEND_NO_ERR != errcode) + { + /* XXX hack alert FIXME implement price rollback */ + PERR (" backend asked engine to rollback, but this isn't" + " handled yet. Return code=%d", errcode); + + /* push error back onto the stack */ + xaccBackendSetError (be, errcode); + } + } + p->not_saved = FALSE; + } else { + p->not_saved = TRUE; + } + LEAVE ("pr=%p, not-saved=%d do-free=%d", p, p->not_saved, p->do_free); +} + +/* ==================================================================== */ +/* setters */ + +void +gnc_price_set_guid (GNCPrice *p, const GUID *guid) +{ + if (!p || !guid) return; + xaccRemoveEntity (&p->guid); + p->guid = *guid; + if(p->db) p->db->dirty = TRUE; + xaccStoreEntity(p, &p->guid, GNC_ID_PRICE); +} + +void +gnc_price_set_commodity(GNCPrice *p, gnc_commodity *c) +{ + if(!p) return; + + if(!gnc_commodity_equiv(p->commodity, c)) + { + /* Changing the commodity requires the hash table + * position to be modified. The easiest way of doing + * this is to remove and reinsert. */ + gnc_price_ref (p); + remove_price (p->db, p, TRUE); + gnc_price_begin_edit (p); + p->commodity = c; + if(p->db) p->db->dirty = TRUE; + gnc_price_commit_edit (p); + add_price (p->db, p); + gnc_price_unref (p); + } +} + + +void +gnc_price_set_currency(GNCPrice *p, gnc_commodity *c) +{ + if(!p) return; + + if(!gnc_commodity_equiv(p->currency, c)) + { + /* Changing the currency requires the hash table + * position to be modified. The easiest way of doing + * this is to remove and reinsert. */ + gnc_price_ref (p); + remove_price (p->db, p, TRUE); + gnc_price_begin_edit (p); + p->currency = c; + if(p->db) p->db->dirty = TRUE; + gnc_price_commit_edit (p); + add_price (p->db, p); + gnc_price_unref (p); + } +} + +void +gnc_price_set_time(GNCPrice *p, Timespec t) +{ + if(!p) return; + if(!timespec_equal(&(p->time), &t)) + { + /* Changing the datesatamp requires the hash table + * position to be modified. The easiest way of doing + * this is to remove and reinsert. */ + remove_price (p->db, p, FALSE); + gnc_price_begin_edit (p); + p->time = t; + if(p->db) p->db->dirty = TRUE; + gnc_price_commit_edit (p); + add_price (p->db, p); + } +} + +void +gnc_price_set_source(GNCPrice *p, const char *s) +{ + if(!p) return; + if(safe_strcmp(p->source, s) != 0) + { + GCache *cache; + char *tmp; + + gnc_price_begin_edit (p); + cache = gnc_engine_get_string_cache(); + tmp = g_cache_insert(cache, (gpointer) s); + if(p->source) g_cache_remove(cache, p->source); + p->source = tmp; + if(p->db) p->db->dirty = TRUE; + gnc_price_commit_edit (p); + } +} + +void +gnc_price_set_type(GNCPrice *p, const char* type) +{ + if(!p) return; + if(safe_strcmp(p->type, type) != 0) + { + GCache *cache; + gchar *tmp; + + gnc_price_begin_edit (p); + cache = gnc_engine_get_string_cache(); + tmp = g_cache_insert(cache, (gpointer) type); + if(p->type) g_cache_remove(cache, p->type); + p->type = tmp; + if(p->db) p->db->dirty = TRUE; + gnc_price_commit_edit (p); + } +} + +void +gnc_price_set_value(GNCPrice *p, gnc_numeric value) +{ + if(!p) return; + if(!gnc_numeric_eq(p->value, value)) + { + gnc_price_begin_edit (p); + p->value = value; + if(p->db) p->db->dirty = TRUE; + gnc_price_commit_edit (p); + } +} + +void +gnc_price_set_version(GNCPrice *p, gint32 vers) +{ + /* begin/end edit is inappropriate here, this is a backend thing only. */ + if(!p) return; + p->version = vers; +} + + +/* ==================================================================== */ +/* getters */ + +GNCPrice * +gnc_price_lookup (const GUID *guid) +{ + if (!guid) return NULL; + return xaccLookupEntity (guid, GNC_ID_PRICE); +} + +const GUID * +gnc_price_get_guid (GNCPrice *p) +{ + if (!p) return xaccGUIDNULL(); + return &p->guid; +} + +gnc_commodity * +gnc_price_get_commodity(GNCPrice *p) +{ + if(!p) return NULL; + return p->commodity; +} + +Timespec +gnc_price_get_time(GNCPrice *p) +{ + if(!p) { + Timespec result; + result.tv_sec = 0; + result.tv_nsec = 0; + return result; + } + return p->time; +} + +const char * +gnc_price_get_source(GNCPrice *p) +{ + if(!p) return NULL; + return p->source; +} + +const char * +gnc_price_get_type(GNCPrice *p) +{ + if(!p) return NULL; + return p->type; +} + +gnc_numeric +gnc_price_get_value(GNCPrice *p) +{ + if(!p) { + PERR("price NULL.\n"); + return gnc_numeric_zero(); + } + return p->value; +} + +gnc_commodity * +gnc_price_get_currency(GNCPrice *p) +{ + if(!p) return NULL; + return p->currency; +} + +gint32 +gnc_price_get_version(GNCPrice *p) +{ + if(!p) return 0; + return (p->version); +} + +gboolean +gnc_price_equal (GNCPrice *p1, GNCPrice *p2) +{ + Timespec ts1; + Timespec ts2; + + if (p1 == p2) return TRUE; + if (!p1 || !p2) return FALSE; + + if (!gnc_commodity_equiv (gnc_price_get_commodity (p1), + gnc_price_get_commodity (p2))) + return FALSE; + + if (!gnc_commodity_equiv (gnc_price_get_currency (p1), + gnc_price_get_currency (p2))) + return FALSE; + + ts1 = gnc_price_get_time (p1); + ts2 = gnc_price_get_time (p2); + + if (!timespec_equal (&ts1, &ts2)) + return FALSE; + + if (!safe_strcmp (gnc_price_get_source (p1), + gnc_price_get_source (p2))) + return FALSE; + + if (!safe_strcmp (gnc_price_get_type (p1), + gnc_price_get_type (p2))) + return FALSE; + + if (!gnc_numeric_eq (gnc_price_get_value (p1), + gnc_price_get_value (p2))) + return FALSE; + + return TRUE; +} + +/* ==================================================================== */ +/* price list manipulation functions */ + +static gint +compare_prices_by_date(gconstpointer a, gconstpointer b) +{ + Timespec time_a; + Timespec time_b; + gint result; + + if(!a && !b) return 0; + /* nothing is always less than something */ + if(!a) return -1; + + time_a = gnc_price_get_time((GNCPrice *) a); + time_b = gnc_price_get_time((GNCPrice *) b); + + result = -timespec_cmp(&time_a, &time_b); + if (result) return result; + + /* For a stable sort */ + return guid_compare (gnc_price_get_guid((GNCPrice *) a), + gnc_price_get_guid((GNCPrice *) b)); +} + +gboolean +gnc_price_list_insert(GList **prices, GNCPrice *p) +{ + GList *result_list; + + if(!prices || !p) return FALSE; + gnc_price_ref(p); + result_list = g_list_insert_sorted(*prices, p, compare_prices_by_date); + if(!result_list) return FALSE; + *prices = result_list; + return TRUE; +} + +gboolean +gnc_price_list_remove(GList **prices, GNCPrice *p) +{ + GList *result_list; + GList *found_element; + + if(!prices || !p) return FALSE; + + found_element = g_list_find(*prices, p); + if(!found_element) return TRUE; + + result_list = g_list_remove_link(*prices, found_element); + gnc_price_unref((GNCPrice *) found_element->data); + g_list_free(found_element); + + *prices = result_list; + return TRUE; +} + +static void +price_list_destroy_helper(gpointer data, gpointer user_data) +{ + gnc_price_unref((GNCPrice *) data); +} + +void +gnc_price_list_destroy(GList *prices) +{ + g_list_foreach(prices, price_list_destroy_helper, NULL); + g_list_free(prices); +} + +/* ==================================================================== */ +/* GNCPriceDB functions + + Structurally a GNCPriceDB contains a hash mapping price commodities + (of type gnc_commodity*) to hashes mapping price currencies (of + type gnc_commodity*) to GNCPrice lists (see gnc-pricedb.h for a + description of GNCPrice lists). The top-level key is the commodity + you want the prices for, and the second level key is the commodity + that the value is expressed in terms of. + + */ + +GNCPriceDB * +gnc_pricedb_create(void) +{ + GNCPriceDB * result = g_new0(GNCPriceDB, 1); + result->backend = NULL; + result->commodity_hash = g_hash_table_new(g_direct_hash, g_direct_equal); + g_return_val_if_fail (result->commodity_hash, NULL); + return result; +} + +static void +destroy_pricedb_currency_hash_data(gpointer key, + gpointer data, + gpointer user_data) +{ + GList *price_list = (GList *) data; + GList *node; + + for (node = price_list; node; node = node->next) + { + GNCPrice *p = node->data; + + p->db = NULL; + } + + gnc_price_list_destroy(price_list); +} + +static void +destroy_pricedb_commodity_hash_data(gpointer key, + gpointer data, + gpointer user_data) +{ + GHashTable *currency_hash = (GHashTable *) data; + if (!currency_hash) return; + g_hash_table_foreach (currency_hash, + destroy_pricedb_currency_hash_data, + NULL); + g_hash_table_destroy(currency_hash); +} + +void +gnc_pricedb_destroy(GNCPriceDB *db) +{ + if(!db) return; + g_hash_table_foreach (db->commodity_hash, + destroy_pricedb_commodity_hash_data, + NULL); + g_hash_table_destroy (db->commodity_hash); + db->commodity_hash = NULL; + db->backend = NULL; + g_free(db); +} + +/* ==================================================================== */ + +gboolean +gnc_pricedb_dirty(GNCPriceDB *p) +{ + if(!p) return FALSE; + return p->dirty; +} + +void +gnc_pricedb_mark_clean(GNCPriceDB *p) +{ + if(!p) return; + p->dirty = FALSE; +} + +/* ==================================================================== */ + +static gboolean +num_prices_helper (GNCPrice *p, gpointer user_data) +{ + guint *count = user_data; + + *count += 1; + + return TRUE; +} + +guint +gnc_pricedb_get_num_prices(GNCPriceDB *db) +{ + guint count; + + if (!db) return 0; + + count = 0; + + gnc_pricedb_foreach_price(db, num_prices_helper, &count, FALSE); + + return count; +} + +/* ==================================================================== */ +/* The add_price() function is a utility that only manages the + * dual hash table instertion */ + +static gboolean +add_price(GNCPriceDB *db, GNCPrice *p) +{ + /* this function will use p, adding a ref, so treat p as read-only + if this function succeeds. */ + GList *price_list; + gnc_commodity *commodity; + gnc_commodity *currency; + GHashTable *currency_hash; + + if(!db || !p) return FALSE; + ENTER ("db=%p, pr=%p not-saved=%d do-free=%d", db, p, p->not_saved, p->do_free); + commodity = gnc_price_get_commodity(p); + if(!commodity) { + PWARN("no commodity"); + return FALSE; + } + currency = gnc_price_get_currency(p); + if(!currency) { + PWARN("no currency"); + return FALSE; + } + if(!db->commodity_hash) return FALSE; + + currency_hash = g_hash_table_lookup(db->commodity_hash, commodity); + if(!currency_hash) { + currency_hash = g_hash_table_new(g_direct_hash, g_direct_equal); + g_hash_table_insert(db->commodity_hash, commodity, currency_hash); + } + + price_list = g_hash_table_lookup(currency_hash, currency); + if(!gnc_price_list_insert(&price_list, p)) return FALSE; + if(!price_list) return FALSE; + g_hash_table_insert(currency_hash, currency, price_list); + p->db = db; + + LEAVE ("db=%p, pr=%p not-saved=%d do-free=%d", db, p, p->not_saved, p->do_free); + return TRUE; +} + +/* the gnc_pricedb_add_price() function will use p, adding a ref, so treat p as read-only + if this function succeeds. (Huh ???) */ +gboolean +gnc_pricedb_add_price(GNCPriceDB *db, GNCPrice *p) +{ + + if(!db || !p) return FALSE; + ENTER ("db=%p, pr=%p not-saved=%d do-free=%d", db, p, p->not_saved, p->do_free); + + if (FALSE == add_price(db, p)) return FALSE; + + /* if we haven't been able to call the backend before, call it now */ + if (TRUE == p->not_saved) { + gnc_price_begin_edit(p); + db->dirty = TRUE; + gnc_price_commit_edit(p); + } + LEAVE ("db=%p, pr=%p not-saved=%d do-free=%d", db, p, p->not_saved, p->do_free); + return TRUE; +} + +/* remove_price() is a utility; its only function is to remove the price + * from the double-hash tables. + */ + +static gboolean +remove_price(GNCPriceDB *db, GNCPrice *p, gboolean cleanup) +{ + GList *price_list; + gnc_commodity *commodity; + gnc_commodity *currency; + GHashTable *currency_hash; + + if(!db || !p) return FALSE; + ENTER ("db=%p, pr=%p not-saved=%d do-free=%d", db, p, p->not_saved, p->do_free); + commodity = gnc_price_get_commodity(p); + if(!commodity) return FALSE; + currency = gnc_price_get_currency(p); + if(!currency) return FALSE; + if(!db->commodity_hash) return FALSE; + + currency_hash = g_hash_table_lookup(db->commodity_hash, commodity); + if(!currency_hash) return FALSE; + + price_list = g_hash_table_lookup(currency_hash, currency); + gnc_price_ref(p); + if(!gnc_price_list_remove(&price_list, p)) { + gnc_price_unref(p); + return FALSE; + } + + /* if the price list is empty, then remove this currency from the commodity hash */ + if(price_list) { + g_hash_table_insert(currency_hash, currency, price_list); + } else { + g_hash_table_remove(currency_hash, currency); + + if (cleanup) { + /* chances are good that this commodity had only one currency ... + * if there are no currencies, we may as well destroy the commodity too. */ + guint num_currencies = g_hash_table_size (currency_hash); + if (0 == num_currencies) { + g_hash_table_remove (db->commodity_hash, commodity); + g_hash_table_destroy (currency_hash); + } + } + } + + gnc_price_unref(p); + LEAVE ("db=%p, pr=%p", db, p); + return TRUE; +} + +gboolean +gnc_pricedb_remove_price(GNCPriceDB *db, GNCPrice *p) +{ + gboolean rc; + if(!db || !p) return FALSE; + ENTER ("db=%p, pr=%p not-saved=%d do-free=%d", db, p, p->not_saved, p->do_free); + + gnc_price_ref(p); + rc = remove_price (db, p, TRUE); + + /* invoke the backend to delete this price */ + gnc_price_begin_edit (p); + db->dirty = TRUE; + p->do_free = TRUE; + gnc_price_commit_edit (p); + + p->db = NULL; + gnc_price_unref(p); + LEAVE ("db=%p, pr=%p", db, p); + return rc; +} + +/* ==================================================================== */ +/* lookup/query functions */ + +GNCPrice * +gnc_pricedb_lookup_latest(GNCPriceDB *db, + gnc_commodity *commodity, + gnc_commodity *currency) +{ + GList *price_list; + GNCPrice *result; + GHashTable *currency_hash; + + ENTER ("db=%p commodity=%p currency=%p", db, commodity, currency); + if(!db || !commodity || !currency) return NULL; + + if (db->backend && db->backend->price_lookup) + { + GNCPriceLookup pl; + pl.type = LOOKUP_LATEST; + pl.prdb = db; + pl.commodity = commodity; + pl.currency = currency; + (db->backend->price_lookup) (db->backend, &pl); + } + + currency_hash = g_hash_table_lookup(db->commodity_hash, commodity); + if(!currency_hash) return NULL; + + price_list = g_hash_table_lookup(currency_hash, currency); + if(!price_list) return NULL; + + /* This works magically because prices are inserted in date-sorted order, + * and the latest date always comes first. So return the first in the list. */ + result = price_list->data; + gnc_price_ref(result); + LEAVE(" "); + return result; +} + +GList * +gnc_pricedb_get_prices(GNCPriceDB *db, + gnc_commodity *commodity, + gnc_commodity *currency) +{ + GList *price_list; + GList *result; + GList *node; + GHashTable *currency_hash; + + ENTER ("db=%p commodity=%p currency=%p", db, commodity, currency); + if(!db || !commodity || !currency) return NULL; + + if (db->backend && db->backend->price_lookup) + { + GNCPriceLookup pl; + pl.type = LOOKUP_ALL; + pl.prdb = db; + pl.commodity = commodity; + pl.currency = currency; + (db->backend->price_lookup) (db->backend, &pl); + } + + currency_hash = g_hash_table_lookup(db->commodity_hash, commodity); + if(!currency_hash) return NULL; + + price_list = g_hash_table_lookup(currency_hash, currency); + if(!price_list) return NULL; + + result = g_list_copy (price_list); + for (node = result; node; node = node->next) + gnc_price_ref (node->data); + + LEAVE (" "); + return result; +} + +GList * +gnc_pricedb_lookup_at_time(GNCPriceDB *db, + gnc_commodity *c, + gnc_commodity *currency, + Timespec t) +{ + GList *price_list; + GList *result = NULL; + GList *item = NULL; + GHashTable *currency_hash; + + ENTER ("db=%p commodity=%p currency=%p", db, c, currency); + if(!db || !c || !currency) return NULL; + + if (db->backend && db->backend->price_lookup) + { + GNCPriceLookup pl; + pl.type = LOOKUP_AT_TIME; + pl.prdb = db; + pl.commodity = c; + pl.currency = currency; + pl.date = t; + (db->backend->price_lookup) (db->backend, &pl); + } + + currency_hash = g_hash_table_lookup(db->commodity_hash, c); + if(!currency_hash) return NULL; + + price_list = g_hash_table_lookup(currency_hash, currency); + if(!price_list) return NULL; + + item = price_list; + while(item) { + GNCPrice *p = item->data; + Timespec price_time = gnc_price_get_time(p); + if(timespec_equal(&price_time, &t)) { + result = g_list_prepend(result, p); + gnc_price_ref(p); + } + item = item->next; + } + LEAVE (" "); + return result; +} + + +GNCPrice * +gnc_pricedb_lookup_nearest_in_time(GNCPriceDB *db, + gnc_commodity *c, + gnc_commodity *currency, + Timespec t) +{ + GList *price_list; + GNCPrice *current_price = NULL; + GNCPrice *next_price = NULL; + GNCPrice *result = NULL; + GList *item = NULL; + GHashTable *currency_hash; + + ENTER ("db=%p commodity=%p currency=%p", db, c, currency); + if(!db || !c || !currency) return NULL; + + if (db->backend && db->backend->price_lookup) + { + GNCPriceLookup pl; + pl.type = LOOKUP_NEAREST_IN_TIME; + pl.prdb = db; + pl.commodity = c; + pl.currency = currency; + pl.date = t; + (db->backend->price_lookup) (db->backend, &pl); + } + + currency_hash = g_hash_table_lookup(db->commodity_hash, c); + if(!currency_hash) return NULL; + + price_list = g_hash_table_lookup(currency_hash, currency); + if(!price_list) return NULL; + + item = price_list; + + /* default answer */ + current_price = item->data; + + /* find the first candidate past the one we want. Remember that + prices are in most-recent-first order. */ + while (!next_price && item) { + GNCPrice *p = item->data; + Timespec price_time = gnc_price_get_time(p); + if (timespec_cmp(&price_time, &t) <= 0) { + next_price = item->data; + break; + } + current_price = item->data; + item = item->next; + } + + if (current_price) { + if (!next_price) { + result = current_price; + } else { + Timespec current_t = gnc_price_get_time(current_price); + Timespec next_t = gnc_price_get_time(next_price); + Timespec diff_current = timespec_diff(¤t_t, &t); + Timespec diff_next = timespec_diff(&next_t, &t); + Timespec abs_current = timespec_abs(&diff_current); + Timespec abs_next = timespec_abs(&diff_next); + + if (timespec_cmp(&abs_current, &abs_next) <= 0) { + result = current_price; + } else { + result = next_price; + } + } + } + + gnc_price_ref(result); + LEAVE (" "); + return result; +} + +/* ==================================================================== */ +/* gnc_pricedb_foreach_price infrastructure + */ + +typedef struct { + gboolean ok; + gboolean (*func)(GNCPrice *p, gpointer user_data); + gpointer user_data; +} GNCPriceDBForeachData; + +static void +pricedb_foreach_pricelist(gpointer key, gpointer val, gpointer user_data) +{ + GList *price_list = (GList *) val; + GList *node = price_list; + GNCPriceDBForeachData *foreach_data = (GNCPriceDBForeachData *) user_data; + + /* stop traversal when func returns FALSE */ + while(foreach_data->ok && node) { + GNCPrice *p = (GNCPrice *) node->data; + foreach_data->ok = foreach_data->func(p, foreach_data->user_data); + node = node->next; + } +} + +static void +pricedb_foreach_currencies_hash(gpointer key, gpointer val, gpointer user_data) +{ + GHashTable *currencies_hash = (GHashTable *) val; + g_hash_table_foreach(currencies_hash, pricedb_foreach_pricelist, user_data); +} + +static gboolean +unstable_price_traversal(GNCPriceDB *db, + gboolean (*f)(GNCPrice *p, gpointer user_data), + gpointer user_data) +{ + GNCPriceDBForeachData foreach_data; + + if(!db || !f) return FALSE; + foreach_data.ok = TRUE; + foreach_data.func = f; + foreach_data.user_data = user_data; + + g_hash_table_foreach(db->commodity_hash, + pricedb_foreach_currencies_hash, + &foreach_data); + + return foreach_data.ok; +} + +static gint +compare_kvpairs_by_commodity_key(gconstpointer a, gconstpointer b) +{ + GHashTableKVPair *kvpa = (GHashTableKVPair *) a; + GHashTableKVPair *kvpb = (GHashTableKVPair *) b; + gnc_commodity *ca; + gnc_commodity *cb; + int cmp_result; + + if(a == b) return 0; + if(!a && !b) return 0; + if(!a) return -1; + if(!b) return 1; + + ca = (gnc_commodity *) kvpa->key; + cb = (gnc_commodity *) kvpb->key; + + cmp_result = safe_strcmp(gnc_commodity_get_namespace(ca), + gnc_commodity_get_namespace(cb)); + + if(cmp_result != 0) return cmp_result; + + return safe_strcmp(gnc_commodity_get_mnemonic(ca), + gnc_commodity_get_mnemonic(cb)); +} + +static gboolean +stable_price_traversal(GNCPriceDB *db, + gboolean (*f)(GNCPrice *p, gpointer user_data), + gpointer user_data) +{ + GSList *currency_hashes = NULL; + GSList *foo = NULL; + gboolean ok = TRUE; + GSList *i = NULL; + + if(!db || !f) return FALSE; + + currency_hashes = g_hash_table_key_value_pairs(db->commodity_hash); + currency_hashes = g_slist_sort(currency_hashes, + compare_kvpairs_by_commodity_key); + + for(i = currency_hashes; i; i = i->next) { + GHashTableKVPair *kv_pair = (GHashTableKVPair *) i->data; + GHashTable *currency_hash = (GHashTable *) kv_pair->value; + GSList *price_lists = g_hash_table_key_value_pairs(currency_hash); + GSList *j; + + price_lists = g_slist_sort(price_lists, compare_kvpairs_by_commodity_key); + for(j = price_lists; j; j = j->next) { + GHashTableKVPair *pricelist_kvp = (GHashTableKVPair *) j->data; + GList *price_list = (GList *) pricelist_kvp->value; + GList *node; + + for(node = (GList *) price_list; node; node = node->next) { + GNCPrice *price = (GNCPrice *) node->data; + + /* stop traversal when f returns FALSE */ + if (FALSE == ok) break; + if(!f(price, user_data)) ok = FALSE; + } + } + if(price_lists) { + g_slist_foreach(price_lists, g_hash_table_kv_pair_free_gfunc, NULL); + g_slist_free(price_lists); + price_lists = NULL; + } + } + + if(currency_hashes) { + g_slist_foreach(currency_hashes, g_hash_table_kv_pair_free_gfunc, NULL); + g_slist_free(currency_hashes); + } + return ok; +} + +gboolean +gnc_pricedb_foreach_price(GNCPriceDB *db, + gboolean (*f)(GNCPrice *p, gpointer user_data), + gpointer user_data, + gboolean stable_order) +{ + ENTER ("db=%p f=%p", db, f); + if(stable_order) return stable_price_traversal(db, f, user_data); + return unstable_price_traversal(db, f, user_data); + LEAVE (" "); +} + +/* ==================================================================== */ +/* commodity substitution */ + +typedef struct { + gnc_commodity *old_c; + gnc_commodity *new_c; +} GNCPriceFixupData; + +static gboolean +add_price_to_list (GNCPrice *p, gpointer data) +{ + GList **list = data; + + *list = g_list_prepend (*list, p); + + return TRUE; +} + +static void +gnc_price_fixup_legacy_commods(gpointer data, gpointer user_data) +{ + GNCPrice *p = data; + GNCPriceFixupData *fixup_data = user_data; + gnc_commodity *price_c; + + if (!p) return; + + price_c = gnc_price_get_commodity(p); + if (gnc_commodity_equiv(price_c, fixup_data->old_c)) { + gnc_price_set_commodity (p, fixup_data->new_c); + } + price_c = gnc_price_get_currency(p); + if (gnc_commodity_equiv(price_c, fixup_data->old_c)) { + gnc_price_set_currency (p, fixup_data->new_c); + } +} + +void +gnc_pricedb_substitute_commodity(GNCPriceDB *db, + gnc_commodity *old_c, + gnc_commodity *new_c) +{ + GHashTable *currency_hash; + GNCPriceFixupData data; + GList *prices = NULL; + + if(!db || !old_c || !new_c) return; + + data.old_c = old_c; + data.new_c = new_c; + + gnc_pricedb_foreach_price (db, add_price_to_list, &prices, FALSE); + + g_list_foreach (prices, gnc_price_fixup_legacy_commods, &data); + + g_list_free (prices); +} + +/***************************************************************************/ + +/* Semi-lame debugging code */ + +void +gnc_price_print(GNCPrice *p, FILE *f, int indent) +{ + gnc_commodity *commodity; + gnc_commodity *currency; + gchar *istr = NULL; /* indent string */ + const char *str; + + if(!p) return; + if(!f) return; + + commodity = gnc_price_get_commodity(p); + currency = gnc_price_get_currency(p); + + if(!commodity) return; + if(!currency) return; + + istr = g_strnfill(indent, ' '); + + fprintf(f, "%s\n", istr); + fprintf(f, "%s \n", istr, commodity); + str = gnc_commodity_get_namespace(commodity); + str = str ? str : "(null)"; + fprintf(f, "%s %s\n", istr, str); + str = gnc_commodity_get_mnemonic(commodity); + str = str ? str : "(null)"; + fprintf(f, "%s %s\n", istr, str); + fprintf(f, "%s \n", istr); + fprintf(f, "%s \n", istr, currency); + str = gnc_commodity_get_namespace(currency); + str = str ? str : "(null)"; + fprintf(f, "%s %s\n", istr, str); + str = gnc_commodity_get_mnemonic(currency); + str = str ? str : "(null)"; + fprintf(f, "%s %s\n", istr, str); + fprintf(f, "%s \n", istr); + str = gnc_price_get_source(p); + str = str ? str : "(null)"; + fprintf(f, "%s %s\n", istr, str); + str = gnc_price_get_type(p); + str = str ? str : "(null)"; + fprintf(f, "%s %s\n", istr, str); + fprintf(f, "%s %g\n", istr, gnc_numeric_to_double(gnc_price_get_value(p))); + fprintf(f, "%s\n", istr); + + g_free(istr); +} + +static void +gnc_price_print_stdout(GNCPrice *p, int indent) +{ + gnc_price_print(p, stdout, indent); +} + +static gboolean +print_pricedb_adapter(GNCPrice *p, gpointer user_data) +{ + FILE *f = (FILE *) user_data; + gnc_price_print(p, f, 1); + return TRUE; +} + +void +gnc_pricedb_print_contents(GNCPriceDB *db, FILE *f) +{ + if(!db) { PERR("NULL PriceDB\n"); return; } + if(!f) { PERR("NULL FILE*\n"); return; } + + fprintf(f, "\n"); + gnc_pricedb_foreach_price(db, print_pricedb_adapter, f, FALSE); + fprintf(f, "\n"); +} + +/* ========================= END OF FILE ============================== */ diff --git a/src/engine/gnc-pricedb.h b/src/engine/gnc-pricedb.h new file mode 100644 index 0000000000..f2e8678808 --- /dev/null +++ b/src/engine/gnc-pricedb.h @@ -0,0 +1,289 @@ +/******************************************************************** + * gnc-pricedb.h -- a simple price database for gnucash. * + * Copyright (C) 2001 Rob Browning, Linas Vepstas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + *******************************************************************/ + +#ifndef GNC_PRICEDB_H +#define GNC_PRICEDB_H + +#include "date.h" +#include "gnc-numeric.h" +#include "gnc-commodity.h" +#include "guid.h" + +#include + +/**********************************************************************\ + + Introduction: + + The PriceDB is intended to be a database of price quotes, or more + specifically, a database of GNCPrices. For the time being, it is + still a fairly simple database supporting only fairly simple + queries. It is expected that new queries will be added as needed, + and that there is some advantage to delaying complex queries for + now in the hope that we get a real DB implementation before + they're really needed. + + Every GNCBook contains a GNCPriceDB, accessable via + gnc_book_get_pricedb. + +*/ + + +/**********************************************************************\ + + GNCPrice: + + Each price in the database represents an "instantaneous" quote for + a given commodity with respect to another commodity. For example, + a given price might represent the value of LNUX in USD on + 2001-02-03. + + Fields: + + commodity: the item being priced. + + currency: the denomination of the value of the item being priced. + + value: the value of the item being priced. + + time: the time the price was valid. + + source: a string describing the source of the quote. These + strings will be something like this: "Finance::Quote", + "user:misc", "user:foo", etc. If the quote came from a user, + as a matter of policy, you *must* prefix the string you give + with "user:". For now, the only other reserved values are + "Finance::Quote" and "old-file-import". + + type: the type of quote - types possible right now are bid, ask, + last, nav, and unknown. + + Implementation Details: + + NOTE: for source and type, NULL and the empty string are + considered the same, so if one of these is "", then after a file + save/restore, it might be NULL. Behave accordingly. + + GNCPrices are reference counted. When you gnc_price_create one + or clone it, the new price's count is set to 1. When you are + finished with a price, call gnc_price_unref. If you hand the + price pointer to some other code that needs to keep it, make + sure it calls gnc_price_ref to indicate its interest in that + price, and calls gnc_price_unref when it's finished with the + price. For those unfamiliar with reference counting, basically + each price stores an integer count which starts at 1 and is + incremented every time someone calls gnc_price_ref. Conversely, + the count is decremented every time someone calls + gnc_price_unref. If the count ever reaches 0, the price is + destroyed. + + All of the getters return data that's internal to the GNCPrice, + not copies, so don't free these values. + + All of the setters store copies of the data given, with the + exception of the commodity field which just stores the pointer + given. It is assumed that commodities are a global resource and + are pointer unique. + + */ + +typedef struct _GNCPrice GNCPrice; +typedef struct _GNCPriceLookup GNCPriceLookup; + +/****************/ +/* constructors */ + +/* gnc_price_create - returns a newly allocated and initialized price + with a reference count of 1. */ +GNCPrice *gnc_price_create(void); + +/* gnc_price_clone - returns a newly allocated price that's a + content-wise duplicate of the given price, p. The returned clone + will have a reference count of 1. */ +GNCPrice *gnc_price_clone(GNCPrice* p); + +/*********************/ +/* memory management */ + +/* gnc_price_ref - indicate your need for a given price to stick + around (i.e. increase its reference count by 1). */ +void gnc_price_ref(GNCPrice *p); + +/* gnc_price_ref - indicate you're finished with a price + (i.e. decrease its reference count by 1). */ +void gnc_price_unref(GNCPrice *p); + +/***********/ +/* setters */ + +/* As mentioned above, all of the setters store copies of the data + * given, with the exception of the commodity field which just stores + * the pointer given. It is assumed that commodities are a global + * resource and are pointer unique. + * + * Invocations of the setters should be wrapped with calls to + * gnc_price_begin_edit() and commit_edit(). The begin/commit + * calls help ensure that the local price db is synchronized with + * the backend. + */ +void gnc_price_begin_edit (GNCPrice *p); +void gnc_price_commit_edit (GNCPrice *p); + +void gnc_price_set_commodity(GNCPrice *p, gnc_commodity *c); +void gnc_price_set_currency(GNCPrice *p, gnc_commodity *c); +void gnc_price_set_time(GNCPrice *p, Timespec t); +void gnc_price_set_source(GNCPrice *p, const char *source); +void gnc_price_set_type(GNCPrice *p, const char* type); +void gnc_price_set_value(GNCPrice *p, gnc_numeric value); +void gnc_price_set_version(GNCPrice *p, gint32 versn); + +/***********/ +/* getters */ + +/* As mentioned above all of the getters return data that's internal + to the GNCPrice, not copies, so don't free these values. */ +GNCPrice * gnc_price_lookup (const GUID *guid); +const GUID * gnc_price_get_guid (GNCPrice *p); +gnc_commodity * gnc_price_get_commodity(GNCPrice *p); +gnc_commodity * gnc_price_get_currency(GNCPrice *p); +Timespec gnc_price_get_time(GNCPrice *p); +const char * gnc_price_get_source(GNCPrice *p); +const char * gnc_price_get_type(GNCPrice *p); +gnc_numeric gnc_price_get_value(GNCPrice *p); +gint32 gnc_price_get_version(GNCPrice *p); +gboolean gnc_price_equal(GNCPrice *p1, GNCPrice *p2); + +/********************************************************************** + GNCPrice lists: + + The database communicates multiple prices in and out via gnc price + lists. These are just time sorted GLists of GNCPrice pointers. + Functions for manipulating these lists are provided. These + functions are helpful in that they handle maintaining proper + reference counts on behalf of the price list for every price being + held in a given list. I.e. insert "refs" the prices being + inserted, remove and destroy "unref" the prices that will no + longer be referred to by the list. + +*/ + +/* gnc_price_list_insert - insert a price into the given list, calling + gnc_price_ref on it during the process. */ +gboolean gnc_price_list_insert(GList **prices, GNCPrice *p); +/* gnc_price_list_remove - remove the price, p, from the given list, + calling gnc_price_unref on it during the process. */ +gboolean gnc_price_list_remove(GList **prices, GNCPrice *p); +/* gnc_price_list_destroy - destroy the given price list, calling + gnc_price_unref on all the prices included in the list. */ +void gnc_price_list_destroy(GList *prices); + + +/********************************************************************** + GNCPriceDB + + Whenever a you store a price in the pricedb, the pricedb adds its + own reference to the price, so you can safely unref that price after + inserting it into the DB if you're finished with it otherwise. + + Similarly, when the pricedb returns a price to you, either singly, + or in a price list, the price will have had a ref added for you, so + you only need to unref the price(s) when you're finished with + it/them. + +*/ + +typedef struct _GNCPriceDB GNCPriceDB; + + +/* gnc_pricedb_create - create a new pricedb. Normally you won't need + this; you will get the pricedb via gnc_book_get_pricedb. */ +GNCPriceDB * gnc_pricedb_create(void); + +/* gnc_pricedb_destroy - destroy the given pricedb and unref all of + the prices it contains. This may not deallocate all of those + prices. Other code may still be holding references to them. */ +void gnc_pricedb_destroy(GNCPriceDB *db); + +/* gnc_pricedb_add_price - add a price to the pricedb, you may drop + your reference to the price (i.e. call unref) after this + succeeds, whenever you're finished with the price. */ +gboolean gnc_pricedb_add_price(GNCPriceDB *db, GNCPrice *p); + +/* gnc_pricedb_remove_price - removes the given price, p, from the + pricedb. Returns TRUE if successful, FALSE otherwise. */ +gboolean gnc_pricedb_remove_price(GNCPriceDB *db, GNCPrice *p); + +/* gnc_pricedb_lookup_latest - find the most recent price for the + given commodity in the given currency. Returns NULL on + failure. */ +GNCPrice * gnc_pricedb_lookup_latest(GNCPriceDB *db, + gnc_commodity *commodity, + gnc_commodity *currency); + +/* gnc_pricedb_get_prices - return all the prices for a given + commodity in the given currency. Returns NULL on failure. The + result is a GNCPrice list (see above). */ +GList * gnc_pricedb_get_prices(GNCPriceDB *db, + gnc_commodity *commodity, + gnc_commodity *currency); + +/* gnc_pricedb_lookup_at_time - return all prices that match the given + commodity, currency, and timespec. Prices will be returned as a + GNCPrice list (see above). */ +GList * gnc_pricedb_lookup_at_time(GNCPriceDB *db, + gnc_commodity *commodity, + gnc_commodity *currency, + Timespec t); + +/* gnc_pricedb_lookup_nearest_in_time - return the price for the given + commodity in the given currency nearest to the given time t. */ +GNCPrice * +gnc_pricedb_lookup_nearest_in_time(GNCPriceDB *db, + gnc_commodity *c, + gnc_commodity *currency, + Timespec t); + +/* gnc_pricedb_foreach_price - call f once for each price in db, until + and unless f returns FALSE. If stable_order is not FALSE, make + sure the ordering of the traversal is stable (i.e. the same order + every time given the same db contents -- stable traversals may be + less efficient). */ +gboolean gnc_pricedb_foreach_price(GNCPriceDB *db, + gboolean (*f)(GNCPrice *p, + gpointer user_data), + gpointer user_data, + gboolean stable_order); + +/* gnc_pricedb_dirty - return FALSE if the database has not been + modified. */ +gboolean gnc_pricedb_dirty(GNCPriceDB *db); + +/* gnc_pricedb_get_num_prices - return the number of prices + in the database. */ +guint gnc_pricedb_get_num_prices(GNCPriceDB *db); + +/* semi-lame debugging code */ +void gnc_price_print(GNCPrice *db, FILE *f, int indent); +void gnc_pricedb_print_contents(GNCPriceDB *db, FILE *f); + +#endif diff --git a/src/engine/guid.c b/src/engine/guid.c new file mode 100644 index 0000000000..1cd2cb4e21 --- /dev/null +++ b/src/engine/guid.c @@ -0,0 +1,571 @@ +/********************************************************************\ + * guid.c -- globally unique ID implementation * + * Copyright (C) 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#define _GNU_SOURCE + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "guid.h" +#include "md5.h" +#include "gnc-engine-util.h" + +# ifndef P_tmpdir +# define P_tmpdir "/tmp" +# endif + + +/** Constants *******************************************************/ +#define DEBUG_GUID 0 +#define BLOCKSIZE 4096 +#define THRESHOLD (2 * BLOCKSIZE) + + +/** Static global variables *****************************************/ +static gboolean guid_initialized = FALSE; +static struct md5_ctx guid_context; + +/* This static indicates the debugging module that this .o belongs to. */ +static short module = MOD_ENGINE; + + +/** Function implementations ****************************************/ + +/* This code is based on code in md5.c in GNU textutils. */ +static size_t +init_from_stream(FILE *stream, size_t max_size) +{ + char buffer[BLOCKSIZE + 72]; + size_t sum, block_size, total; + + if (max_size <= 0) + return 0; + + total = 0; + + /* Iterate over file contents. */ + while (1) + { + /* We read the file in blocks of BLOCKSIZE bytes. One call of the + * computation function processes the whole buffer so that with the + * next round of the loop another block can be read. */ + size_t n; + sum = 0; + + if (max_size < BLOCKSIZE) + block_size = max_size; + else + block_size = BLOCKSIZE; + + /* Read block. Take care for partial reads. */ + do + { + n = fread (buffer + sum, 1, block_size - sum, stream); + + sum += n; + } + while (sum < block_size && n != 0); + + max_size -= sum; + + if (n == 0 && ferror (stream)) + return total; + + /* If end of file or max_size is reached, end the loop. */ + if ((n == 0) || (max_size == 0)) + break; + + /* Process buffer with BLOCKSIZE bytes. Note that + * BLOCKSIZE % 64 == 0 */ + md5_process_block (buffer, BLOCKSIZE, &guid_context); + + total += sum; + } + + /* Add the last bytes if necessary. */ + if (sum > 0) + { + md5_process_bytes (buffer, sum, &guid_context); + total += sum; + } + + return total; +} + +static size_t +init_from_file(const char *filename, size_t max_size) +{ + struct stat stats; + size_t total = 0; + size_t file_bytes; + FILE *fp; + + if (stat(filename, &stats) != 0) + return 0; + + md5_process_bytes(&stats, sizeof(stats), &guid_context); + total += sizeof(stats); + + if (max_size <= 0) + return total; + + fp = fopen (filename, "r"); + if (fp == NULL) + return total; + + file_bytes = init_from_stream(fp, max_size); + +#if DEBUG_GUID + g_warning ("guid_init got %u bytes from %s", file_bytes, filename); +#endif + + total += file_bytes; + + fclose(fp); + + return total; +} + +static size_t +init_from_dir(const char *dirname, unsigned int max_files) +{ + char filename[1024]; + struct dirent *de; + struct stat stats; + size_t total; + int result; + DIR *dir; + + if (max_files <= 0) + return 0; + + dir = opendir (dirname); + if (dir == NULL) + return 0; + + total = 0; + + do + { + de = readdir(dir); + if (de == NULL) + break; + + md5_process_bytes(de, sizeof(struct dirent), &guid_context); + total += sizeof(struct dirent); + + result = snprintf(filename, sizeof(filename), + "%s/%s", dirname, de->d_name); + if ((result < 0) || (result >= sizeof(filename))) + continue; + + if (stat(filename, &stats) != 0) + continue; + md5_process_bytes(&stats, sizeof(stats), &guid_context); + total += sizeof(stats); + + max_files--; + } while (max_files > 0); + + closedir(dir); + + return total; +} + +static size_t +init_from_time(void) +{ + size_t total; + time_t t_time; + clock_t clocks; + struct tms tms_buf; + + total = 0; + + t_time = time(NULL); + md5_process_bytes(&t_time, sizeof(t_time), &guid_context); + total += sizeof(t_time); + + clocks = times(&tms_buf); + md5_process_bytes(&clocks, sizeof(clocks), &guid_context); + md5_process_bytes(&tms_buf, sizeof(tms_buf), &guid_context); + total += sizeof(clocks) + sizeof(tms_buf); + + return total; +} + +void +guid_init(void) +{ + size_t bytes = 0; + + md5_init_ctx(&guid_context); + + /* entropy pool */ + bytes += init_from_file ("/dev/urandom", 512); + + /* files */ + { + const char * files[] = + { "/etc/passwd", + "/proc/loadavg", + "/proc/meminfo", + "/proc/net/dev", + "/proc/rtc", + "/proc/self/environ", + "/proc/self/stat", + "/proc/stat", + "/proc/uptime", + NULL + }; + int i; + + for (i = 0; files[i] != NULL; i++) + bytes += init_from_file(files[i], BLOCKSIZE); + } + + /* directories */ + { + const char * dirname; + const char * dirs[] = + { + "/proc", + P_tmpdir, + "/var/lock", + "/var/log", + "/var/mail", + "/var/spool/mail", + "/var/run", + NULL + }; + int i; + + for (i = 0; dirs[i] != NULL; i++) + bytes += init_from_dir(dirs[i], 32); + + dirname = getenv("HOME"); + if (dirname != NULL) + bytes += init_from_dir(dirname, 32); + } + + /* process and parent ids */ + { + pid_t pid; + + pid = getpid(); + md5_process_bytes(&pid, sizeof(pid), &guid_context); + bytes += sizeof(pid); + + pid = getppid(); + md5_process_bytes(&pid, sizeof(pid), &guid_context); + bytes += sizeof(pid); + } + + /* user info */ + { + uid_t uid; + gid_t gid; + char *s; + + s = getlogin(); + if (s != NULL) + { + md5_process_bytes(s, strlen(s), &guid_context); + bytes += strlen(s); + } + + uid = getuid(); + md5_process_bytes(&uid, sizeof(uid), &guid_context); + bytes += sizeof(uid); + + gid = getgid(); + md5_process_bytes(&gid, sizeof(gid), &guid_context); + bytes += sizeof(gid); + } + + /* host info */ + { + char string[1024]; + + gethostname(string, sizeof(string)); + md5_process_bytes(string, sizeof(string), &guid_context); + bytes += sizeof(string); + } + + /* plain old random */ + { + int n, i; + + srand((unsigned int) time(NULL)); + + for (i = 0; i < 32; i++) + { + n = rand(); + + md5_process_bytes(&n, sizeof(n), &guid_context); + bytes += sizeof(n); + } + } + + /* time in secs and clock ticks */ + bytes += init_from_time(); + +#if DEBUG_GUID + g_warning ("guid_init got %u bytes", bytes); +#endif + + if (bytes < THRESHOLD) + g_warning("WARNING: guid_init only got %u bytes.\n" + "The identifiers might not be very random.\n", bytes); + + guid_initialized = TRUE; +} + +void +guid_init_with_salt(const void *salt, size_t salt_len) +{ + guid_init(); + + md5_process_bytes(salt, salt_len, &guid_context); +} + +void +guid_init_only_salt(const void *salt, size_t salt_len) +{ + md5_init_ctx(&guid_context); + + md5_process_bytes(salt, salt_len, &guid_context); + + guid_initialized = TRUE; +} + +#define GUID_PERIOD 5000 + +void +guid_new(GUID *guid) +{ + static int counter = 0; + struct md5_ctx ctx; + + if (guid == NULL) + return; + + if (!guid_initialized) + guid_init(); + + /* make the id */ + ctx = guid_context; + md5_finish_ctx(&ctx, guid->data); + + /* update the global context */ + init_from_time(); + + if (counter == 0) + { + FILE *fp; + + fp = fopen ("/dev/urandom", "r"); + if (fp == NULL) + return; + + init_from_stream(fp, 32); + + fclose(fp); + + counter = GUID_PERIOD; + } + + counter--; +} + +/* needs 32 bytes exactly, doesn't print a null char */ +static void +encode_md5_data(const unsigned char *data, char *buffer) +{ + size_t count; + + for (count = 0; count < 16; count++, buffer += 2) + sprintf(buffer, "%02x", data[count]); +} + +/* returns true if the first 32 bytes of buffer encode + * a hex number. returns false otherwise. Decoded number + * is packed into data in little endian order. */ +static gboolean +decode_md5_string(const char *string, unsigned char *data) +{ + unsigned char n1, n2; + size_t count = -1; + char c1, c2; + + if (NULL == data) return FALSE; + if (NULL == string) goto badstring; + + for (count = 0; count < 16; count++) + { + /* check for a short string e.g. null string ... */ + if ((0==string[2*count]) || (0==string[2*count+1])) goto badstring; + + c1 = tolower(string[2 * count]); + if (!isxdigit(c1)) goto badstring; + + c2 = tolower(string[2 * count + 1]); + if (!isxdigit(c2)) goto badstring; + + if (isdigit(c1)) + n1 = c1 - '0'; + else + n1 = c1 - 'a' + 10; + + if (isdigit(c2)) + n2 = c2 - '0'; + else + n2 = c2 - 'a' + 10; + + data[count] = (n1 << 4) | n2; + } + return TRUE; + +badstring: + PERR ("bad string, stopped at %d\n", count); + for (count = 0; count < 16; count++) + { + data[count] = 0; + } + return FALSE; + +} + +char * +guid_to_string(const GUID * guid) +{ + char *string; + + if(!guid) return(NULL); + + string = g_malloc(GUID_ENCODING_LENGTH+1); + + encode_md5_data(guid->data, string); + + string[GUID_ENCODING_LENGTH] = '\0'; + + return string; +} + +char * +guid_to_string_buff(const GUID * guid, char *string) +{ + if (!string || !guid) return NULL; + + encode_md5_data(guid->data, string); + + string[GUID_ENCODING_LENGTH] = '\0'; + return &string[GUID_ENCODING_LENGTH]; +} + +gboolean +string_to_guid(const char * string, GUID * guid) +{ + return decode_md5_string(string, (guid != NULL) ? guid->data : NULL); +} + +gboolean +guid_equal(const GUID *guid_1, const GUID *guid_2) +{ + if (guid_1 && guid_2) + return (memcmp(guid_1, guid_2, sizeof(GUID)) == 0); + else + return FALSE; +} + +gint +guid_compare(const GUID *guid_1, const GUID *guid_2) +{ + if (guid_1 == guid_2) + return 0; + + /* nothing is always less than something */ + if (!guid_1 && guid_2) + return -1; + + if (guid_1 && !guid_2) + return 1; + + return memcmp (guid_1, guid_2, sizeof (GUID)); +} + +guint +guid_hash_to_guint (gconstpointer ptr) +{ + const GUID *guid = ptr; + + if (!guid) + { + PERR ("received NULL guid pointer."); + return 0; + } + + if (sizeof(guint) <= sizeof(guid->data)) + { + return (*((guint *) guid->data)); + } + else + { + guint hash = 0; + int i, j; + + for (i = 0, j = 0; i < sizeof(guint); i++, j++) { + if (j == 16) j = 0; + + hash <<= 4; + hash |= guid->data[j]; + } + + return hash; + } +} + +static gint +guid_g_hash_table_equal (gconstpointer guid_a, gconstpointer guid_b) +{ + return guid_equal (guid_a, guid_b); +} + +GHashTable * +guid_hash_table_new (void) +{ + return g_hash_table_new (guid_hash_to_guint, guid_g_hash_table_equal); +} diff --git a/src/engine/guid.h b/src/engine/guid.h new file mode 100644 index 0000000000..9428530568 --- /dev/null +++ b/src/engine/guid.h @@ -0,0 +1,112 @@ +/********************************************************************\ + * guid.h -- globally unique ID User API * + * Copyright (C) 2000 Dave Peticolas * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef GUID_H +#define GUID_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include + +/* This file defines an API for using globally unique identifiers. */ + +/* The type used to store guids */ +typedef union _GUID +{ + unsigned char data[16]; + + int __align_me; /* this just ensures that GUIDs are 32-bit + * aligned on systems that need them to be. */ +} GUID; + + +/* number of characters needed to encode a guid as a string + * not including the null terminator. */ +#define GUID_ENCODING_LENGTH 32 + + +/* Three functions to initialize the id generator. Only one needs to + * be called. Calling any initialization function a second time will + * reset the generator and erase the effect of the first call. + * + * guid_init() will initialize the generator with a variety of random + * sources. + * + * guid_init_with_salt() will initialize the generator with guid_init() + * and with the data given in the salt argument. This argument can be + * used to add additional randomness to the generated ids. + * + * guid_init_only_salt() will initialize the generator with the data + * given in the salt argument, but not with any other source. Calling + * guid_init_only_salt() with a specific argument will produce a + * specific sequence of ids reliably. */ +void guid_init(void); +void guid_init_with_salt(const void *salt, size_t salt_len); +void guid_init_only_salt(const void *salt, size_t salt_len); + + +/* Generate a new id. If no initialization function has been called, + * guid_init() will be called before the id is created. */ +void guid_new(GUID *guid); + + +/* The guid_to_string() routine returns a null-terminated string + * encoding of the id. String encodings of identifiers are hex + * numbers printed only with the characters '0' through '9' and + * 'a' through 'f'. The encoding will always be GUID_ENCODING_LENGTH + * characters long. The returned string should be freed when no + * longer needed. + * + * The guid_to_string_buff() routine does the same, except that the + * string is written into the memory pointed at by buff. The + * buffer must be at least GUID_ENCODING_LENGTH+1 characters long. + * This routine is handy for avoiding a malloc/free cycle. + * It returns a pointer to the >>end<< of what was written. + * (i.e. it can be used like 'stpcpy' during string concatenation) + */ +char * guid_to_string (const GUID * guid); +char * guid_to_string_buff (const GUID * guid, char *buff); + + +/* Given a string, decode the id into the guid if guid is non-NULL. + * The function returns TRUE if the string was a valid 32 character + * hexadecimal number. This function accepts both upper and lower case + * hex digits. If the return value is FALSE, the effect on guid is + * undefined. */ +gboolean string_to_guid(const char * string, GUID * guid); + + +/* Given two GUIDs, return TRUE if they are non-NULL and equal. + * Return FALSE, otherwise. */ +gboolean guid_equal(const GUID *guid_1, const GUID *guid_2); +gint guid_compare(const GUID *g1, const GUID *g2); + +/* Given a GUID *, hash it to a guint */ +guint guid_hash_to_guint(gconstpointer ptr); + +GHashTable *guid_hash_table_new(void); + +#endif diff --git a/src/engine/kvp_doc.txt b/src/engine/kvp_doc.txt new file mode 100644 index 0000000000..359ddc58cf --- /dev/null +++ b/src/engine/kvp_doc.txt @@ -0,0 +1,176 @@ +This file documents the use of keys in the key-value pair system +used by the Engine. Before assigning keys for use, please read +the Key-Value Policy in the GnuCash Design Document located +under src/doc/design. + +The format of the data below is: + +Name: The name of the key, including key names of parent frames + with the parent frames listed first, as in a fully-qualified + filename. Use the '/' character to separate keys. + +Type: The type of value stored in the key. The types are listed in + 'kvp_frame.h'. + +Entities: Which engine entities (Accounts, Transactions, Splits) + can use this key. Use 'All' if every entity could have + the key. + +Use: The use to which the key will be put. Include any requirements + for using the key here. Also include any API calls which use + the key. If more than one entity can use the key, + + +Example: + +Name: my-favorite-thing +Type: string +Entities: Accounts +Use: xaccGetMyFavoriteThing(), xaccSetMyFavoriteThing() + This key stores a text description of the user's + favorite thing. Do not use this key to store things + which the user merely likes! + + +Please put the keys in alphabetical order. + +-------------------------------------------------------------------------- + +Name: last-num +Type: string +Entities: Account +Use: xaccAccountGetLastNum, xaccAccountSetLastNum + Store the last number used in an account's transactions. + Used in auto-filling the Num field. + +Name: memo +Type: string +Entities: Split +Use: xaccSplitGetMemo, xaccSplitSetMemo + Store the 'Memo' string associated with Splits. + +Name: notes +Type: string +Entities: Account, Transaction +Use: xaccAccountGetNotes, xaccAccountSetNotes, + xaccTransGetNotes, xaccTransSetNotes + Store the 'Notes' string which was previously stored in + the Account structure directly. + +Name: old-currency +Type: string +Entities: Account +Use: This string holds the canonical name of the old Account + currency. This may be deleted at some point in the future. + +Name: old-currency-scu +Type: gint64 +Entities: Account +Use: This holds the old currency scu. This may be deleted at + some point in the future. + +Name: old-docref +Type: string +Entities: Transactions, Splits +Use: This string holds the old Docref value which was supported in earlier + versions of GnuCash but which was never used for any purpose. The + value is retained in case any users were making use of it. + +Name: old-price-source +Type: string +Entities: Account +Use: This string holds the old Price Source code used by earlier versions + of GnuCash and stored in the obsolete AccInfo structure. The use of + this value will be deprecated when the new version of Finance::Quote + is fully supported. The new version of Finance::Quote uses a different + scheme to identify sources for price quotes. + +Name: old-security +Type: string +Entities: Account +Use: This string holds the canonical name of the old Account + security. This may be deleted at some point in the future. + +Name: old-security-scu +Type: gint64 +Entities: Account +Use: This holds the old security scu. This may be deleted at + some point in the future. + +Name: reconcile-info +Type: frame +Entities: Account +Use: store reconcile information about accounts + +Name: reconcile-info/last-date +Type: frame +Entities: Account +Use: store the statement date of the last reconciliation + +Name: reconcile-info/postpone/date +Type: gint64 +Entities: Account +Use: store the ending statement date of a postponed reconciliation + +Name: reconcile-info/postpone/balance +Type: numeric +Entities: Account +Use: store the ending balance of a postponed reconciliation + +Name: reconcile-info/auto-interest-transfer +Type: string +Entities: Account +Use: allows the user to override the global reconcile option + "Automatic interest transfer" on a per-account basis. + Acceptable values are "true" and "false". + (This really could use a KVP_TYPE_BOOLEAN.) + +Name: split-type +Entities: Split +Use: xaccSplitGetType, xaccSplitMakeStockSplit + Store a string representing the type of split, if not normal. + +Name: tax-US/code +Type: string +Entities: Account +Use: see src/scm/report/[tax,txf]*.scm + +Name: tax-US/payer-name-source +Type: string +Entities: Account +Use: see src/scm/report/[tax,txf]*.scm + +Name: tax-related +Type: gint64 +Entities: Account +Use: A boolean flag indicated whether the Account is tax-related. + +Name: sched-xaction +Type: frame +Entities: Split in a SchedXaction +Use: Storage for the various fields of a scheduled transaction's + template Splits. + +Name: sched-xaction/xfrm +Type: GUID +Entities: Split associated with a SchedXaction +Use: The GUID of this Split's xfrm account. + +Name: sched-xaction/amnt +Type: string +Entities: Split associated with a SchedXaction +Use: The amount field of a SchedXaction might be a formula, which we + store here. + +Name: from-sched-xaction +Type: GUID +Entities: Transaction +Use: Identifies that the Transaction was created from a ScheduledTransaction, + and stores the GUID of that SX. + +Name: user-keys +Type: frame +Entities: All +Use: This frame is used to store keys which are editable directly by + the user. The program should not attach any semantics to keys + under this frame. diff --git a/src/engine/kvp_frame.c b/src/engine/kvp_frame.c new file mode 100644 index 0000000000..14454bac61 --- /dev/null +++ b/src/engine/kvp_frame.c @@ -0,0 +1,1107 @@ +/******************************************************************** + * kvp_frame.c -- a key-value frame system for gnucash. * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * + ********************************************************************/ + +#include "config.h" + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include "gnc-engine.h" +#include "gnc-engine-util.h" +#include "gnc-numeric.h" +#include "guid.h" +#include "kvp_frame.h" + + + /* Note that we keep the keys for this hash table in a GCache + * (gnc_string_cache), as it is likely we will see the same keys + * over and over again */ +struct _kvp_frame { + GHashTable * hash; +}; + + +typedef struct { + void *data; + int datasize; +} kvp_value_binary_data; + +struct _kvp_value { + kvp_value_t type; + union { + gint64 int64; + double dbl; + gnc_numeric numeric; + gchar *str; + GUID *guid; + kvp_value_binary_data binary; + GList *list; + kvp_frame *frame; + } value; +}; + +/* This static indicates the debugging module that this .o belongs to. */ +static short module = MOD_ENGINE; + +/******************************************************************** + * kvp_frame functions + ********************************************************************/ + +static guint +kvp_hash_func(gconstpointer v) { + return g_str_hash(v); +} + +static gint +kvp_comp_func(gconstpointer v, gconstpointer v2) { + return g_str_equal(v, v2); +} + +static gboolean +init_frame_body_if_needed(kvp_frame *f) { + if(!f->hash) { + f->hash = g_hash_table_new(&kvp_hash_func, + &kvp_comp_func); + } + return(f->hash != NULL); +} + +kvp_frame * +kvp_frame_new(void) { + kvp_frame * retval = g_new0(kvp_frame, 1); + /* save space until we have data */ + retval->hash = NULL; + return retval; +} + +static void +kvp_frame_delete_worker(gpointer key, gpointer value, gpointer user_data) { + g_cache_remove(gnc_engine_get_string_cache(), key); + kvp_value_delete((kvp_value *)value); +} + +void +kvp_frame_delete(kvp_frame * frame) { + if (!frame) + return; + + if(frame->hash) { + /* free any allocated resource for frame or its children */ + g_hash_table_foreach(frame->hash, & kvp_frame_delete_worker, + (gpointer)frame); + + /* delete the hash table */ + g_hash_table_destroy(frame->hash); + frame->hash = NULL; + } + g_free(frame); +} + +gboolean +kvp_frame_is_empty(kvp_frame * frame) +{ + if (!frame) return TRUE; + if (!frame->hash) return TRUE; + return FALSE; +} + +static void +kvp_frame_copy_worker(gpointer key, gpointer value, gpointer user_data) { + kvp_frame * dest = (kvp_frame *)user_data; + g_hash_table_freeze(dest->hash); + g_hash_table_insert(dest->hash, + (gpointer)g_cache_insert(gnc_engine_get_string_cache(), key), + (gpointer)kvp_value_copy(value)); + g_hash_table_thaw(dest->hash); +} + +kvp_frame * +kvp_frame_copy(const kvp_frame * frame) { + kvp_frame * retval = kvp_frame_new(); + + if (!frame) + return retval; + + if(frame->hash) { + if(!init_frame_body_if_needed(retval)) return(NULL); + g_hash_table_foreach(frame->hash, + & kvp_frame_copy_worker, + (gpointer)retval); + } + return retval; +} + +static void +kvp_frame_set_slot_destructively(kvp_frame * frame, const char * slot, + kvp_value * new_value) { + /* FIXME: no way to indicate errors... */ + + gpointer orig_key; + gpointer orig_value; + int key_exists; + + if(!new_value && !frame->hash) return; /* don't need to do anything */ + if(!init_frame_body_if_needed(frame)) return; + + g_hash_table_freeze(frame->hash); + + key_exists = g_hash_table_lookup_extended(frame->hash, slot, + & orig_key, & orig_value); + if(key_exists) { + g_hash_table_remove(frame->hash, slot); + g_cache_remove(gnc_engine_get_string_cache(), orig_key); + kvp_value_delete(orig_value); + } + + if(new_value) { + g_hash_table_insert(frame->hash, + g_cache_insert(gnc_engine_get_string_cache(), + (gpointer) slot), + new_value); + } + + g_hash_table_thaw(frame->hash); +} + +void +kvp_frame_set_slot(kvp_frame * frame, const char * slot, + const kvp_value * value) { + /* FIXME: no way to indicate errors... */ + + kvp_value *new_value = NULL; + + if (!frame) + return; + + if(value) new_value = kvp_value_copy(value); + kvp_frame_set_slot_destructively(frame, slot, new_value); +} + +void +kvp_frame_set_slot_nc(kvp_frame * frame, const char * slot, + kvp_value * value) { + /* FIXME: no way to indicate errors... */ + + if (!frame) + return; + + kvp_frame_set_slot_destructively(frame, slot, value); +} + +kvp_value * +kvp_frame_get_slot(kvp_frame * frame, const char * slot) { + if (!frame) + return NULL; + if(!frame->hash) return(NULL); + return (kvp_value *)g_hash_table_lookup(frame->hash, slot); +} + +/* ============================================================ */ + +void +kvp_frame_set_slot_path (kvp_frame *frame, + const kvp_value *new_value, + const char *first_key, ...) { + va_list ap; + const char *key; + + if (!frame || !first_key) + return; + + va_start (ap, first_key); + + key = first_key; + + while (TRUE) { + kvp_value *value; + const char *next_key; + + next_key = va_arg (ap, const char *); + if (!next_key) { + kvp_frame_set_slot (frame, key, new_value); + break; + } + + value = kvp_frame_get_slot (frame, key); + if (!value) { + kvp_frame *new_frame = kvp_frame_new (); + kvp_value *frame_value = kvp_value_new_frame (new_frame); + + kvp_frame_set_slot_nc (frame, key, frame_value); + + value = kvp_frame_get_slot (frame, key); + if (!value) + break; + } + + frame = kvp_value_get_frame (value); + if (!frame) + break; + + key = next_key; + } + + va_end (ap); +} + +void +kvp_frame_set_slot_path_gslist (kvp_frame *frame, + const kvp_value *new_value, + GSList *key_path) { + if (!frame || !key_path) + return; + + while (TRUE) { + const char *key = key_path->data; + kvp_value *value; + + if (!key) + return; + + key_path = key_path->next; + if (!key_path) { + kvp_frame_set_slot (frame, key, new_value); + return; + } + + value = kvp_frame_get_slot (frame, key); + if (!value) { + kvp_frame *new_frame = kvp_frame_new (); + kvp_value *frame_value = kvp_value_new_frame (new_frame); + + kvp_frame_set_slot_nc (frame, key, frame_value); + + value = kvp_frame_get_slot (frame, key); + if (!value) + return; + } + + frame = kvp_value_get_frame (value); + if (!frame) + return; + } +} + +/* ============================================================ */ + + +kvp_frame * +kvp_frame_get_frame_gslist (kvp_frame *frame, GSList *key_path) +{ + if (!frame || !key_path) + return frame; + + while (TRUE) { + const char *key = key_path->data; + kvp_value *value; + + if (!key) + return frame; /* an unusual but valid exit for this routine. */ + + /* get the named frame, or create it if it doesn't exist */ + value = kvp_frame_get_slot (frame, key); + if (!value) { + kvp_frame *new_frame = kvp_frame_new (); + kvp_value *frame_value = kvp_value_new_frame (new_frame); + + kvp_frame_set_slot_nc (frame, key, frame_value); + + value = kvp_frame_get_slot (frame, key); + if (!value) + return frame; /* this should never happen */ + } + + frame = kvp_value_get_frame (value); + if (!frame) + return NULL; /* this should never happen */ + + key_path = key_path->next; + if (!key_path) { + return frame; /* this is the normal exit for this func */ + } + } +} + +static GSList* +charstar_va_list_to_gslist(va_list lst) +{ + const char *val; + GSList *ret = NULL; + + while(TRUE) + { + val = va_arg(lst, const char*); + + if(!val) + { + break; + } + + ret = g_slist_append(ret, (gpointer)val); + } + + return ret; +} + +kvp_frame * +kvp_frame_get_frame (kvp_frame *frame, ...) +{ + va_list ap; + GSList *lst; + kvp_frame *ret; + + if (!frame) + return NULL; + + va_start (ap, frame); + + lst = charstar_va_list_to_gslist(ap); + + ret = kvp_frame_get_frame_gslist(frame, lst); + + va_end (ap); + + g_slist_free(lst); + + return ret; +} + +static GSList* +kvp_frame_parse_slash_path(const char *key_path) +{ + char *root, *key, *next; + GSList *ret; + + ret = NULL; + + root = g_strdup (key_path); + key = root; + key --; + + while (key) { + key ++; + while ('/' == *key) { key++; } + if ('\0' == *key) break; /* trailing slash */ + next = strchr (key, '/'); + if (next) *next = '\0'; + + ret = g_slist_append(ret, g_strdup(key)); + + key = next; + } + + g_free(root); + return ret; +} + +static void +kvp_frame_sp_free_string(gpointer data, gpointer ignored) +{ + g_free(data); +} + +static void +kvp_frame_delete_slash_path_gslist(GSList *lst) +{ + g_slist_foreach(lst, kvp_frame_sp_free_string, NULL); + g_slist_free(lst); +} + +kvp_frame * +kvp_frame_get_frame_slash (kvp_frame *frame, const char *key_path) +{ + GSList *lst; + kvp_frame *ret; + + if (!frame || !key_path) + return frame; + + lst = kvp_frame_parse_slash_path(key_path); + ret = kvp_frame_get_frame_gslist(frame, lst); + kvp_frame_delete_slash_path_gslist(lst); + + return ret; +} + +/* ============================================================ */ + +kvp_value * +kvp_frame_get_slot_path (kvp_frame *frame, + const char *first_key, ...) { + va_list ap; + kvp_value *value; + const char *key; + + if (!frame || !first_key) + return NULL; + + va_start (ap, first_key); + + key = first_key; + value = NULL; + + while (TRUE) { + value = kvp_frame_get_slot (frame, key); + if (!value) + break; + + key = va_arg (ap, const char *); + if (!key) + break; + + frame = kvp_value_get_frame (value); + if (!frame) + { + value = NULL; + break; + } + } + + va_end (ap); + + return value; +} + +kvp_value * +kvp_frame_get_slot_path_gslist (kvp_frame *frame, + GSList *key_path) { + if (!frame || !key_path) + return NULL; + + while (TRUE) { + const char *key = key_path->data; + kvp_value *value; + + if (!key) + return NULL; + + value = kvp_frame_get_slot (frame, key); + if (!value) + return NULL; + + key_path = key_path->next; + if (!key_path) + return value; + + frame = kvp_value_get_frame (value); + if (!frame) + return NULL; + } +} + +/******************************************************************** + * kvp glist functions + ********************************************************************/ + +static void +kvp_glist_delete_worker(gpointer datum, gpointer user_data) { + kvp_value * val = (kvp_value *)datum; + kvp_value_delete(val); +} + +void +kvp_glist_delete(GList * list) { + if(list) { + /* delete the data in the list */ + g_list_foreach(list, & kvp_glist_delete_worker, NULL); + + /* free the backbone */ + g_list_free(list); + } +} + +GList * +kvp_glist_copy(const GList * list) { + GList * retval = NULL; + GList * lptr; + + if(!list) return retval; + + /* duplicate the backbone of the list (this duplicates the POINTERS + * to the values; we need to deep-copy the values separately) */ + retval = g_list_copy((GList *) list); + + /* this step deep-copies the values */ + for(lptr = retval; lptr; lptr = lptr->next) { + lptr->data = kvp_value_copy(lptr->data); + } + + return retval; +} + +gint +kvp_glist_compare(const GList * list1, const GList * list2) { + const GList *lp1; + const GList *lp2; + + if(list1 == list2) return 0; + /* nothing is always less than something */ + if(!list1 && list2) return -1; + if(list1 && !list2) return 1; + + lp1 = list1; + lp2 = list2; + while(lp1 && lp2) { + kvp_value *v1 = (kvp_value *) lp1->data; + kvp_value *v2 = (kvp_value *) lp2->data; + gint vcmp = kvp_value_compare(v1, v2); + if(vcmp != 0) return vcmp; + lp1 = lp1->next; + lp2 = lp2->next; + } + if(!lp1 && lp2) return -1; + if(!lp2 && lp1) return 1; + return 0; +} + +/******************************************************************** + * kvp_value functions + ********************************************************************/ + +kvp_value * +kvp_value_new_gint64(gint64 value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_GINT64; + retval->value.int64 = value; + return retval; +} + +kvp_value * +kvp_value_new_double(double value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_DOUBLE; + retval->value.dbl = value; + return retval; +} + +kvp_value * +kvp_value_new_gnc_numeric(gnc_numeric value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_NUMERIC; + retval->value.numeric = value; + return retval; +} + +kvp_value * +kvp_value_new_string(const char * value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_STRING; + retval->value.str = g_strdup(value); + return retval; +} + +kvp_value * +kvp_value_new_guid(const GUID * value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_GUID; + retval->value.guid = g_new0(GUID, 1); + memcpy(retval->value.guid, value, sizeof(GUID)); + return retval; +} + +kvp_value * +kvp_value_new_binary(const void * value, guint64 datasize) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_BINARY; + retval->value.binary.data = g_new0(char, datasize); + retval->value.binary.datasize = datasize; + memcpy(retval->value.binary.data, value, datasize); + return retval; +} + +kvp_value * +kvp_value_new_binary_nc(void * value, guint64 datasize) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_BINARY; + retval->value.binary.data = value; + retval->value.binary.datasize = datasize; + return retval; +} + +kvp_value * +kvp_value_new_glist(const GList * value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_GLIST; + retval->value.list = kvp_glist_copy(value); + return retval; +} + +kvp_value * +kvp_value_new_glist_nc(GList * value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_GLIST; + retval->value.list = value; + return retval; +} + +kvp_value * +kvp_value_new_frame(const kvp_frame * value) { + kvp_value * retval = g_new0(kvp_value, 1); + retval->type = KVP_TYPE_FRAME; + retval->value.frame = kvp_frame_copy(value); + return retval; +} + +void +kvp_value_delete(kvp_value * value) { + if(!value) return; + + switch(value->type) { + case KVP_TYPE_STRING: + g_free(value->value.str); + break; + case KVP_TYPE_GUID: + g_free(value->value.guid); + break; + case KVP_TYPE_BINARY: + g_free(value->value.binary.data); + break; + case KVP_TYPE_GLIST: + kvp_glist_delete(value->value.list); + break; + case KVP_TYPE_FRAME: + kvp_frame_delete(value->value.frame); + break; + + case KVP_TYPE_GINT64: + case KVP_TYPE_DOUBLE: + case KVP_TYPE_NUMERIC: + default: + break; + } + g_free(value); +} + +kvp_value_t +kvp_value_get_type(const kvp_value * value) { + if (!value) return -1; + return value->type; +} + +gint64 +kvp_value_get_gint64(const kvp_value * value) { + if (!value) return 0; + if(value->type == KVP_TYPE_GINT64) { + return value->value.int64; + } + else { + return 0; + } +} + +double +kvp_value_get_double(const kvp_value * value) { + if (!value) return 0.0; + if(value->type == KVP_TYPE_DOUBLE) { + return value->value.dbl; + } + else { + return 0.0; + } +} + +gnc_numeric +kvp_value_get_numeric(const kvp_value * value) { + if (!value) return gnc_numeric_zero (); + if(value->type == KVP_TYPE_NUMERIC) { + return value->value.numeric; + } + else { + return gnc_numeric_zero (); + } +} + +char * +kvp_value_get_string(const kvp_value * value) { + if (!value) return NULL; + if(value->type == KVP_TYPE_STRING) { + return value->value.str; + } + else { + return NULL; + } +} + +GUID * +kvp_value_get_guid(const kvp_value * value) { + if (!value) return NULL; + if(value->type == KVP_TYPE_GUID) { + return value->value.guid; + } + else { + return NULL; + } +} + +void * +kvp_value_get_binary(const kvp_value * value, guint64 * size_return) { + if (!value) + { + if (size_return) + *size_return = 0; + return NULL; + } + + if(value->type == KVP_TYPE_BINARY) { + if (size_return) + *size_return = value->value.binary.datasize; + return value->value.binary.data; + } + else { + if (size_return) + *size_return = 0; + return NULL; + } +} + +GList * +kvp_value_get_glist(const kvp_value * value) { + if (!value) return NULL; + if(value->type == KVP_TYPE_GLIST) { + return value->value.list; + } + else { + return NULL; + } +} + +kvp_frame * +kvp_value_get_frame(const kvp_value * value) { + if (!value) return NULL; + if(value->type == KVP_TYPE_FRAME) { + return value->value.frame; + } + else { + return NULL; + } +} + +/* manipulators */ + +kvp_value * +kvp_value_copy(const kvp_value * value) { + + if(!value) return NULL; + + switch(value->type) { + case KVP_TYPE_GINT64: + return kvp_value_new_gint64(value->value.int64); + break; + case KVP_TYPE_DOUBLE: + return kvp_value_new_double(value->value.dbl); + break; + case KVP_TYPE_NUMERIC: + return kvp_value_new_gnc_numeric(value->value.numeric); + break; + case KVP_TYPE_STRING: + return kvp_value_new_string(value->value.str); + break; + case KVP_TYPE_GUID: + return kvp_value_new_guid(value->value.guid); + break; + case KVP_TYPE_BINARY: + return kvp_value_new_binary(value->value.binary.data, + value->value.binary.datasize); + break; + case KVP_TYPE_GLIST: + return kvp_value_new_glist(value->value.list); + break; + case KVP_TYPE_FRAME: + return kvp_value_new_frame(value->value.frame); + break; + } + return NULL; +} + +void +kvp_frame_for_each_slot(kvp_frame *f, + void (*proc)(const char *key, + kvp_value *value, + gpointer data), + gpointer data) { + + if(!f) return; + if(!proc) return; + if(!(f->hash)) return; + + g_hash_table_foreach(f->hash, (GHFunc) proc, data); +} + +gint +double_compare(double d1, double d2) +{ + if(isnan(d1) && isnan(d2)) return 0; + if(d1 < d2) return -1; + if(d1 > d2) return 1; + return 0; +} + +gint +kvp_value_compare(const kvp_value * kva, const kvp_value * kvb) { + if(kva == kvb) return 0; + /* nothing is always less than something */ + if(!kva && kvb) return -1; + if(kva && !kvb) return 1; + + if(kva->type < kvb->type) return -1; + if(kva->type > kvb->type) return 1; + + switch(kva->type) { + case KVP_TYPE_GINT64: + if(kva->value.int64 < kvb->value.int64) return -1; + if(kva->value.int64 > kvb->value.int64) return 1; + return 0; + break; + case KVP_TYPE_DOUBLE: + return double_compare(kva->value.dbl, kvb->value.dbl); + break; + case KVP_TYPE_NUMERIC: + return gnc_numeric_compare (kva->value.numeric, kvb->value.numeric); + break; + case KVP_TYPE_STRING: + return strcmp(kva->value.str, kvb->value.str); + break; + case KVP_TYPE_GUID: + return guid_compare(kva->value.guid, kvb->value.guid); + break; + case KVP_TYPE_BINARY: + /* I don't know that this is a good compare. Ab is bigger than Acef. + But I'm not sure that actually matters here. */ + if(kva->value.binary.datasize < kvb->value.binary.datasize) return -1; + if(kva->value.binary.datasize > kvb->value.binary.datasize) return 1; + return memcmp(kva->value.binary.data, + kvb->value.binary.data, + kva->value.binary.datasize); + break; + case KVP_TYPE_GLIST: + return kvp_glist_compare(kva->value.list, kvb->value.list); + break; + case KVP_TYPE_FRAME: + return kvp_frame_compare(kva->value.frame, kvb->value.frame); + break; + } + PERR ("reached unreachable code."); + return FALSE; +} + +typedef struct { + gint compare; + kvp_frame *other_frame; +} kvp_frame_cmp_status; + +static void +kvp_frame_compare_helper(const char *key, kvp_value* val, gpointer data) { + kvp_frame_cmp_status *status = (kvp_frame_cmp_status *) data; + if(status->compare == 0) { + kvp_frame *other_frame = status->other_frame; + kvp_value *other_val = kvp_frame_get_slot(other_frame, key); + + if(other_val) { + status->compare = kvp_value_compare(val, other_val); + } else { + status->compare = 1; + } + } +} + +gint +kvp_frame_compare(const kvp_frame *fa, const kvp_frame *fb) { + kvp_frame_cmp_status status; + + if(fa == fb) return 0; + /* nothing is always less than something */ + if(!fa && fb) return -1; + if(fa && !fb) return 1; + + /* nothing is always less than something */ + if(!fa->hash && fb->hash) return -1; + if(fa->hash && !fb->hash) return 1; + + status.compare = 0; + status.other_frame = (kvp_frame *) fb; + + kvp_frame_for_each_slot((kvp_frame *) fa, kvp_frame_compare_helper, &status); + + return(status.compare); +} + +gchar* +binary_to_string(const void *data, guint32 size) +{ + GString *output; + guint32 i; + guchar *data_str = (guchar*)data; + + output = g_string_sized_new(size * sizeof(char)); + + for(i = 0; i < size; i++) + { + g_string_sprintfa(output, "%02x", (unsigned int) (data_str[i])); + } + + return output->str; +} + +gchar* +kvp_value_glist_to_string(const GList *list) +{ + gchar *tmp1; + gchar *tmp2; + const GList *cursor; + + tmp1 = g_strdup_printf("[ "); + + for(cursor = list; cursor; cursor = cursor->next) + { + gchar *tmp3; + + tmp3 = kvp_value_to_string((kvp_value*)cursor->data); + tmp2 = g_strdup_printf("%s %s,", tmp1, tmp3 ? tmp3 : ""); + g_free(tmp1); + g_free(tmp3); + tmp1 = tmp2; + } + + tmp2 = g_strdup_printf("%s ]", tmp1); + g_free(tmp1); + + return tmp2; +} + +gchar* +kvp_value_to_string(const kvp_value *val) +{ + gchar *tmp1; + gchar *tmp2; + + g_return_val_if_fail(val, NULL); + + switch(kvp_value_get_type(val)) + { + case KVP_TYPE_GINT64: + return g_strdup_printf("KVP_VALUE_GINT64(%lld)", + (long long int) kvp_value_get_gint64(val)); + break; + + case KVP_TYPE_DOUBLE: + return g_strdup_printf("KVP_VALUE_DOUBLE(%g)", + kvp_value_get_double(val)); + break; + + case KVP_TYPE_NUMERIC: + tmp1 = gnc_numeric_to_string(kvp_value_get_numeric(val)); + tmp2 = g_strdup_printf("KVP_VALUE_NUMERIC(%s)", tmp1 ? tmp1 : ""); + g_free(tmp1); + return tmp2; + break; + + case KVP_TYPE_STRING: + tmp1 = kvp_value_get_string (val); + return g_strdup_printf("KVP_VALUE_STRING(%s)", tmp1 ? tmp1 : ""); + break; + + case KVP_TYPE_GUID: + tmp1 = guid_to_string(kvp_value_get_guid(val)); + tmp2 = g_strdup_printf("KVP_VALUE_GUID(%s)", tmp1 ? tmp1 : ""); + g_free(tmp1); + return tmp2; + break; + + case KVP_TYPE_BINARY: + { + guint64 len; + void *data; + data = kvp_value_get_binary(val, &len); + tmp1 = binary_to_string(data, len); + return g_strdup_printf("KVP_VALUE_BINARY(%s)", tmp1 ? tmp1 : ""); + } + break; + + case KVP_TYPE_GLIST: + tmp1 = kvp_value_glist_to_string(kvp_value_get_glist(val)); + tmp2 = g_strdup_printf("KVP_VALUE_GLIST(%s)", tmp1 ? tmp1 : ""); + g_free(tmp1); + return tmp2; + break; + + case KVP_TYPE_FRAME: + tmp1 = kvp_frame_to_string(kvp_value_get_frame(val)); + tmp2 = g_strdup_printf("KVP_VALUE_FRAME(%s)", tmp1 ? tmp1 : ""); + g_free(tmp1); + return tmp2; + break; + + default: + return g_strdup_printf(" "); + break; + } +} + +static void +kvp_frame_to_string_helper(gpointer key, gpointer value, gpointer data) +{ + gchar *tmp_val; + gchar **str = (gchar**)data; + gchar *old_data = *str; + + tmp_val = kvp_value_to_string((kvp_value*)value); + + *str = g_strdup_printf("%s %s => %s,\n", + *str ? *str : "", + key ? (char *) key : "", + tmp_val ? tmp_val : ""); + + g_free(old_data); + g_free(tmp_val); +} + +gchar* +kvp_frame_to_string(const kvp_frame *frame) +{ + gchar *tmp1; + + g_return_val_if_fail (frame != NULL, NULL); + + tmp1 = g_strdup_printf("{\n"); + + g_hash_table_foreach(frame->hash, kvp_frame_to_string_helper, &tmp1); + + { + gchar *tmp2; + tmp2 = g_strdup_printf("%s}\n", tmp1); + g_free(tmp1); + tmp1 = tmp2; + } + + return tmp1; +} + +GHashTable* +kvp_frame_get_hash(const kvp_frame *frame) +{ + g_return_val_if_fail (frame != NULL, NULL); + return frame->hash; +} diff --git a/src/engine/kvp_frame.h b/src/engine/kvp_frame.h new file mode 100644 index 0000000000..b4aa13e912 --- /dev/null +++ b/src/engine/kvp_frame.h @@ -0,0 +1,197 @@ +/********************************************************************\ + * kvp_frame.h -- a key-value frame system for gnucash. * + * Copyright (C) 2000 Bill Gribble * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * + * Boston, MA 02111-1307, USA gnu@gnu.org * + * * +\********************************************************************/ + +#ifndef KVP_FRAME_H +#define KVP_FRAME_H + +#include + +#include "gnc-numeric.h" +#include "guid.h" + + +/* a kvp_frame is a set of associations between character strings + * (keys) and kvp_value structures. A kvp_value is a union with + * possible types enumerated in the kvp_value_t enum. + * + * Pointers passed as arguments into set_slot and get_slot are the + * responsibility of the caller. Pointers returned by get_slot are + * owned by the kvp_frame. Make copies as needed. + * + * kvp_frame_delete and kvp_value_delete are deep (recursive) deletes. + * kvp_frame_copy and kvp_value_copy are deep value copies. + */ + +typedef enum { + KVP_TYPE_GINT64, + KVP_TYPE_DOUBLE, + KVP_TYPE_NUMERIC, + KVP_TYPE_STRING, + KVP_TYPE_GUID, + KVP_TYPE_BINARY, + KVP_TYPE_GLIST, + KVP_TYPE_FRAME +} kvp_value_t; + +typedef struct _kvp_frame kvp_frame; +typedef struct _kvp_value kvp_value; + + +/* kvp_frame functions */ +kvp_frame * kvp_frame_new(void); +void kvp_frame_delete(kvp_frame * frame); +kvp_frame * kvp_frame_copy(const kvp_frame * frame); +gboolean kvp_frame_is_empty(kvp_frame * frame); + +gchar* kvp_frame_to_string(const kvp_frame *frame); +gchar* binary_to_string(const void *data, guint32 size); +gchar* kvp_value_glist_to_string(const GList *list); + +GHashTable* kvp_frame_get_hash(const kvp_frame *frame); + +/* The kvp_frame_set_slot() routine copies the value into the frame, + * associating it with 'key'. + * The kvp_frame_set_slot_nc() routine puts the value (without copying + * it) into the frame, associating it with 'key'. This routine is + * handy for avoiding excess memory allocations & frees. + * Pointers passed as arguments into set_slot and get_slot are the + * responsibility of the caller. Pointers returned by get_slot are + * owned by the kvp_frame. Make copies as needed. + */ +void kvp_frame_set_slot(kvp_frame * frame, + const char * key, const kvp_value * value); +void kvp_frame_set_slot_nc(kvp_frame * frame, + const char * key, kvp_value * value); +kvp_value * kvp_frame_get_slot(kvp_frame * frame, + const char * key); + +/* The kvp_frame_set_slot_path() routines walk the hierarchy, + * using the key falues to pick each branch. When the terminal node + * is reached, the value is copied into it. + */ +void kvp_frame_set_slot_path (kvp_frame *frame, + const kvp_value *value, + const char *first_key, ...); + +void kvp_frame_set_slot_path_gslist (kvp_frame *frame, + const kvp_value *value, + GSList *key_path); + +/* The following routines return the last frame of the path. + * If the frame path doesn't exist, it is created. + * + * The kvp_frame_get_frame_slash() routine takes a single string + * where the keys are separated by slashes; thus, for example: + * /this/is/a/valid/path and///so//is////this/ + * Multiple slashes are compresed. leading slash is optional. + * The pointers . and .. are *not* followed/obeyed. (This is + * arguably a bug that needs fixing). + * + * + */ +kvp_frame * kvp_frame_get_frame (kvp_frame *frame, ...); + +kvp_frame * kvp_frame_get_frame_gslist (kvp_frame *frame, + GSList *key_path); + +kvp_frame * kvp_frame_get_frame_slash (kvp_frame *frame, + const char *path); + +/* The following routines return the value att the end of the + * path, or NULL if any portion of the path doesn't exist. + */ +kvp_value * kvp_frame_get_slot_path (kvp_frame *frame, + const char *first_key, ...); + +kvp_value * kvp_frame_get_slot_path_gslist (kvp_frame *frame, + GSList *key_path); + +gint kvp_frame_compare(const kvp_frame *fa, const kvp_frame *fb); +gint double_compare(double v1, double v2); + +void kvp_value_delete(kvp_value * value); +kvp_value * kvp_value_copy(const kvp_value * value); +/** + * Similar returns as strcmp. + **/ +gint kvp_value_compare(const kvp_value *va, const kvp_value *vb); + +/* list convenience funcs. */ +gint kvp_glist_compare(const GList * list1, const GList * list2); + +GList * kvp_glist_copy(const GList * list); + /* deep copy: same as mapping kvp_value_copy over the + elements and then copying the spine. */ +void kvp_glist_delete(GList * list); + /* deep delete: same as mapping kvp_value_delete over the + elements and then deleting the GList. */ + +/* value constructors (copying for pointer args) */ +kvp_value * kvp_value_new_gint64(gint64 value); +kvp_value * kvp_value_new_double(double value); +kvp_value * kvp_value_new_gnc_numeric(gnc_numeric value); +kvp_value * kvp_value_new_string(const char * value); +kvp_value * kvp_value_new_guid(const GUID * guid); +kvp_value * kvp_value_new_binary(const void * data, guint64 datasize); +kvp_value * kvp_value_new_glist(const GList * value); +kvp_value * kvp_value_new_frame(const kvp_frame * value); + +/* value constructors (non-copying - kvp_value takes pointer ownership) + values *must* have been allocated via glib allocators! (gnew, etc.) */ +kvp_value * kvp_value_new_binary_nc(void * data, guint64 datasize); +kvp_value * kvp_value_new_glist_nc(GList *lst); + +/* value accessors (NON-copying for frames/lists/guids/binary) */ +kvp_value_t kvp_value_get_type(const kvp_value * value); + +gint64 kvp_value_get_gint64(const kvp_value * value); +double kvp_value_get_double(const kvp_value * value); +gnc_numeric kvp_value_get_numeric(const kvp_value * value); +char * kvp_value_get_string(const kvp_value * value); +GUID * kvp_value_get_guid(const kvp_value * value); +void * kvp_value_get_binary(const kvp_value * value, + guint64 * size_return); +GList * kvp_value_get_glist(const kvp_value * value); +kvp_frame * kvp_value_get_frame(const kvp_value * value); + +gchar* kvp_value_to_string(const kvp_value *val); + +/* manipulators */ + +gboolean kvp_value_binary_append(kvp_value *v, void *data, guint64 size); +/* copying - but more efficient than creating a new kvp_value manually. */ + +/* iterators */ + +/* Traverse all of the slots in the given kvp_frame. This function + does not descend recursively to traverse any kvp_frames stored as + slot values. You must handle that in proc, with a suitable + recursive call if desired. */ +void kvp_frame_for_each_slot(kvp_frame *f, + void (*proc)(const char *key, + kvp_value *value, + gpointer data), + gpointer data); + + +#endif diff --git a/src/engine/md5.c b/src/engine/md5.c new file mode 100644 index 0000000000..465ebe1284 --- /dev/null +++ b/src/engine/md5.c @@ -0,0 +1,444 @@ +/* md5.c - Functions to compute MD5 message digest of files or memory blocks + according to the definition of MD5 in RFC 1321 from April 1992. + Copyright (C) 1995, 1996 Free Software Foundation, Inc. + NOTE: The canonical source of this file is maintained with the GNU C + Library. Bugs can be reported to bug-glibc@prep.ai.mit.edu. + + 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ + +/* Written by Ulrich Drepper , 1995. */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include + +#if STDC_HEADERS || defined _LIBC +# include +# include +#else +# ifndef HAVE_MEMCPY +# define memcpy(d, s, n) bcopy ((s), (d), (n)) +# endif +#endif + +#include "md5.h" + +#ifdef _LIBC +# include +# if __BYTE_ORDER == __BIG_ENDIAN +# define WORDS_BIGENDIAN 1 +# endif +#endif + +#ifdef WORDS_BIGENDIAN +# define SWAP(n) \ + (((n) << 24) | (((n) & 0xff00) << 8) | (((n) >> 8) & 0xff00) | ((n) >> 24)) +#else +# define SWAP(n) (n) +#endif + + +/* This array contains the bytes used to pad the buffer to the next + 64-byte boundary. (RFC 1321, 3.1: Step 1) */ +static const unsigned char fillbuf[64] = { 0x80, 0 /* , 0, 0, ... */ }; + + +/* Initialize structure containing state of computation. + (RFC 1321, 3.3: Step 3) */ +void +md5_init_ctx (ctx) + struct md5_ctx *ctx; +{ + ctx->A = 0x67452301; + ctx->B = 0xefcdab89; + ctx->C = 0x98badcfe; + ctx->D = 0x10325476; + + ctx->total[0] = ctx->total[1] = 0; + ctx->buflen = 0; +} + +/* Put result from CTX in first 16 bytes following RESBUF. The result + must be in little endian byte order. + + IMPORTANT: On some systems it is required that RESBUF is correctly + aligned for a 32 bits value. */ +void * +md5_read_ctx (ctx, resbuf) + const struct md5_ctx *ctx; + void *resbuf; +{ + ((md5_uint32 *) resbuf)[0] = SWAP (ctx->A); + ((md5_uint32 *) resbuf)[1] = SWAP (ctx->B); + ((md5_uint32 *) resbuf)[2] = SWAP (ctx->C); + ((md5_uint32 *) resbuf)[3] = SWAP (ctx->D); + + return resbuf; +} + +/* Process the remaining bytes in the internal buffer and the usual + prolog according to the standard and write the result to RESBUF. + + IMPORTANT: On some systems it is required that RESBUF is correctly + aligned for a 32 bits value. */ +void * +md5_finish_ctx (ctx, resbuf) + struct md5_ctx *ctx; + void *resbuf; +{ + /* Take yet unprocessed bytes into account. */ + md5_uint32 bytes = ctx->buflen; + size_t pad; + + /* Now count remaining bytes. */ + ctx->total[0] += bytes; + if (ctx->total[0] < bytes) + ++ctx->total[1]; + + pad = bytes >= 56 ? 64 + 56 - bytes : 56 - bytes; + memcpy (&ctx->buffer[bytes], fillbuf, pad); + + /* Put the 64-bit file length in *bits* at the end of the buffer. */ + *(md5_uint32 *) &ctx->buffer[bytes + pad] = SWAP (ctx->total[0] << 3); + *(md5_uint32 *) &ctx->buffer[bytes + pad + 4] = SWAP ((ctx->total[1] << 3) | + (ctx->total[0] >> 29)); + + /* Process last bytes. */ + md5_process_block (ctx->buffer, bytes + pad + 8, ctx); + + return md5_read_ctx (ctx, resbuf); +} + +/* Compute MD5 message digest for bytes read from STREAM. The + resulting message digest number will be written into the 16 bytes + beginning at RESBLOCK. */ +int +md5_stream (stream, resblock) + FILE *stream; + void *resblock; +{ + /* Important: BLOCKSIZE must be a multiple of 64. */ +#define BLOCKSIZE 4096 + struct md5_ctx ctx; + char buffer[BLOCKSIZE + 72]; + size_t sum; + + /* Initialize the computation context. */ + md5_init_ctx (&ctx); + + /* Iterate over full file contents. */ + while (1) + { + /* We read the file in blocks of BLOCKSIZE bytes. One call of the + computation function processes the whole buffer so that with the + next round of the loop another block can be read. */ + size_t n; + sum = 0; + + /* Read block. Take care for partial reads. */ + do + { + n = fread (buffer + sum, 1, BLOCKSIZE - sum, stream); + + sum += n; + } + while (sum < BLOCKSIZE && n != 0); + if (n == 0 && ferror (stream)) + return 1; + + /* If end of file is reached, end the loop. */ + if (n == 0) + break; + + /* Process buffer with BLOCKSIZE bytes. Note that + BLOCKSIZE % 64 == 0 + */ + md5_process_block (buffer, BLOCKSIZE, &ctx); + } + + /* Add the last bytes if necessary. */ + if (sum > 0) + md5_process_bytes (buffer, sum, &ctx); + + /* Construct result in desired memory. */ + md5_finish_ctx (&ctx, resblock); + return 0; +} + +/* Compute MD5 message digest for LEN bytes beginning at BUFFER. The + result is always in little endian byte order, so that a byte-wise + output yields to the wanted ASCII representation of the message + digest. */ +void * +md5_buffer (buffer, len, resblock) + const char *buffer; + size_t len; + void *resblock; +{ + struct md5_ctx ctx; + + /* Initialize the computation context. */ + md5_init_ctx (&ctx); + + /* Process whole buffer but last len % 64 bytes. */ + md5_process_bytes (buffer, len, &ctx); + + /* Put result in desired memory area. */ + return md5_finish_ctx (&ctx, resblock); +} + + +void +md5_process_bytes (buffer, len, ctx) + const void *buffer; + size_t len; + struct md5_ctx *ctx; +{ +#define NUM_MD5_WORDS 1024 + size_t add = 0; + + /* When we already have some bits in our internal buffer concatenate + both inputs first. */ + if (ctx->buflen != 0) + { + size_t left_over = ctx->buflen; + + add = 128 - left_over > len ? len : 128 - left_over; + + memcpy (&ctx->buffer[left_over], buffer, add); + ctx->buflen += add; + + if (left_over + add > 64) + { + md5_process_block (ctx->buffer, (left_over + add) & ~63, ctx); + /* The regions in the following copy operation cannot overlap. */ + memcpy (ctx->buffer, &ctx->buffer[(left_over + add) & ~63], + (left_over + add) & 63); + ctx->buflen = (left_over + add) & 63; + } + + buffer = (const char *) buffer + add; + len -= add; + } + + /* Process available complete blocks. */ + if (len > 64) + { + if ((add & 3) == 0) /* buffer is still 32-bit aligned */ + { + md5_process_block (buffer, len & ~63, ctx); + buffer = (const char *) buffer + (len & ~63); + } + else /* buffer is not 32-bit aligned */ + { + md5_uint32 md5_buffer[NUM_MD5_WORDS]; + size_t num_bytes; + size_t buf_bytes; + + num_bytes = len & ~63; + while (num_bytes > 0) + { + buf_bytes = (num_bytes < sizeof(md5_buffer)) ? + num_bytes : sizeof(md5_buffer); + memcpy (md5_buffer, buffer, buf_bytes); + md5_process_block (md5_buffer, buf_bytes, ctx); + num_bytes -= buf_bytes; + buffer = (const char *) buffer + buf_bytes; + } + } + + len &= 63; + } + + /* Move remaining bytes in internal buffer. */ + if (len > 0) + { + memcpy (ctx->buffer, buffer, len); + ctx->buflen = len; + } +} + + +/* These are the four functions used in the four steps of the MD5 algorithm + and defined in the RFC 1321. The first function is a little bit optimized + (as found in Colin Plumbs public domain implementation). */ +/* #define FF(b, c, d) ((b & c) | (~b & d)) */ +#define FF(b, c, d) (d ^ (b & (c ^ d))) +#define FG(b, c, d) FF (d, b, c) +#define FH(b, c, d) (b ^ c ^ d) +#define FI(b, c, d) (c ^ (b | ~d)) + +/* Process LEN bytes of BUFFER, accumulating context into CTX. + It is assumed that LEN % 64 == 0. */ + +void +md5_process_block (buffer, len, ctx) + const void *buffer; + size_t len; + struct md5_ctx *ctx; +{ + md5_uint32 correct_words[16]; + const md5_uint32 *words = buffer; + size_t nwords = len / sizeof (md5_uint32); + const md5_uint32 *endp = words + nwords; + md5_uint32 A = ctx->A; + md5_uint32 B = ctx->B; + md5_uint32 C = ctx->C; + md5_uint32 D = ctx->D; + + /* First increment the byte count. RFC 1321 specifies the possible + length of the file up to 2^64 bits. Here we only compute the + number of bytes. Do a double word increment. */ + ctx->total[0] += len; + if (ctx->total[0] < len) + ++ctx->total[1]; + + /* Process all bytes in the buffer with 64 bytes in each round of + the loop. */ + while (words < endp) + { + md5_uint32 *cwp = correct_words; + md5_uint32 A_save = A; + md5_uint32 B_save = B; + md5_uint32 C_save = C; + md5_uint32 D_save = D; + + /* First round: using the given function, the context and a constant + the next context is computed. Because the algorithms processing + unit is a 32-bit word and it is determined to work on words in + little endian byte order we perhaps have to change the byte order + before the computation. To reduce the work for the next steps + we store the swapped words in the array CORRECT_WORDS. */ + +#define OP(a, b, c, d, s, T) \ + do \ + { \ + a += FF (b, c, d) + (*cwp++ = SWAP (*words)) + T; \ + ++words; \ + CYCLIC (a, s); \ + a += b; \ + } \ + while (0) + + /* It is unfortunate that C does not provide an operator for + cyclic rotation. Hope the C compiler is smart enough. */ +#define CYCLIC(w, s) (w = (w << s) | (w >> (32 - s))) + + /* Before we start, one word to the strange constants. + They are defined in RFC 1321 as + + T[i] = (int) (4294967296.0 * fabs (sin (i))), i=1..64 + */ + + /* Round 1. */ + OP (A, B, C, D, 7, 0xd76aa478); + OP (D, A, B, C, 12, 0xe8c7b756); + OP (C, D, A, B, 17, 0x242070db); + OP (B, C, D, A, 22, 0xc1bdceee); + OP (A, B, C, D, 7, 0xf57c0faf); + OP (D, A, B, C, 12, 0x4787c62a); + OP (C, D, A, B, 17, 0xa8304613); + OP (B, C, D, A, 22, 0xfd469501); + OP (A, B, C, D, 7, 0x698098d8); + OP (D, A, B, C, 12, 0x8b44f7af); + OP (C, D, A, B, 17, 0xffff5bb1); + OP (B, C, D, A, 22, 0x895cd7be); + OP (A, B, C, D, 7, 0x6b901122); + OP (D, A, B, C, 12, 0xfd987193); + OP (C, D, A, B, 17, 0xa679438e); + OP (B, C, D, A, 22, 0x49b40821); + + /* For the second to fourth round we have the possibly swapped words + in CORRECT_WORDS. Redefine the macro to take an additional first + argument specifying the function to use. */ +#undef OP +#define OP(f, a, b, c, d, k, s, T) \ + do \ + { \ + a += f (b, c, d) + correct_words[k] + T; \ + CYCLIC (a, s); \ + a += b; \ + } \ + while (0) + + /* Round 2. */ + OP (FG, A, B, C, D, 1, 5, 0xf61e2562); + OP (FG, D, A, B, C, 6, 9, 0xc040b340); + OP (FG, C, D, A, B, 11, 14, 0x265e5a51); + OP (FG, B, C, D, A, 0, 20, 0xe9b6c7aa); + OP (FG, A, B, C, D, 5, 5, 0xd62f105d); + OP (FG, D, A, B, C, 10, 9, 0x02441453); + OP (FG, C, D, A, B, 15, 14, 0xd8a1e681); + OP (FG, B, C, D, A, 4, 20, 0xe7d3fbc8); + OP (FG, A, B, C, D, 9, 5, 0x21e1cde6); + OP (FG, D, A, B, C, 14, 9, 0xc33707d6); + OP (FG, C, D, A, B, 3, 14, 0xf4d50d87); + OP (FG, B, C, D, A, 8, 20, 0x455a14ed); + OP (FG, A, B, C, D, 13, 5, 0xa9e3e905); + OP (FG, D, A, B, C, 2, 9, 0xfcefa3f8); + OP (FG, C, D, A, B, 7, 14, 0x676f02d9); + OP (FG, B, C, D, A, 12, 20, 0x8d2a4c8a); + + /* Round 3. */ + OP (FH, A, B, C, D, 5, 4, 0xfffa3942); + OP (FH, D, A, B, C, 8, 11, 0x8771f681); + OP (FH, C, D, A, B, 11, 16, 0x6d9d6122); + OP (FH, B, C, D, A, 14, 23, 0xfde5380c); + OP (FH, A, B, C, D, 1, 4, 0xa4beea44); + OP (FH, D, A, B, C, 4, 11, 0x4bdecfa9); + OP (FH, C, D, A, B, 7, 16, 0xf6bb4b60); + OP (FH, B, C, D, A, 10, 23, 0xbebfbc70); + OP (FH, A, B, C, D, 13, 4, 0x289b7ec6); + OP (FH, D, A, B, C, 0, 11, 0xeaa127fa); + OP (FH, C, D, A, B, 3, 16, 0xd4ef3085); + OP (FH, B, C, D, A, 6, 23, 0x04881d05); + OP (FH, A, B, C, D, 9, 4, 0xd9d4d039); + OP (FH, D, A, B, C, 12, 11, 0xe6db99e5); + OP (FH, C, D, A, B, 15, 16, 0x1fa27cf8); + OP (FH, B, C, D, A, 2, 23, 0xc4ac5665); + + /* Round 4. */ + OP (FI, A, B, C, D, 0, 6, 0xf4292244); + OP (FI, D, A, B, C, 7, 10, 0x432aff97); + OP (FI, C, D, A, B, 14, 15, 0xab9423a7); + OP (FI, B, C, D, A, 5, 21, 0xfc93a039); + OP (FI, A, B, C, D, 12, 6, 0x655b59c3); + OP (FI, D, A, B, C, 3, 10, 0x8f0ccc92); + OP (FI, C, D, A, B, 10, 15, 0xffeff47d); + OP (FI, B, C, D, A, 1, 21, 0x85845dd1); + OP (FI, A, B, C, D, 8, 6, 0x6fa87e4f); + OP (FI, D, A, B, C, 15, 10, 0xfe2ce6e0); + OP (FI, C, D, A, B, 6, 15, 0xa3014314); + OP (FI, B, C, D, A, 13, 21, 0x4e0811a1); + OP (FI, A, B, C, D, 4, 6, 0xf7537e82); + OP (FI, D, A, B, C, 11, 10, 0xbd3af235); + OP (FI, C, D, A, B, 2, 15, 0x2ad7d2bb); + OP (FI, B, C, D, A, 9, 21, 0xeb86d391); + + /* Add the starting values of the context. */ + A += A_save; + B += B_save; + C += C_save; + D += D_save; + } + + /* Put checksum in context given as argument. */ + ctx->A = A; + ctx->B = B; + ctx->C = C; + ctx->D = D; +} diff --git a/src/engine/md5.h b/src/engine/md5.h new file mode 100644 index 0000000000..346bfec3a6 --- /dev/null +++ b/src/engine/md5.h @@ -0,0 +1,163 @@ +/* md5.h - Declaration of functions and data types used for MD5 sum + computing library functions. + Copyright (C) 1995, 1996 Free Software Foundation, Inc. + NOTE: The canonical source of this file is maintained with the GNU C + Library. Bugs can be reported to bug-glibc@prep.ai.mit.edu. + + 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ + +#ifndef _MD5_H +#define _MD5_H 1 + +#include +#include + +#if defined HAVE_LIMITS_H || _LIBC +# include +#endif + +/* The following contortions are an attempt to use the C preprocessor + to determine an unsigned integral type that is 32 bits wide. An + alternative approach is to use autoconf's AC_CHECK_SIZEOF macro, but + doing that would require that the configure script compile and *run* + the resulting executable. Locally running cross-compiled executables + is usually not possible. */ + +#ifdef _LIBC +# include +typedef u_int32_t md5_uint32; +#else +# if defined __STDC__ && __STDC__ +# define UINT_MAX_32_BITS 4294967295U +# else +# define UINT_MAX_32_BITS 0xFFFFFFFF +# endif + +/* If UINT_MAX isn't defined, assume it's a 32-bit type. + This should be valid for all systems GNU cares about because + that doesn't include 16-bit systems, and only modern systems + (that certainly have ) have 64+-bit integral types. */ + +# ifndef UINT_MAX +# define UINT_MAX UINT_MAX_32_BITS +# endif + +# if UINT_MAX == UINT_MAX_32_BITS + typedef unsigned int md5_uint32; +# else +# if USHRT_MAX == UINT_MAX_32_BITS + typedef unsigned short md5_uint32; +# else +# if ULONG_MAX == UINT_MAX_32_BITS + typedef unsigned long md5_uint32; +# else + /* The following line is intended to evoke an error. + Using #error is not portable enough. */ + "Cannot determine unsigned 32-bit data type." +# endif +# endif +# endif +#endif + +#undef __P +#if defined (__STDC__) && __STDC__ +#define __P(x) x +#else +#define __P(x) () +#endif + +/* Structure to save state of computation between the single steps. */ +struct md5_ctx +{ + md5_uint32 A; + md5_uint32 B; + md5_uint32 C; + md5_uint32 D; + + md5_uint32 total[2]; + md5_uint32 buflen; + char buffer[128]; +}; + +/* + * The following three functions are build up the low level used in + * the functions `md5_stream' and `md5_buffer'. + */ + +/* Initialize structure containing state of computation. + (RFC 1321, 3.3: Step 3) */ +extern void md5_init_ctx __P ((struct md5_ctx *ctx)); + + +/* Starting with the result of former calls of this function (or the + initialization function update the context for the next LEN bytes + starting at BUFFER. + + It is necessary that LEN is a multiple of 64!!! + + IMPORTANT: On some systems it is required that buffer be 32-bit + aligned. */ +extern void md5_process_block __P ((const void *buffer, size_t len, + struct md5_ctx *ctx)); + +/* Starting with the result of former calls of this function (or the + initialization function) update the context for the next LEN bytes + starting at BUFFER. + + It is NOT required that LEN is a multiple of 64. + + IMPORTANT: On some systems it is required that buffer be 32-bit + aligned. */ +extern void md5_process_bytes __P ((const void *buffer, size_t len, + struct md5_ctx *ctx)); + +/* Process the remaining bytes in the buffer and put result from CTX + in first 16 bytes following RESBUF. The result is always in little + endian byte order, so that a byte-wise output yields to the wanted + ASCII representation of the message digest. + + IMPORTANT: On some systems it is required that RESBUF be correctly + aligned for a 32 bits value. */ +extern void *md5_finish_ctx __P ((struct md5_ctx *ctx, void *resbuf)); + + +/* Put result from CTX in first 16 bytes following RESBUF. The result is + always in little endian byte order, so that a byte-wise output yields + to the wanted ASCII representation of the message digest. + + IMPORTANT: On some systems it is required that RESBUF be correctly + aligned for a 32 bits value. */ +extern void *md5_read_ctx __P ((const struct md5_ctx *ctx, void *resbuf)); + + +/* Compute MD5 message digest for bytes read from STREAM. The + resulting message digest number will be written into the 16 bytes + beginning at RESBLOCK. + + IMPORTANT: On some systems it is required that resblock be 32-bit + aligned. */ +extern int md5_stream __P ((FILE *stream, void *resblock)); + + +/* Compute MD5 message digest for LEN bytes beginning at BUFFER. The + result is always in little endian byte order, so that a byte-wise + output yields to the wanted ASCII representation of the message + digest. + + IMPORTANT: On some systems it is required that buffer and resblock + be correctly 32-bit aligned. */ +extern void *md5_buffer __P ((const char *buffer, size_t len, void *resblock)); + +#endif