Skip to content

Commit

Permalink
zerosum: add config option "tolerance" (#16)
Browse files Browse the repository at this point in the history
* style: s/Vsia/Visa/g

* style: trim trailing whitespace for two files

* test: add two new unit tests in prep for tolerance

In preparation of the "tolerance" config option feature, add two new
unit tests:

- `test_two_matched_below_epsilon`: demonstrate that two transactions
  that logically shouldn't match can end up matching because they sum up
  to be below epsilon (aka tolerance)
- `test_two_unmatched_above_epsilon`: prepare to demonstrate that two
  transactions that logically shouldn't match can avoid matching if
  epsilon (aka tolerance) is set low enough (this test should currently
  fail because tolerance is too high!)

* feat: add epsilon_delta as config parameter

* style: S/epsilon_delta/tolerance/g

* style: s/TOLERANCE/DEFAULT_TOLERANCE/

* style: add trailing comma to config dict

* test: set tolerance to 0.0098

Set tolerance slightly lower so that `test_two_unmatched_above_epsilon`
lives up to its name and now passes.

* style: s/Visa/Green/
  • Loading branch information
jaki authored Feb 5, 2021
1 parent f410b45 commit b874459
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 27 deletions.
75 changes: 55 additions & 20 deletions beancount_reds_plugins/zerosum/test_zerosum.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from beancount import loader

config = """{
'zerosum_accounts' : {
'zerosum_accounts' : {
'Assets:Zero-Sum-Accounts:Returns-and-Temporary' : ('', 90),
},
'account_name_replace' : ('Zero-Sum-Accounts', 'ZSA-Matched')
'account_name_replace' : ('Zero-Sum-Accounts', 'ZSA-Matched'),
'tolerance' : 0.0098,
}"""

def get_entries_with_acc_regexp(entries, regexp):
Expand Down Expand Up @@ -65,19 +66,19 @@ def test_empty_config(self, entries, _, options_map):
@loader.load_doc()
def test_single_rename(self, entries, _, options_map):
"""
2015-01-01 open Liabilities:Credit-Cards:Vsia
2015-01-01 open Liabilities:Credit-Cards:Green
2015-01-01 open Assets:Zero-Sum-Accounts:Returns-and-Temporary
2015-06-15 * "Expensive furniture"
Liabilities:Credit-Cards:Vsia -2526.02 USD
Liabilities:Credit-Cards:Green -2526.02 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary 1263.01 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary 1263.01 USD
2015-06-23 * "Expensive furniture Refund"
Liabilities:Credit-Cards:Vsia 1263.01 USD
Liabilities:Credit-Cards:Green 1263.01 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary
2015-06-23 * "Expensive furniture Refund"
Liabilities:Credit-Cards:Vsia 1263.01 USD
Liabilities:Credit-Cards:Green 1263.01 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary
"""
new_entries, _ = zerosum.zerosum(entries, options_map, config)
Expand All @@ -91,16 +92,16 @@ def test_single_rename(self, entries, _, options_map):
self.assertEqual('Assets:ZSA-Matched:Returns-and-Temporary', matched[m].postings[p].account)

@loader.load_doc()
def test_above_epsilon(self, entries, _, options_map):
def test_above_tolerance(self, entries, _, options_map):
"""
2015-01-01 open Liabilities:Credit-Cards:Vsia
2015-01-01 open Liabilities:Credit-Cards:Green
2015-01-01 open Assets:Zero-Sum-Accounts:Returns-and-Temporary
2015-06-15 * "Trinket"
Liabilities:Credit-Cards:Vsia -0.014 USD
Liabilities:Credit-Cards:Green -0.014 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary
2015-06-23 * "Trinket refund"
Liabilities:Credit-Cards:Vsia 0.014 USD
Liabilities:Credit-Cards:Green 0.014 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary
"""
new_entries, _ = zerosum.zerosum(entries, options_map, config)
Expand All @@ -114,16 +115,16 @@ def test_above_epsilon(self, entries, _, options_map):
self.assertEqual('Assets:ZSA-Matched:Returns-and-Temporary', matched[m].postings[p].account)

@loader.load_doc()
def test_below_epsilon(self, entries, _, options_map):
def test_below_tolerance(self, entries, _, options_map):
"""
2015-01-01 open Liabilities:Credit-Cards:Vsia
2015-01-01 open Liabilities:Credit-Cards:Green
2015-01-01 open Assets:Zero-Sum-Accounts:Returns-and-Temporary
2015-06-15 * "Trinket"
Liabilities:Credit-Cards:Vsia -0.004 USD
Liabilities:Credit-Cards:Green -0.004 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary
2015-06-23 * "Trinket refund"
Liabilities:Credit-Cards:Vsia 0.004 USD
Liabilities:Credit-Cards:Green 0.004 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary
"""
new_entries, _ = zerosum.zerosum(entries, options_map, config)
Expand All @@ -139,7 +140,7 @@ def test_below_epsilon(self, entries, _, options_map):
@loader.load_doc()
def test_lookalike(self, entries, _, options_map):
"""
2015-01-01 open Liabilities:Credit-Cards:Vsia
2015-01-01 open Liabilities:Credit-Cards:Green
2015-01-01 open Assets:Zero-Sum-Accounts:Returns-and-Temporary
2020-06-01 * "Match two lookalike postings in one txn" ; should not error
Expand All @@ -159,7 +160,7 @@ def test_lookalike(self, entries, _, options_map):
@loader.load_doc()
def test_both_postings_in_one_txn(self, entries, _, options_map):
"""
2015-01-01 open Liabilities:Credit-Cards:Vsia
2015-01-01 open Liabilities:Credit-Cards:Green
2015-01-01 open Assets:Zero-Sum-Accounts:Returns-and-Temporary
2020-01-01 * "Match both postings in one txn"
Expand All @@ -176,4 +177,38 @@ def test_both_postings_in_one_txn(self, entries, _, options_map):
for (m, p) in ref:
self.assertEqual('Assets:ZSA-Matched:Returns-and-Temporary', matched[m].postings[p].account)

@loader.load_doc()
def test_two_matched_below_tolerance(self, entries, _, options_map):
"""
2015-01-01 open Liabilities:Credit-Cards:Green
2015-01-01 open Assets:Zero-Sum-Accounts:Returns-and-Temporary
2021-01-01 * "(two unmatched postings summing under tolerance)"
Assets:Zero-Sum-Accounts:Returns-and-Temporary -0.001 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary -0.002 USD
Liabilities:Credit-Cards:Green
"""
new_entries, _ = zerosum.zerosum(entries, options_map, config)

matched_txns = get_entries_with_acc_regexp(new_entries, ':ZSA-Matched')
self.assertEqual(1, len(matched_txns))
matched_postings = sum(map(
lambda posting: bool(re.search(':ZSA-Matched', posting.account)),
matched_txns[0].postings))
self.assertEqual(2, matched_postings)

@loader.load_doc()
def test_two_unmatched_above_tolerance(self, entries, _, options_map):
"""
2015-01-01 open Liabilities:Credit-Cards:Green
2015-01-01 open Assets:Zero-Sum-Accounts:Returns-and-Temporary
2021-01-01 * "(two unmatched postings summing under tolerance)"
Assets:Zero-Sum-Accounts:Returns-and-Temporary -0.00494 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary -0.00496 USD
Liabilities:Credit-Cards:Green
"""
new_entries, _ = zerosum.zerosum(entries, options_map, config)

matched_txns = get_entries_with_acc_regexp(new_entries, ':ZSA-Matched')
self.assertEqual(0, len(matched_txns))
7 changes: 5 additions & 2 deletions beancount_reds_plugins/zerosum/zerosum.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
from beancount.core import getters

DEBUG = 0
DEFAULT_TOLERANCE = 0.0099

__plugins__ = ('zerosum', 'flag_unmatched',)

Expand Down Expand Up @@ -202,6 +203,8 @@ def zerosum(entries, options_map, config):
- 'account_name_replace': tuple of two entries. See above
- 'tolerance': the maximum cost difference between two matching postings
- 'flag_unmatched': bool to control whether to flag unmatched
transactions as warnings (default off)
Expand All @@ -224,7 +227,7 @@ def find_match():
if p is posting:
# Don't match with the same exact posting.
continue
if (abs(p.units.number + posting.units.number) < EPSILON_DELTA
if (abs(p.units.number + posting.units.number) < tolerance
and p.account == zs_account):
return (p, t)
return None
Expand All @@ -237,11 +240,11 @@ def find_match():
config_obj = literal_eval(config) #TODO: error check
zs_accounts_list = config_obj.pop('zerosum_accounts', {})
(account_name_from, account_name_to) = config_obj.pop('account_name_replace', ('', ''))
tolerance = config_obj.pop('tolerance', DEFAULT_TOLERANCE)

new_accounts = set()
zerosum_postings_count = 0
match_count = 0
EPSILON_DELTA = 0.0099

# Build zerosum_txns_all for all zs_accounts, so we iterate through entries only once (for performance)
zerosum_txns_all = defaultdict(list)
Expand Down
10 changes: 5 additions & 5 deletions beancount_reds_plugins/zerosum/zs_test.beancount
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@ option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"

plugin "beancount_reds_plugins.zerosum.zerosum" "{
'zerosum_accounts' : {
'zerosum_accounts' : {
'Assets:Zero-Sum-Accounts:Returns-and-Temporary' : ('', 90),
},
'account_name_replace' : ('Zero-Sum-Accounts', 'ZSA-Matched')
}"


2015-06-15 * "Expensive furniture"
Liabilities:Credit-Cards:Vsia -2526.02 USD
Liabilities:Credit-Cards:Green -2526.02 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary 1263.01 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary 1263.01 USD

2015-06-23 * "Expensive furniture Refund"
Liabilities:Credit-Cards:Vsia 1263.01 USD
Liabilities:Credit-Cards:Green 1263.01 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary

2015-06-23 * "Expensive furniture Refund"
Liabilities:Credit-Cards:Vsia 1263.01 USD
Liabilities:Credit-Cards:Green 1263.01 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary

2020-01-01 * "Match both postings in one txn"
Expand All @@ -32,5 +32,5 @@ plugin "beancount_reds_plugins.zerosum.zerosum" "{
Assets:Zero-Sum-Accounts:Returns-and-Temporary 0.00 USD

2021-01-01 * "Unmatched" ; should not error
Liabilities:Credit-Cards:Vsia -0.00495 USD
Liabilities:Credit-Cards:Green -0.00495 USD
Assets:Zero-Sum-Accounts:Returns-and-Temporary

0 comments on commit b874459

Please sign in to comment.