User Guide

The below code samples should give you an idea of how to use the tools provided in this library. If anything remains unclear, please feel free to open an issue on GitHub or reach out to the author.

Expression basics

The top-level class for interacting with boolean expressions in tt is, fittingly named, BooleanExpression. Let’s start by importing it:

>>> from tt import BooleanExpression

This class accepts boolean expressions as strings and provides the interface for parsing and tokenizing string expressions into a sequence of tokens and symbols, as we see here:

>>> b = BooleanExpression('(A nand B) or (C and D)')
>>> b.tokens
['(', 'A', 'nand', 'B', ')', 'or', '(', 'C', 'and', 'D', ')']
>>> b.symbols
['A', 'B', 'C', 'D']

During initialization, the BooleanExpression also does some work behind the scenes to build a basic understanding of the expression’s structure. It re-orders the tokens into postfix order, and uses this representation to build a BooleanExpressionTree. We can see this with:

>>> b.postfix_tokens
['A', 'B', 'nand', 'C', 'D', 'and', 'or']
>>> print(b.tree)
or
`----nand
|    `----A
|    `----B
`----and
     `----C
     `----D

Under the hood, this expression tree is used for evaluation of the expression. The expression object provides an interface to this evaluation functionality; use it like this:

>>> b.evaluate(A=True, B=False, C=True, D=False)
True
>>> b.evaluate(A=1, B=0, C=1, D=0)
True

Notice that we can use 0 or False to represent low values and 1 or True to represent high values. tt makes sure that only valid Boolean-esque values are accepted for evaluation. For example, if we tried something like:

>>> b.evaluate(A=1, B='not a Boolean value', C=0, D=0)
Traceback (most recent call last):
    ...
tt.errors.evaluation.InvalidBooleanValueError: "not a Boolean value" passed as value for "B" is not a valid Boolean value

or if we didn’t include a value for each of the symbols:

>>> b.evaluate(A=1, B=0, C=0)
Traceback (most recent call last):
    ...
tt.errors.symbols.MissingSymbolError: Did not receive value for the following symbols: "D"

we see that we get an exception. This brings up one of the key design philosophies of tt: exceptions are specific and abundant. This library does its best to make sure you use its top-level interfaces as designed, and will raise exceptions if something isn’t right.

Exceptions in tt are organized in a hierarchy, with each category of exceptions grouped under a base exception type. For example, a common group of exceptions you’ll deal with if you might handle malformed expressions is GrammarError. GrammarError is a unique type of exception in tt, as it provides attributes for accessing the specific position in the expression string that caused an error. This is best illustrated with an example:

>>> from tt import GrammarError
>>> try:
...     b = BooleanExpression('A or or B')
... except GrammarError as e:
...     print("Here's what happened:")
...     print(e.message)
...     print("Here's where it happened:")
...     print(e.expr_str)
...     print(' '*e.error_pos + '^')
...
Here's what happened:
Unexpected binary operator "or"
Here's where it happened:
A or or B
     ^

Table basics

Now that we’ve gotten the basic idea of how expressions work, we can use them to fill some truth tables. Surprisingly, the top-level class for dealing with truth tables in tt is called TruthTable. Let’s begin by importing it:

>>> from tt import TruthTable

There are a few ways we can fill up a truth table in tt. One of them is to pass in an expression, either as an already-created BooleanExpression object or a string:

>>> t = TruthTable('A xor B')
>>> print(t)
+---+---+---+
| A | B |   |
+---+---+---+
| 0 | 0 | 0 |
+---+---+---+
| 0 | 1 | 1 |
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 1 | 1 | 0 |
+---+---+---+

As we saw in the above example, printing tables produces a nicely-formatted text table. While we have stuck to simple variable names (A, B, C, etc.) so far, these tables will scale to fit the size of the symbol names:

>>> t = TruthTable('operand_1 and operand_2')
>>> print(t)
+-----------+-----------+---+
| operand_1 | operand_2 |   |
+-----------+-----------+---+
|     0     |     0     | 0 |
+-----------+-----------+---+
|     0     |     1     | 0 |
+-----------+-----------+---+
|     1     |     0     | 0 |
+-----------+-----------+---+
|     1     |     1     | 1 |
+-----------+-----------+---+

By default, tt will order the symbols in the top row of of the table to match the order of their appearance in the original expression; however, you can impose your own order, too:

