# Represent Accounting Equation Using SWI-Prolog

Regarding…

I’ve implemented accounting systems (not using Prolog) and can assure you that Prolog (or Datalog) would be a fine way of adding business rules to databases.

This is good information.

The conformance suite is public domain, free for anyone to use.

So I have a proposal that might help you meet your needs. This example, Common Elements of Financial Statements, is small but very comprehensive example. It is also well documented.

What I could do is create a new conformance suite for you that would help you achieve what is is that you want to achieve. In my view, it would be really great to be able to convert XBRL into SWI-Prolog. In fact, what would be REALLY useful would be to import XBRL into SWI-Prolog directly (not sure if I am explaining this precisely).

I have created this informally defined logical model of a business report, Open Source Framework for Implementing XBRL-based Digital Financial Reporting. Those would be the preferable terms to use.

Also, of use might be my “standard” explanation of a logical system terms.

The objective of all this is to be able to explain the logic of a business report (note that a financial report is a specialization of the more general business report).

Answer Set Programming (ASP) is the opposite end of the spectrum from Datalog.

A step back from ASP is constraint solving (there are a number of such solvers within SWI-Prolog). These are very powerful and can be quite efficient. But debugging constraint solvers can be difficult – the constraint solver says “no” but doesn’t give a hint as to why because there are an infinite number of ways that the constraints can’t be solved. (As an experiment, one of our PhD interns tried using a boolean satisfiability solver to infer Python datatypes; even trivial programs ended up with millions of constraints, which were impossible to debug.)

So, if you use constraints – and I think that they’re a great way of doing certain things – you need to think about how the constraint solver would explain failures. If the constraints can be thought of an extension to how Prolog computes a solution (e.g., using freeze/2 to delay goals until sufficiently instantiated), they are fairly easy to use; more sophisticated constraints can be a challenge.

Thanks.

Let me do the basic conformance items first to get my feet in the water and better understand the accounting terms then we can expand on it from there. I don’t want to bite off more than I can chew.

Also and I noted to another programmer many years ago when she could not understand that I did not learn about debits and credits in school is that I have a BS and not a BA. In other words the STEM stuff is more of my knowledge and this accounting problem will help to expand my knowledge.

Peter, what is your view of OWL as contrast to Prolog/Datalog? One of the reasons OMG is interested in SBRM is so that they can get XBRL converted to RDF/OWL. Both XBRL processors and OWL/RDF have limitations in terms of their capabilities to process the logic of a financial report. With XBRL, you have to add some specific things and I know what those are. With OWL/RDF; you also have to use SHACL (Shapes Constraint Language) . I will know exactly how much you need to use SHACL within a couple of days most likely. What would be really good to understand is what Prolog/Datalog might be missing.

You might be interested in this; here is information that explains double-entry accounting in terms of mathematics. Double-entry accounting is basic freshman algebra.

Notations such as OWL (or “Semantic Web” or “RDF” or …) are more constrained than Datalog or Prolog. In return, computations over OWL ontologies can be more efficient because of the restrictions.

In the “old days”, there was a lot of work with systems such as KL-ONE, which turned out to have almost intractable problems with defining concepts such as “is-a” versus “has-attribute” (e.g., you might define a class of “red object” but what about objects that have an attribute “color=red”?). These problems endure, but are often swept under the rug by proponents of simple ontological representation systems.

It’s been many years since I’ve worked on this; and I don’t consider myself very competent in it. You might want read http://www.jfsowa.com/ or ask a question on http://gnowledge.org/pipermail/cg/ or https://groups.google.com/forum/#!forum/ontolog-forum – people such as John Sowa or Harry Delugach (and many others whose names I don’t remember off-hand) know far more about this than I ever will.

You can do a lot in Prolog with aggregate functions, or if you want with tabling. An accounting for a period, without some special reporting views, would be modelled by two fact predicates:

``````% account(account_id, start_value, end_value, type).
% booking(booking_id, account_id1, account_id2, booking_value).
``````

The above works for everything, accounts will be used for everything(*).
You can then first verify that each account is ok relative to the bookings:

``````?- account(A,S,E,_),
aggregate_all(sum(V),(booking(B,A,_,V);booking(B,_,A,U),V is -U),W),
(E=:=S+W -> write(A), write(' is ok'), nl; write(A), write(' is nok'), nl).
``````

After you did that, you can forget about the bookings. And through a clever arrangement of account types, you can then verify:

`````` ?- aggregate_all(sum(E),(account(A,S,E,0);account(A,S,F,1),E is -F),P),
(P=:=0 -> write('book is ok'); write('book is nok')).
``````

