Skip to content

Commit

Permalink
Support filtering for recurrent entries with indefinite end
Browse files Browse the repository at this point in the history
Close #80
  • Loading branch information
pylipp committed Jan 9, 2022
1 parent aca6845 commit 65504ff
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 31 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Deprecated

## [v1.0.0] - 2022-
### Added
- Filtering for recurrent entries with indefinite end is now supported (omit the filter value, as in `list -f end=`). (#80)
### Changed
- For filtering the output of the `list` command, the `-f` option can now be specified multiple times with one argument each (previously, multiple arguments could be passed but when using `-f` multiple times, the last occurrence would overrule the previous ones). The filters are combined. The long option is called `--filter` (singular). (#83)
- If the `list -f` option is used with a filter for frequency, start, or end, the `-r/--recurrent-only` option is automatically added. (#83)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ In order to only list category entries incl. their respective percentage of earn

> fina list --category-percentage

In order to only list recurrent entries run (you can apply additional filtering and sorting)
In order to only list recurrent entries run (you can apply additional filtering (use `-f end=` to list entries with indefinite end) and sorting)

> fina list --recurrent-only

Expand Down
17 changes: 10 additions & 7 deletions financeager/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,16 @@ def _preprocess(data):
key, value = item.split("=")
parsed_items[key] = value.lower()

try:
# Substitute category default name
if parsed_items["category"] == entries.CategoryEntry.DEFAULT_NAME:
parsed_items["category"] = None
except KeyError:
# No 'category' field present
pass
for field, indicator in zip(
["category", "end"], [entries.CategoryEntry.DEFAULT_NAME, ""]
):
# Substitute category default name, or empty string for end
try:
if parsed_items[field] == indicator:
parsed_items[field] = None
except KeyError:
# No field present
pass

data["filters"] = parsed_items
except ValueError:
Expand Down
47 changes: 24 additions & 23 deletions financeager/pocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,36 +487,37 @@ def _create_query_condition(**filters):
"""Construct query condition according to given filters. A filter is
given by a key-value pair. The key indicates the field, the value the
pattern to filter for. Valid keys are 'name', 'date', 'value' and/or
'category'. Patterns must be of type string, or None (only for the field
'category'; indicates filtering for all entries of the default
category).
'category'. Patterns must be of type string, or None (only for the fields
'category' and 'end; indicates filtering for all entries of the default
category, and recurrent entries with indefinite end, resp.).
:return: tinydb.queries.QueryInstance (default: noop)
"""
condition = Query().noop()
if not filters:
return condition

entry = Query()
try:
# The 'category' field is of type string or None. The condition is
# constructed depending on the filter pattern
pattern = filters["category"]

if pattern is None:
condition = entry["category"] == None # noqa
else:
# Use regex searching of the filter pattern in the field if it
# is not None
def test(e):
if e is None:
return False
return re.compile(pattern).search(e)

condition = entry["category"].test(test)

except KeyError:
# No 'category' filter present
pass
for field in ["category", "end"]:
try:
# The 'category' and 'end' fields are of type string or None. The
# condition is constructed depending on the filter pattern
pattern = filters[field]

if pattern is None:
condition = entry[field] == None # noqa
else:
# Use regex searching of the filter pattern in the field if it
# is not None
def test(e):
if e is None:
return False
return re.compile(pattern).search(e)

condition = entry[field].test(test)

except KeyError:
# No field filter present
pass

for field, pattern in filters.items():
if pattern is None:
Expand Down
5 changes: 5 additions & 0 deletions test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,11 @@ def test_recurrent_only_fields_filter(self):
self.assertEqual(data["filters"], {name: value})
self.assertTrue(data["recurrent_only"])

def test_filter_indefinite_end(self):
data = {"filters": ["end="]}
cli._preprocess(data)
self.assertEqual(data["filters"], {"end": None})


class FormatResponseTestCase(unittest.TestCase):
def test_add(self):
Expand Down
25 changes: 25 additions & 0 deletions test/test_pocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,31 @@ def test_recurrent_entry_ending_in_future(self):
self.assertEqual(recurrent_entries[0]["date"], f"{year}-01-01")
self.assertEqual(recurrent_entries[0]["name"], "insurance, january")

def test_filter_recurrent_entries_with_indefinite_end(self):
eid = self.pocket.add_entry(
name="fees",
value=-5,
table_name=RECURRENT_TABLE,
frequency="monthly",
)
recurrent_entries = self.pocket.get_entries(
filters={"end": None}, recurrent_only=True
)
self.assertEqual(
recurrent_entries,
[
{
"name": "fees",
"value": -5.00,
"category": None,
"start": dt.date.today().isoformat(),
"end": None,
"frequency": "monthly",
"id": eid,
}
],
)

def tearDown(self):
self.pocket.close()

Expand Down

0 comments on commit 65504ff

Please sign in to comment.