>>> t = TruthTable('A xor B', ordering=['B', 'A'])
>>> print(t)
+---+---+---+
| B | A |   |
+---+---+---+
| 0 | 0 | 0 |
+---+---+---+
| 0 | 1 | 1 |
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 1 | 1 | 0 |
+---+---+---+

These tables are populated by evaluating the expression for each combination of input values. Let’s say that you already have the values you want in your truth table and would rather skip over re-evaluating ; you’d populate your table like this:

>>> t = TruthTable(from_values='00x1')
>>> print(t)
+---+---+---+
| A | B |   |
+---+---+---+
| 0 | 0 | 0 |
+---+---+---+
| 0 | 1 | 0 |
+---+---+---+
| 1 | 0 | x |
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+

Notice that populating tables like this allows for don’t cares (indicating by x) to be present in your table. Additionally, we can see that symbol names were automatically generated for us. That’s nice sometimes, but what if we want to specify them ourselves? We return to the ordering keyword argument:

>>> t = TruthTable(from_values='1x01', ordering=['op1', 'op2'])
>>> print(t)
+-----+-----+---+
| op1 | op2 |   |
+-----+-----+---+
|  0  |  0  | 1 |
+-----+-----+---+
|  0  |  1  | x |
+-----+-----+---+
|  1  |  0  | 0 |
+-----+-----+---+
|  1  |  1  | 1 |
+-----+-----+---+

So far, we’ve only been able to examine the results stored in our tables by printing them. This is nice for looking at an end result, but we need programmatic methods of accessing the values in our tables, like these:

>>> t = TruthTable('!A && B')
>>> t.results
[False, True, False, False]
>>> for inputs, result in t:
...     inputs, result
...
((False, False), False)
((False, True), True)
((True, False), False)
((True, True), False)
>>> t[0], t[1], t[2], t[3]
(False, True, False, False)

Accessing results by index is also an intuitive time to use binary literal values:

>>> t[0b00], t[0b01], t[0b10], t[0b11]
(False, True, False, False)

Up to this point, we’ve only taken a look at tables with all their results filled in, but we don’t have to completely fill up our tables to start working with them. Here’s an example of iteratively filling a table:

>>> t = TruthTable('A nor B', fill_all=False)
>>> t.is_full
False
>>> print(t)
Empty!
>>> t.fill(A=0)
>>> t.is_full
False
>>> print(t)
+---+---+---+
| A | B |   |
+---+---+---+
| 0 | 0 | 1 |
+---+---+---+
| 0 | 1 | 0 |
+---+---+---+
>>> t.fill()
>>> t.is_full
True
>>> print(t)
+---+---+---+
| A | B |   |
+---+---+---+
| 0 | 0 | 1 |
+---+---+---+
| 0 | 1 | 0 |
+---+---+---+
| 1 | 0 | 0 |
+---+---+---+
| 1 | 1 | 0 |
+---+---+---+

Empty slots in the table will be represented with a corresponding None entry for their result:

>>> t = TruthTable('A or B', fill_all=False)
>>> t.results
[None, None, None, None]
>>> t.fill(B=0)
>>> t.results
[False, None, True, None]

Make sure not to try to keep filling an already-full table, though:

>>> t = TruthTable(from_values='0110')
>>> t.is_full
True
>>> t.fill()
Traceback (most recent call last):
    ...
tt.errors.state.AlreadyFullTableError: Cannot fill an already-full table

Another neat feature provided by tt’s tables is the checking of logical equivalence:

>>> t1 = TruthTable('A xor B')
>>> t2 = TruthTable(from_values='0110')
>>> t1.equivalent_to(t2)
True
>>> t1.equivalent_to('C xor D')
True

Note that this equivalence comparison looks only at the result values of the tables and doesn’t look at the symbols of either table.

Next, let’s examine how don’t cares function within tt’s concept of logical equivalence. Don’t cares in the calling table will be considered to equal to any value in the comparison table, but any explicity value in the calling table must be matched in the comparison table to be considered equal.

In this sense, a fully-specified table (i.e., one without any don’t cares) will never be logically equivalent to one which contains don’t cares, but the converse may be true. Let’s see an example:

>>> t1 = TruthTable('C nand D')
>>> t2 = TruthTable(from_values='xx10')
>>> t1.equivalent_to(t2)
False
>>> t2.equivalent_to(t1)
True