Example:

``````account(kassa,0,70000,0).
account(cars,100000,30000,0).
account(allmine,100000,100000,1).

booking(car_sold,kassa,cars,70000).
``````

Here is an example run. First the accounts:

And now the whole book:

Of course you can do million other things with such books, and also some use cases in the wild might break down to multiple bookings.

(*) A typical coding of accounts in Switzerland, whereas my example used type=0,1, the below uses type=1,2,3,4,5,6,7,8,9 named “Klasse”:
https://www.banana.ch/files/K%C3%84FER%20KONTENRAHMEN.pdf

BTW: Bananasoft is quite popular, even in China. All that it offers is the above two Prolog facts, and a few extras, like VAT and different currencies. And configurable reports, if you need special “equations”.

Here is the some proof of concept code that reads the sample XBLR data and checks that the formula

Assets = Liabilities + Equity

is true.

I originally tried to read the data with the SGML parser but it expected end tags instead of tags that ended with `/>` which was causing more problems than it was worth. As this is the first time I have used either the SGML or XML parser with SWI-Prolog, probably something I need to learn, I could not find an option for allowing tags ending with `/>`.

Note: Click triangle below to expand section.

Proof of concept code
``````xbrl(Xbrl,Assets,Liabilities,Equity) :-
Xbrl = element(xbrl,_,Elements),
elements(Elements,Assets,Liabilities,Equity), !.

elements([],0,0,0).
elements([element('ae:Assets',_,Value_list)|Elements],Assets,Liabilities,Equity) :-
Value_list = [Value_atom],
atom_number(Value_atom,Value),
elements(Elements,Assets0,Liabilities,Equity),
Assets is Assets0 + Value.
elements([element('ae:Liabilities',_,Value_list)|Elements],Assets,Liabilities,Equity) :-
Value_list = [Value_atom],
atom_number(Value_atom,Value),
elements(Elements,Assets,Liabilities0,Equity),
Liabilities is Liabilities0 + Value.
elements([element('ae:Equity',_,Value_list)|Elements],Assets,Liabilities,Equity) :-
Value_list = [Value_atom],
atom_number(Value_atom,Value),
elements(Elements,Assets,Liabilities,Equity0),
Equity is Equity0 + Value.
elements([_|T],Assets,Liabilities,Equity) :-
elements(T,Assets,Liabilities,Equity).

does_balance_sheet_balance(Assets,Liabilities,Equity) :-
Assets is Liabilities + Equity.

:- begin_tests(xblr).

test(01) :-
Xblr_string =
"<!--  Created by Charles Hoffman, CPA: 2019-11-02  -->\n\c
<context id=\"I-2020\">\n\c
<entity>\n\c
<identifier scheme=\"http://standards.iso.org/iso/17442\">GH259400TOMPUOLS65II</identifier>\n\c
</entity>\n\c
<period>\n\c
<instant>2020-12-31</instant>\n\c
</period>\n\c
</context>\n\c
<unit id=\"U-Monetary\">\n\c
<measure>iso4217:USD</measure>\n\c
</unit>\n\c
<ae:Assets contextRef=\"I-2020\" unitRef=\"U-Monetary\" decimals=\"INF\">5000</ae:Assets>\n\c
<ae:Liabilities contextRef=\"I-2020\" unitRef=\"U-Monetary\" decimals=\"INF\">1000</ae:Liabilities>\n\c
<ae:Equity contextRef=\"I-2020\" unitRef=\"U-Monetary\" decimals=\"INF\">4000</ae:Equity>\n\c
</xbrl>",
open_string(Xblr_string,Stream),
xbrl(Xbrl,Assets,Liabilities,Equity),
does_balance_sheet_balance(Assets,Liabilities,Equity).

:- end_tests(xblr).
``````

Example run.

You will need to change to the appropriate directory on your system.

``````?- working_directory(_,'c:/users/eric/documents/Projects/Prolog/xbrl').
true.

?- consult("xbrl.pl").
true.

?- run_tests.
% PL-Unit: xblr . done
% test passed
true.
``````

I took a detailed look at some of the files in XBRL-based Digital Financial Reporting Conformance Suite Tests and while I could identify the values, it was not simple Assets, Liabilities and Equity as I expected so I didn’t know what kind of rules to write for it or how to view it to make sense. I did see the dates you noted earlier for the range checks you needed.

Your code doesn’t scale so easily maintenance wise. Already the predicate arity changes depending on the number of variables in the equation. Maybe do as Jan W. suggested here. Dont use rules, use some representation instead. This equation here:

