HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Truth Table Calculator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
calculatortabletruth

Problem

In of my software engineering classes, I had to write a truth table calculator in the language of my choice. This is what I came up with, and I would like to know what I should do better next time:

import copy
import sys

print("This program reads boolean equations and outputs the truth table.")
print("You may use the operators ~, ∨, and ∧, or")
print("their text equivalents, not, or, and and.")
print("You may use parenthesis, braces, and brackets to group your equations,")
print("and you must use single letters for your variable names, as 'a∨b'\n")

equation = input()
equation_copy = equation
equation_vars = []
equation = " " + equation + " "
equation = equation.replace("~", " not ")
equation = equation.replace("∨", " or ")
equation = equation.replace("∧", " and ")
equation = equation.replace("[", "(")
equation = equation.replace("]", ")")
equation = equation.replace("{", "(")
equation = equation.replace("}", ")")

for index in range(len(equation)):
    if equation[index] in " ()^":
        continue
    if equation[index + 1] in " ()" and equation[index - 1] in " ()" and equation[index] not in equation_vars:
        equation_vars.append(equation[index])

equation_vars.sort()

for equationIndex in equation_vars:
    sys.stdout.write(equationIndex + '\t')

print(equation_copy)

for equationIndex in range(pow(2,len(equation_vars))):
    vals = str(bin(equationIndex)).replace('0b','').zfill(len(equation_vars))

    for value in vals:
        sys.stdout.write(value + '\t')

    equation_vars_copy = copy.deepcopy(equation_vars)
    for index in range(len(equation_vars_copy)):
        equation_vars_copy[index] += " = " + vals[index]
        exec(equation_vars_copy[index])

    truthTable = "result = equation"
    truthTable = truthTable.replace('equation', equation)
    exec(truthTable)
    if result:print(1)
    else:print(0)


I have a working version at IdeOne.

Solution

Let's proceed top-down. I'll suggest cleverer ways to do a bunch of stuff, but the review will culminate in the two most important points:

  • this is a huge security hole, and



  • you should organize your code into functions.



Introductory message

A good way to write a multiline string is using """the longstring""" syntax.

In this case, though, that introductory message could just as well be the docstring for the program. You could use the special __doc__ variable to fetch the docstring by introspection.

"""This program reads boolean expressions and outputs the truth table.
You may use the operators ~, ∨, and ∧, or
their text equivalents, not, or, and and.
You may use parenthesis, braces, and brackets to group your expressions,
and you must use single letters for your variable names, as 'a∨b'"""

print(__doc__)


Note that "equation" is not the right word: you don't want a string with an equals sign. You just want a "boolean expression".

Canonicalizing the expression

Doing string substitutions in multiple passes is bad practice. In the best case, you waste a bit of effort making multiple passes over the string. In the worst case, passing previously substituted text through another round could be incorrect — though fortunately that's not the case here.

I recommend doing the substitution in one pass using a regular expression.

REPLACEMENTS = {
   '~': ' not ',
   'v': ' or ',
   '^': ' and ',
   '[': '(',
   ']': ')',
   '{': '(',
   '}': ')',
}
expr = re.sub('|'.join(re.escape(sym) for sym in REPLACEMENTS.keys()),
              lambda sym: REPLACEMENTS[sym.group(0)],
              expr).strip()


Extracting the variables

This, too, can be simplified using a regular expression. \b[A-Za-z]\b looks for any single letter bounded by word boundaries on both sides.

In addition, deduplication is more elegant using a set.

vars = sorted(set(re.findall(r'\b[A-Za-z]\b', expr)))


Generating the boolean values

This is a perfect job for itertools.product(), and in fact the documentation has just such an example:

product(range(2), repeat=len(vars))


Evaluating the expression

First of all, you should use eval(), not exec(), since you want to evaluate the expression and get its value, rather than running some code for its side-effect. result = eval(expr) is better than exec('result = ' + expr).

Most importantly, though, calling eval() or exec() like that is a huge security hole — it lets the user execute arbitrary Python code. For example, the user might enter this to be executed:

__import__('os').system('rm -rfi /')


The consensus is that Python can't be safely sandboxed. We can try to do better by evaluating the expression in a scope that contains just the relevant variables.

NO_GLOBALS = {'__builtins__': {}}
for vals in product(range(2), repeat=len(vars)):
    locals = dict(zip(vars, vals))
    result = eval(expr, NO_GLOBALS, locals)


However, I suspect that even that won't be secure. The lambda keyword is still available, for example, and that makes me nervous. Ultimately, the only safe approach may be to write your own code to parse and evaluate the expression — Python itself is too powerful.

Suggested solution

As a final remark: the problem can be divided neatly into functions. Naming these chunks of work make your code easier to follow.

"""This program reads boolean expressions and outputs the truth table.
You may use the operators ~, ∨, and ∧, or
their text equivalents, not, or, and and.
You may use parenthesis, braces, and brackets to group your expressions,
and you must use single letters for your variable names, as 'a∨b'"""

from itertools import product
import re

def canonicalize(expr):
    REPLACEMENTS = {
       '~': ' not ',
       'v': ' or ',
       '^': ' and ',
       '[': '(',
       ']': ')',
       '{': '(',
       '}': ')',
    }
    return re.sub('|'.join(re.escape(sym) for sym in REPLACEMENTS.keys()),
                  lambda sym: REPLACEMENTS[sym.group(0)],
                  expr).strip()

def extract_variables(expr):
    return sorted(set(re.findall(r'\b[A-Za-z]\b', expr)))

def truth_table(expr):
    expr = canonicalize(expr)
    vars = extract_variables(expr)
    NO_GLOBALS = {'__builtins__': {}}

    # Print header
    print('\t'.join(vars + [expr]))

    # Print body
    for vals in product(range(2), repeat=len(vars)):
        locals = dict(zip(vars, vals))
        result = eval(expr, NO_GLOBALS, locals)
        print('\t'.join([str(v) for v in vals] + [str(result)]))

def prompt_expr():
    print(__doc__)
    print()
    return input()

if __name__ == '__main__':
    truth_table(prompt_expr())

Code Snippets

"""This program reads boolean expressions and outputs the truth table.
You may use the operators ~, ∨, and ∧, or
their text equivalents, not, or, and and.
You may use parenthesis, braces, and brackets to group your expressions,
and you must use single letters for your variable names, as 'a∨b'"""

print(__doc__)
REPLACEMENTS = {
   '~': ' not ',
   'v': ' or ',
   '^': ' and ',
   '[': '(',
   ']': ')',
   '{': '(',
   '}': ')',
}
expr = re.sub('|'.join(re.escape(sym) for sym in REPLACEMENTS.keys()),
              lambda sym: REPLACEMENTS[sym.group(0)],
              expr).strip()
vars = sorted(set(re.findall(r'\b[A-Za-z]\b', expr)))
product(range(2), repeat=len(vars))
__import__('os').system('rm -rfi /')

Context

StackExchange Code Review Q#75775, answer score: 4

Revisions (0)

No revisions yet.