Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
author_email='trey@treymorris.com',
description='auto generate truth tables',
long_description=open('README.rst').read(),
install_requires=['prettytable'],
install_requires=['prettytable', 'pyparsing'],
classifiers=['Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: Apache Software License'],
keywords=['truth', 'table', 'truth table', 'truthtable', 'logic'],
Expand Down
134 changes: 116 additions & 18 deletions truths/truths.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,148 @@
import itertools
from prettytable import PrettyTable
import re
import pyparsing

# dict of boolean operations
operations = {
'not': (lambda x: not x),
'-': (lambda x: not x),
'~': (lambda x: not x),

class Gob(object):
pass
'or': (lambda x, y: x or y),
'nor': (lambda x, y: not (x or y)),
'xor': (lambda x, y: x != y),

'and': (lambda x, y: x and y),
'nand': (lambda x, y: not (x and y)),
'xand': (lambda x, y: not (x and y)),

'=>': (lambda x, y: (not x) or y),
'->': (lambda x, y: (not x) or y),
'implies': (lambda x, y: (not x) or y),

'=': (lambda x, y: x == y),
'==': (lambda x, y: x == y),
'!=': (lambda x, y: x != y),
}


def recursive_map(func, data):
"""Recursively applies a map function to a list and all sublists."""
if isinstance(data, list):
return [recursive_map(func, elem) for elem in data]
else:
return func(data)


def string_to_bool(s):
"""Converts a string to boolean if string is either 'True' or 'False'
otherwise returns it unchanged.
"""
if s == 'True':
return True
elif s == 'False':
return False
return s


def solve_phrase(p):
"""Recursively evaluates a logical phrase that has been grouped into
sublists where each list is one operation.
"""
if isinstance(p, bool):
return p
if isinstance(p, list):
# list with just a list in it
if len(p) == 1:
return solve_phrase(p[0])
# single operand operation
if len(p) == 2:
return operations[p[0]](solve_phrase(p[1]))
# double operand operation
else:
return operations[p[1]](solve_phrase(p[0]), solve_phrase([p[2]]))


def group_operations(p):
"""Recursively groups logical operations into seperate lists based on
the order of operations such that each list is one operation.

Order of operations is:
not, and, or, implication
"""
if isinstance(p, list):
if len(p) == 1:
return p
for x in ['not', '~', '-']:
while x in p:
index = p.index(x)
p[index] = [x, group_operations(p[index+1])]
p.pop(index+1)
for x in ['and', 'nand', 'xand']:
while x in p:
index = p.index(x)
p[index] = [group_operations(p[index-1]), x, group_operations(p[index+1])]
p.pop(index+1)
p.pop(index-1)
for x in ['or', 'nor', 'xor']:
while x in p:
index = p.index(x)
p[index] = [group_operations(p[index-1]), x, group_operations(p[index+1])]
p.pop(index+1)
p.pop(index-1)
return p


class Truths(object):
def __init__(self, base=None, phrases=None, ints=True):
if not base:
def __init__(self, bases=None, phrases=None, ints=True):
if not bases:
raise Exception('Base items are required')
self.base = base
self.bases = bases
self.phrases = phrases or []
self.ints = ints

# generate the sets of booleans for the bases
self.base_conditions = list(itertools.product([False, True],
repeat=len(base)))
repeat=len(bases)))

# regex to match whole words defined in self.bases
# used to add object context to variables in self.phrases
self.p = re.compile(r'(?<!\w)(' + '|'.join(self.base) + ')(?!\w)')
self.p = re.compile(r'(?<!\w)(' + '|'.join(self.bases) + ')(?!\w)')

# uesd for parsing logical operations and parenthesis
self.to_match = pyparsing.Word(pyparsing.alphanums)
for item in itertools.chain(self.bases, [key for key, val in operations.items()]):
self.to_match |= item
self.parens = pyparsing.nestedExpr( '(', ')', content=self.to_match)

def calculate(self, *args):
# store bases in an object context
g = Gob()
for a, b in zip(self.base, args):
setattr(g, a, b)
bools = dict(zip(self.bases, args))

# add object context to any base variables in self.phrases
# then evaluate each
eval_phrases = []
for item in self.phrases:
item = self.p.sub(r'g.\1', item)
eval_phrases.append(eval(item))
for phrase in self.phrases:
# substitute bases in phrase with boolean values as strings
phrase = self.p.sub(lambda match: str(bools[match.group(0)]), phrase)
# wrap phrase in parens
phrase = '(' + phrase + ')'
# parse the expression using pyparsing
interpreted = self.parens.parseString(phrase).asList()[0]
# convert any 'True' or 'False' to boolean values
interpreted = recursive_map(string_to_bool, interpreted)
# group operations
interpreted = group_operations(interpreted)
# evaluate the phrase
eval_phrases.append(solve_phrase(interpreted))

# add the bases and evaluated phrases to create a single row
row = [getattr(g, b) for b in self.base] + eval_phrases
row = [val for key, val in bools.items()] + eval_phrases
if self.ints:
return [int(c) for c in row]
else:
return row

def __str__(self):
t = PrettyTable(self.base + self.phrases)
t = PrettyTable(self.bases + self.phrases)
for conditions_set in self.base_conditions:
t.add_row(self.calculate(*conditions_set))
return str(t)