`Assets = Liabilities + Equity`

Can be represented as a weighted sum that must be zero:

`(+1)*Assets + (-1)*Liabilities + (-1)*Equity = 0`

So you could represent it as:

``````weight('ae:Assets', +1).
weight('ae:Liabilities', -1).
weight('ae:Equity', -1).
``````

And then use some generic code to run the check using a mixture of the real data and the weight/2 meta data. Or use type=0 (for -1),1 (for +1) directly in your accounts. If you use types in your accounts, then most accounting equations get even more easier to configure.

Edit 12.11.2019:
Disadvantages of Jan W. suggestion. You basically create a DSL, and then are confined into what the DSL can do. Which might also be viewed an advantage.

I cannot resists. In Switzerland we use negative and positive values. Negative values are usually used for example for earnings, and they are then rendered in red. Which can be very confusing for the uninitiated.

With negative values, here something from a balance and not earnings, my example gets even easier, you only need one type, can spare the type argument:

``````account(kassa,0,70000).
account(cars,100000,30000).
account(allmine,-100000,-100000).

booking(car_sold,kassa,cars,70000).
``````

The query to check wether everything is ok, then reduces to:

``````?- aggregate_all(sum(E),account(A,S,E),P),
(P=:=0 -> write('book is ok'); write('book is nok')).
``````

If you pay attention, you might find some company year end PDF from Switzerland on the internet, and then you see account comments (Gewinn -, Verlust +), translated (Earnings -, Loss +). Not everybody does it like this, but some do it like this.

Because there can be differences in usage of positive and negative values, it is custom in accounting to say what the sign means, put comments explaining the signs into your reports. Especially for the totals, which then explains also the individual accounts.

I think negative values posed a problem for the medieval people. So it was not possible for them have a signed sum in an account since mathematics respectively manual calculation was not that advanced. This is a modern solution.

In medieval times you would probably not see a grand total that can express both earnings and loss, simply indicated by a sign. You would have split the account into two measures, and then if the first measures is greater than the second measure, there was an earning,

and otherwise if the first measure was less the second measure, there was a loss. The op wrote:

So he might tell more. Despite the luxury of positive and negative values nowadays, inside an account, that is not some aggregate except for its bookings, we nevertheless try keep it tidy, in that we always use the same sign.

Not enforced by banana, and not necessarily enforced by your bank. It might be the that you can withdraw more money than you have. They will simply charge you some interest rate, and wait till your next salary makes the account positive again.

With incremental tabling you can have real-time views on the sanity of a book. Lets make a first simplification. We do not have the end_value stored, but do compute it from the bookings. This leads to the following mode directed tabling:

``````:- table account_end(_,sum).
account_end(A,S) :- account_start(A,S).
account_end(A,V) :- booking(_,A,_,V).
account_end(A,V) :- booking(_,_,A,U), V is -U.
``````

And then the simplified book ok check, where we use signs, again mode directed tabling:

``````:- table book_ok(sum).
book_ok(E) :- account_end(_,E).
``````

Here is the car seller again:

Edit 13.11.2019:
I never used incremental tabling in SWI-Prolog. Possibly I need to use a different directive for the tabling, and also make some things dynamic. If I have time I will try incremental tabling for the example, and also show a real time response to a wrong booking. This has to wait, since I need to sleep now.

There are zillion of options. But, e.g., `<img .../>` is not legal SGML AFAIK. Most likely that means the format is XML. If I recall well, HTML5 mode also allows for this. Not sure though. Given the right options it can parse almost any SGML/XML/HTML document, sometimes with warnings. Note that using simple DCGs or other simple pattern matching is quite likely to go wrong at some point. There are a lot of legal different serializations for the same document in all these dialects.

1 Like

It works perfectly fine, without warnings:

``````\$ swipl
Welcome to SWI-Prolog (threaded, 64 bits, version 8.1.15-10-g6718ecfeb)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.

For built-in help, use ?- help(Topic). or ?- apropos(Word).

``````

`instance.xml` is the XML file linked, found exactly here.

Here is some more details on the reading of instance.xml .

In reading the documentation for package SWI-Prolog SGML/XML parser it notes in section 10 Unsupported SGML features

SHORTTAG
The SGML SHORTTAG syntax is only partially implemented. Currently, `<tag/content/` is a valid abbreviation for `<tag>content</tag>` , which can also be written as `<tag>content</>` . Empty start tags ( `<>` ), unclosed start tags ( `<a<b</verb>` ) and unclosed end tags ( `<verb></a<b` ) are not supported.

