Skip to content

Commit

Permalink
Disallow groupings with subsets that sum to zero.
Browse files Browse the repository at this point in the history
  • Loading branch information
carljm committed Dec 6, 2020
1 parent 02cd249 commit d1ce74d
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 3 deletions.
18 changes: 15 additions & 3 deletions beancount_import/matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,9 @@ def get_aggregate_posting_candidates(
3. Subsets must not contain cleared postings, or postings with a `cost` or
`price` specification, or with `MISSING` units.
4. All postings in a subset must have the same `units.currency`, and the
same sign of `units.number` (i.e. positive or negative).
4. All postings in a subset must have the same `units.currency`.
5. Subsets may not sum to zero, or contain any sub-subsets that sum to zero.
6. To limit the computational cost, subsets are limited to at most 4
elements, except that all maximal subsets are also returned.
Expand All @@ -713,7 +714,7 @@ def get_aggregate_posting_candidates(
the sum of the `units` of each posting in the subset.
"""
possible_sets = collections.OrderedDict(
) # type: Dict[Tuple[str, str, bool], List[Posting]]
) # type: Dict[Tuple[str, str], List[Posting]]
for posting in postings:
if (posting.price is not None or posting.cost is not None or
posting.units is None or posting.units is MISSING):
Expand All @@ -724,9 +725,20 @@ def get_aggregate_posting_candidates(
[]).append(posting)
results = []
max_subset_size = 4
sum_to_zero = set() # type: Set[Tuple[int, ...]]

def posting_set_id(postings):
return tuple(id(x) for x in postings)

def add_subset(account, currency, subset):
total = sum(x.units.number for x in subset)
if total == ZERO:
sum_to_zero.add(posting_set_id(subset))
return
for subsubset_size in range(2, len(subset)):
for subsubset in itertools.combinations(subset, subsubset_size):
if posting_set_id(subsubset) in sum_to_zero:
return
aggregate_posting = Posting(
account=account,
units=Amount(currency=currency, number=total),
Expand Down
81 changes: 81 additions & 0 deletions beancount_import/matching_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,3 +737,84 @@ def test_nonmatch_fuzzy_amount():
note: "B"
Expenses:FIXME -99.98 USD
""")

def test_match_grouped_differing_signs():
# Can group postings of differing signs to make a match.
assert_match(
pending_candidate="""
2020-12-05 * "Narration"
note1: "A"
Expenses:FIXME:A 1.23 USD
note1: "B"
Expenses:FIXME:A -0.12 USD
note1: "C"
Assets:Bank -1.11 USD
note2: "A"
""",
journal="""
2020-12-05 * "Narration"
note3: "E"
Assets:Bank -1.11 USD
cleared: TRUE
note3: "A"
Expenses:Foo 1.11 USD
note4: "A"
""",
matches="""
2020-12-05 * "Narration"
note1: "A"
note3: "E"
Assets:Bank -1.11 USD
cleared: TRUE
note2: "A"
note3: "A"
Expenses:Foo 1.23 USD
note1: "B"
note4: "A"
Expenses:Foo -0.12 USD
note1: "C"
note4: "A"
""",
)

def test_match_grouped_differing_signs_sum_zero():
# Cannot make a matching group that contains canceling transactions.
assert_match(
pending_candidate="""
2020-12-05 * "Narration"
note1: "A"
Expenses:FIXME 1.35 USD
note1: "B"
Expenses:FIXME 2.90 USD
note1: "D"
Expenses:FIXME -1.35 USD
note1: "F"
Expenses:FIXME -2.90 USD
note1: "G"
""",
journal="""
2020-12-05 * "Narration"
note3: "A"
Assets:Bank -1.35 USD
cleared: TRUE
note2: "A"
Expenses:Foo 1.35 USD
note3: "B"
""",
matches="""
2020-12-05 * "Narration"
note1: "A"
note3: "A"
Assets:Bank -1.35 USD
cleared: TRUE
note1: "F"
note2: "A"
Expenses:Foo 1.35 USD
note1: "B"
note3: "B"
Expenses:FIXME 2.90 USD
note1: "D"
Expenses:FIXME -2.90 USD
note1: "G"
""",
)

0 comments on commit d1ce74d

Please sign in to comment.