Dear Beancounters,

attached you can find a plugin proposal which I'm using for my personal
Beancount needs but might be useful for others. If there is interest,
I'll be happy to submit it via the BTS, possibly adapting it to be more
generic before doing so.

The plugin validates that the in-file ordering of directives is
consistent with date ordering. That is not what you always want; and the
fact that Beancount does not care by default about file ordering is
indeed a great design principle.

But *if* you adopt the style of linear, date-ordered Beancount file(s),
then this plugin might help you in detecting errors. The typical error
that I'm successful avoiding with this plugin is copy-pasting an old
transaction as a more recent one, but forgetting to bump its date.

I haven't done so because I don't personally need it, but it should be
trivial to make the plugin configurable to, e.g., allow reverse ordering
and/or ensure that transactions are strictly ordered according to
metadata other than date. I'll be happy to support this if there is
interest from others.

As this is my first Beancount plugin, I also welcome comments on the
code and approach.

What do you think?
Cheers.
-- 
Stefano Zacchiroli . [email protected] . upsilon.cc/zack . . o . . . o . o
Computer Science Professor . CTO Software Heritage . . . . . o . . . o o
Former Debian Project Leader . OSI Board Director . . . o o o . . . o .
« the first rule of tautology club is the first rule of tautology club »

-- 
You received this message because you are subscribed to the Google Groups 
"Beancount" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/beancount/20161121095028.lz5k3uzay5xw4yi2%40upsilon.cc.
For more options, visit https://groups.google.com/d/optout.
# Copyright (C) 2016 Stefano Zacchiroli <[email protected]>
# License: GNU General Public License (GPL), version 2 or above

"""This Beancount plugin validates that each Beancount file that contains 2 or
more transactions is strictly chronologically ordered. I.e., no transaction
that occurs later in a given file (in file order) has a date that occurs
earlier (in calendar order) than a previous transaction in the same file.

While Beancount by default doesn't care about file ordering of directives,
ensuring in-file date ordering on transaction is a useful check to avoid
certain kinds of booking errors, e.g., copy-pasting an old transaction,
forgetting to bump its date.

"""

# TODO this plugin can be easily made configurable to, e.g.:
# - enforce reverse date ordering
# - enforce ordering on arbitrary metadata, not only date

import collections

from beancount.core.data import filter_txns

__plugins__ = ('validate_file_ordering',)


FileOrderingError = collections.namedtuple(
    'FileOrderingError',
    'source message entry')


def txns_by_file(entries):
    """Group a list of transaction by origin file and sort them by line number.

    Args:
      entries: a list of directives. All non-transaction directives are ignored
    Returns:
      a dictionary mapping filenames to entries belonging to that file, sorted
      by line number

    """
    file_entries = {}  # return dict

    for entry in filter_txns(entries):  # group by file
        if 'filename' not in entry.meta:
            continue
        filename = entry.meta['filename']

        if filename not in file_entries:
            file_entries[filename] = []
        file_entries[filename].append(entry)

    for filename in file_entries:  # sort by line number
        file_entries[filename].sort(key=lambda e: e.meta['lineno'])

    return file_entries


def validate_date_ordering(entries):
    """Ensure that a given list of entries is chronologically ordered, from oldest
    to newest

    Args:
      entries: a list of directives
    Returns:
      a list of FileOrderingError errors, if any

    """
    errors = []
    prev_date = None

    for entry in entries:
        if prev_date and entry.date < prev_date:
            errors.append(FileOrderingError(
                entry.meta,
                'Date {} occurs after {}, violating in-file date ordering'
                .format(entry.date, prev_date),
                entry))

        prev_date = entry.date

    return errors


def validate_file_ordering(entries, options_map):
    """Traverse all transactions in file order and ensure that, within the same
    file, their dates are strictly increasing.

    Args:
      entries: a list of directives
      options_map: an options map (unused)
    Returns:
      a list of new errors, if any

    """
    errors = []

    for file_entries in txns_by_file(entries).values():
        errors.extend(validate_date_ordering(file_entries))

    return entries, errors

Reply via email to