This is not a show stopper as I have shown with the proof of concept code and as Jan notes there are other ways that can be more beneficial to deal with this.

The reason I am not exploring them in more detail at present is that I would like to see the code related to the validity rules and then other business concepts that might be implemented before optimizing the code. As this code has not been written yet, that is the next logical step. The rule of thumb “Premature optimization is the root of all evil” applies at this time.

To make the issue with the end tags more apparent, when I was writing the code I made use of print_term/2 but removed them in the presented version.

The output is presented here for closer inspection.

``````?- load_sgml('instance.xml',Xbrl,[]),print_term(Xbrl,[]).
Warning: SGML2PL(sgml): instance.xml:19: Inserted omitted end-tag for "xbrll:linkbaseref"
Warning: SGML2PL(sgml): instance.xml:19: Inserted omitted end-tag for "xbrll:schemaref"
[ element(xbrl,
[ xmlns = 'http://www.xbrl.org/2003/instance',
'xmlns:iso4217' = 'http://www.xbrl.org/2003/iso4217',
'xmlns:xbrldi' = 'http://xbrl.org/2006/xbrldi',
'xmlns:xbrldt' = 'http://xbrl.org/2005/xbrldt',
'xmlns:xsi' = 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:ae' = 'http://www.xbrlsite.com/financialreporting/ae',
'xsi:schemalocation' = 'http://xbrl.org/2006/xbrldi http://www.xbrl.org/2006/xbrldi-2006.xsd'
],
[ element('xbrll:schemaref',
],
[ '>\n',
],
[ '>\n',
element(context,
[id='I-2020'],
[ element(entity,
[],
[ element(identifier,
[ scheme = 'http://standards.iso.org/iso/17442'
],
[ 'GH259400TOMPUOLS65II'
])
]),
element(period,
[],
[ element(instant,
[],
[ '2020-12-31'
])
])
]),
element(unit,
[id='U-Monetary'],
[ element(measure,
[],
['iso4217:USD'])
]),
element('ae:assets',
[ contextref = 'I-2020',
unitref = 'U-Monetary',
decimals = 'INF'
],
['5000']),
element('ae:liabilities',
[ contextref = 'I-2020',
unitref = 'U-Monetary',
decimals = 'INF'
],
['1000']),
element('ae:equity',
[ contextref = 'I-2020',
unitref = 'U-Monetary',
decimals = 'INF'
],
['4000'])
])
])
])
]
``````
``````?- load_xml('instance.xml',Xbrl,[]),print_term(Xbrl,[]).
[ element(xbrl,
[ xmlns = 'http://www.xbrl.org/2003/instance',
'xmlns:iso4217' = 'http://www.xbrl.org/2003/iso4217',
'xmlns:xbrldi' = 'http://xbrl.org/2006/xbrldi',
'xmlns:xbrldt' = 'http://xbrl.org/2005/xbrldt',
'xmlns:xsi' = 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:ae' = 'http://www.xbrlsite.com/financialreporting/ae',
'xsi:schemaLocation' = 'http://xbrl.org/2006/xbrldi http://www.xbrl.org/2006/xbrldi-2006.xsd'
],
[ '\n',
element('xbrll:schemaRef',
],
[]),
'\n',
],
[]),
'\n',
element(context,
[id='I-2020'],
[ '\n',
element(entity,
[],
[ '\n',
element(identifier,
[ scheme = 'http://standards.iso.org/iso/17442'
],
['GH259400TOMPUOLS65II']),
'\n'
]),
'\n',
element(period,
[],
['\n',element(instant,[],['2020-12-31']),'\n']),
'\n'
]),
'\n',
element(unit,
[id='U-Monetary'],
['\n',element(measure,[],['iso4217:USD']),'\n']),
'\n',
element('ae:Assets',
[contextRef='I-2020',unitRef='U-Monetary',decimals='INF'],
['5000']),
'\n',
element('ae:Liabilities',
[contextRef='I-2020',unitRef='U-Monetary',decimals='INF'],
['1000']),
'\n',
element('ae:Equity',
[contextRef='I-2020',unitRef='U-Monetary',decimals='INF'],
['4000']),
'\n'
])
]
``````

As is evident with `load_xml/3` as used the tag endings are resulting in `\n`, however these are not disrupting the hierarchy for parsing the data structure.

On the other hand `load_sgml/3` as used the tag endings are resulting in `\n`, and these are disrupting the hierarchy for parsing the data structure.

