Skip to content

Commit

Permalink
colorize match when printing transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
Kraymer committed Jan 4, 2016
1 parent ab87fe0 commit 83dce36
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 45 deletions.
31 changes: 14 additions & 17 deletions qifqif/__init__.py
Expand Up @@ -22,7 +22,7 @@
from ordereddict import OrderedDict

from qifqif import tags
from qifqif.ui import set_completer, complete_matches
from qifqif.ui import set_completer, complete_matches, colorize_match
from qifqif.terminal import TERM

ENCODING = 'utf-8' if sys.stdin.encoding in (None, 'ascii') else \
Expand Down Expand Up @@ -177,11 +177,14 @@ def print_transaction(t, short=True, extras=None):
pad_width = 12
keys = ('date', 'amount', 'payee', 'category') if short else t.keys()
category = ''
_, _, matches = tags.find_tag_for(t)
for field in keys:
if t[field] and not field.isdigit():
line = TERM.clear_eol + '%s: %s' % (TERM.ljust(extras.get(field,
' %s' % field.title()), pad_width, '.'),
t[field] or TERM.red('<none>'))
fieldname = extras.get(field, ' ' + field.title())
line = TERM.clear_eol + '%s: %s' % (
TERM.ljust(fieldname, pad_width, '.'),
colorize_match(t, matches, field.lower()) or TERM.red('<none>')
)
if field != 'category':
print(line)
else:
Expand All @@ -193,11 +196,11 @@ def process_transaction(t, options):
"""Assign a category to a transaction.
"""
if not t['category']:
cat, match = tags.find_tag_for(t)
cat, ruler, m = tags.find_tag_for(t)
t['category'] = cat
else:
cat, match = t['category'], None
extras = {'category': '+ Category'} if (cat and match) else {}
cat, ruler = t['category'], None
extras = {'category': '+ Category'} if (cat and ruler) else {}
print('---\n' + TERM.clear_eol, end='')
print_transaction(t, extras=extras)
edit = False
Expand All @@ -207,20 +210,20 @@ def process_transaction(t, options):
msg = "\nEdit '%s' category" % TERM.green(t['category'])
edit = quick_input(msg, 'yN', vanish=True) == 'Y'
if not edit:
return t['category'], match
return t['category'], ruler

# Query for category if no cached one or edit is True
if (not cat or edit) and not options.get('batch', False):
t['category'] = query_cat(cat)
print(TERM.green('✔ Category..: ') + t['category'])
# Query match if category entered or edit
# Query ruler if category entered or edit
if t['category']:
match = query_ruler(t)
ruler = query_ruler(t)
else: # remove category
if cat:
print_transaction(t)

return t['category'], match
return t['category'], ruler


def process_file(transactions, options):
Expand Down Expand Up @@ -292,12 +295,6 @@ def dump_to_buffer(transactions):
return res


def highlight_char(word, index=0):
"""Return word with n-th letter highlighted
"""
return word[:index] + TERM.reverse(word[index]) + word[index + 1:]


def parse_args(argv):
"""Build application argument parser and parse command line.
"""
Expand Down
29 changes: 18 additions & 11 deletions qifqif/tags.py
Expand Up @@ -33,31 +33,38 @@ def unrulify(ruler):


def match(ruler, t):
"""Return a tuple (match, dict) indicating if transaction matches ruler.
match is a bool, while dict contains matching values for ruler fields.
"""
res = {}
ruler = rulify(ruler)
for field in ruler:
rule = ruler[field]
field = field.lower()
try:
if re.search(ruler[field], t[field.lower()], re.I) is None:
return False, field.lower()
m = re.search(rule, t[field], re.I)
res[field] = t[field][m.start():m.end()] if m else None
except Exception:
return False, field.lower()
return True, None
res[field] = None
return all([x for x in res.values()]), res


def find_tag_for(t):
"""If transaction matches a rule, returns corresponding tuple
(tag, rule).
(tag, ruler, match).
"""
res = []
for (tag, rulers) in TAGS.items():
for ruler in rulers:
if match(ruler, t)[0]:
res.append((tag, ruler))
m, matches = match(ruler, t)
if m:
res.append((tag, ruler, matches))
if res:
# Return rule with the most fields.
# If several, pick the ont with the longer rules.
return max(res, key=lambda (tag, ruler): (len(rulify(ruler).keys()),
sum([len(v) for v in rulify(ruler).values()])))
return None, None
return max(res, key=lambda (tag, ruler, matches): (len(rulify(
ruler).keys()), sum([len(v) for v in matches.values() if v])))
return None, None, None


def convert(tags):
Expand Down Expand Up @@ -100,7 +107,7 @@ def edit(t, tag, match, options={}):

global TAGS
match = unrulify(match)
cached_tag, cached_match = find_tag_for(t)
cached_tag, cached_match, _ = find_tag_for(t)
if tag != cached_tag:
if cached_tag:
TAGS[cached_tag].remove(cached_match)
Expand Down
22 changes: 8 additions & 14 deletions qifqif/ui.py
Expand Up @@ -8,9 +8,9 @@

import readline
import re

from difflib import SequenceMatcher

from qifqif.terminal import TERM

readline.parse_and_bind('tab: complete')

Expand Down Expand Up @@ -38,19 +38,13 @@ def partial_pattern_match(pattern, text):
return good_pattern_offset, good_text_offset


def diff(_str, candidate, term, as_error=False):
match = SequenceMatcher(None, _str.lower(), candidate.lower())
match_indexes = match.find_longest_match(0, len(_str), 0, len(candidate))
_, beg, end = match_indexes
match = candidate[beg:beg + end]
words = match.split(' ')
res = term.red(candidate[:beg]) if as_error else candidate[:beg]
for w in words:
res += (term.green(w) if re.search(r'\b%s\b' % re.escape(w), _str, re.I)
else term.yellow(w)) + ' '
res += '\b' + (term.red(candidate[beg + end:]) if as_error
else candidate[beg + end:])
return res
def colorize_match(t, matches, field):
field_val = t[field]
match = matches.get(field, '') if matches else ''
seqmatch = SequenceMatcher(None, field_val, match)
a, b, size = seqmatch.find_longest_match(0, len(field_val), 0, len(match))
return (field_val[:a] + TERM.green(field_val[a:a + size]) +
field_val[a + size:])


class InputCompleter(object):
Expand Down
6 changes: 3 additions & 3 deletions test/test_tags.py
Expand Up @@ -35,13 +35,13 @@ def test_match__ruler_no_partial_match(self):
self.assertTrue(tags.match('sully ba', TRANSACTION))

def test_find_tag_for__basic_ruler(self):
tag, ruler = tags.find_tag_for({'payee': 'SuLLy'})
tag, ruler, _ = tags.find_tag_for({'payee': 'SuLLy'})
self.assertEqual(tag, 'Bars')
tag, ruler = tags.find_tag_for({'payee': 'Sullyz Sull'})
tag, ruler, _ = tags.find_tag_for({'payee': 'Sullyz Sull'})
self.assertEqual(tag, None)

def test_find_tag_for__best_ruler(self):
tag, ruler = tags.find_tag_for({'payee': 'Foo Art Brut Shop'})
tag, ruler, _ = tags.find_tag_for({'payee': 'Foo Art Brut Shop'})
self.assertEqual(tag, 'Clothes')

def test_convert(self):
Expand Down

0 comments on commit 83dce36

Please sign in to comment.