Using `load_html/3` as suggested by Jan seems to not suffer from the end tag issue.

``````?- load_html('instance.xml',Xbrl,[]),print_term(Xbrl,[]).
[ element(xbrl,
[ xmlns = 'http://www.xbrl.org/2003/instance',
'xmlns:iso4217' = 'http://www.xbrl.org/2003/iso4217',
'xmlns:xbrldi' = 'http://xbrl.org/2006/xbrldi',
'xmlns:xbrldt' = 'http://xbrl.org/2005/xbrldt',
'xmlns:xsi' = 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:ae' = 'http://www.xbrlsite.com/financialreporting/ae',
'xsi:schemalocation' = 'http://xbrl.org/2006/xbrldi http://www.xbrl.org/2006/xbrldi-2006.xsd'
],
[ element('xbrll:schemaref',
],
[]),
],
[]),
element(context,
[id='I-2020'],
[ element(entity,
[],
[ element(identifier,
[ scheme = 'http://standards.iso.org/iso/17442'
],
['GH259400TOMPUOLS65II'])
]),
element(period,
[],
[element(instant,[],['2020-12-31'])])
]),,
element(unit,
[id='U-Monetary'],
[element(measure,[],['iso4217:USD'])]),
element('ae:assets',
[contextref='I-2020',unitref='U-Monetary',decimals='INF'],
['5000']),
element('ae:liabilities',
[contextref='I-2020',unitref='U-Monetary',decimals='INF'],
['1000']),
element('ae:equity',
[contextref='I-2020',unitref='U-Monetary',decimals='INF'],
['4000'])
])
]
``````

Again, I do not see any of this as a show stopper, just a learning exercise.

Although I understand this is not your current priority, I think it is wise to point at features of the SGML/HTML/XML parser.

As @Boris shows, the document is XML. `<x/>` is XML and not valid SGML. White space is by default all part of the document in XML, unless the `xml:space` attribute says otherwise. SWI-Prolog’s XML parser however can be told to process the document otherwise. See https://www.swi-prolog.org/pldoc/man?section=xml-whitespace

1 Like

Thanks Jan.

With `load_xml/3` and using the option `space(remove)` the XML loads without the end tag issue.

Use of load_xml/3 and option space(remove) with print_term/2
``````?- load_xml('instance.xml',Xbrl,[space(remove)]),print_term(Xbrl,[]).
[ element(xbrl,
[ xmlns = 'http://www.xbrl.org/2003/instance',
'xmlns:iso4217' = 'http://www.xbrl.org/2003/iso4217',
'xmlns:xbrldi' = 'http://xbrl.org/2006/xbrldi',
'xmlns:xbrldt' = 'http://xbrl.org/2005/xbrldt',
'xmlns:xsi' = 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:ae' = 'http://www.xbrlsite.com/financialreporting/ae',
'xsi:schemaLocation' = 'http://xbrl.org/2006/xbrldi http://www.xbrl.org/2006/xbrldi-2006.xsd'
],
[ element('xbrll:schemaRef',
],
[]),
],
[]),
element(context,
[id='I-2020'],
[ element(entity,
[],
[ element(identifier,
[ scheme = 'http://standards.iso.org/iso/17442'
],
['GH259400TOMPUOLS65II'])
]),
element(period,
[],
[element(instant,[],['2020-12-31'])])
]),
element(unit,
[id='U-Monetary'],
[element(measure,[],['iso4217:USD'])]),
element('ae:Assets',
[contextRef='I-2020',unitRef='U-Monetary',decimals='INF'],
['5000']),
element('ae:Liabilities',
[contextRef='I-2020',unitRef='U-Monetary',decimals='INF'],
['1000']),
element('ae:Equity',
[contextRef='I-2020',unitRef='U-Monetary',decimals='INF'],
['4000'])
])
]
``````

Can’t demonstrate it with incremental tabling yet, since the incremental property seems currently not to be accepted for mode directed tables. #525

I made a test in my own system and wrote an explicit trigger:

``````assertz_journal(F) :-
assertz(F),
retractall_table(account_end(_,_)),
retractall_table(book_ok(_)).
``````

This works of course fine:

``````?- account_end(A, V).
A = cars,
V = 50000 ;
A = cash,
V = 20000 ;
A = equity,
V = -70000

?- book_ok(V).
V = 0

?- assertz_journal(booking(car_sold, cash, cars, 50000)).
Yes

?- account_end(A, V).
A = cars,
V = 0 ;
A = cash,
V = 70000 ;
A = equity,
V = -70000

?- book_ok(V).
V = 0``````