|
1
|
|
|
""" |
|
2
|
|
|
Boolean expressions algebra. |
|
3
|
|
|
|
|
4
|
|
|
This module defines a Boolean algebra over the set {TRUE, FALSE} with boolean |
|
5
|
|
|
variables called Symbols and the boolean functions AND, OR, NOT. |
|
6
|
|
|
|
|
7
|
|
|
Some basic logic comparison is supported: two expressions can be |
|
8
|
|
|
compared for equivalence or containment. Furthermore you can simplify |
|
9
|
|
|
an expression and obtain its normal form. |
|
10
|
|
|
|
|
11
|
|
|
You can create expressions in Python using familiar boolean operators |
|
12
|
|
|
or parse expressions from strings. The parsing can be extended with |
|
13
|
|
|
your own tokenizer. You can also customize how expressions behave and |
|
14
|
|
|
how they are presented. |
|
15
|
|
|
|
|
16
|
|
|
For extensive documentation look either into the docs directory or view it |
|
17
|
|
|
online, at https://booleanpy.readthedocs.org/en/latest/. |
|
18
|
|
|
|
|
19
|
|
|
Copyright (c) 2009-2020 Sebastian Kraemer, [email protected] and others |
|
20
|
|
|
SPDX-License-Identifier: BSD-2-Clause |
|
21
|
|
|
""" |
|
22
|
|
|
|
|
23
|
2 |
|
from __future__ import absolute_import |
|
24
|
2 |
|
from __future__ import unicode_literals |
|
25
|
2 |
|
from __future__ import print_function |
|
26
|
|
|
|
|
27
|
2 |
|
import inspect |
|
28
|
2 |
|
import itertools |
|
29
|
2 |
|
from operator import and_ as and_operator |
|
30
|
2 |
|
from operator import or_ as or_operator |
|
31
|
|
|
|
|
32
|
|
|
# Python 2 and 3 |
|
33
|
2 |
|
try: |
|
34
|
2 |
|
basestring # NOQA |
|
|
|
|
|
|
35
|
2 |
|
except NameError: |
|
36
|
2 |
|
basestring = str # NOQA |
|
37
|
|
|
|
|
38
|
|
|
# Python 2 and 3 |
|
39
|
2 |
|
try: |
|
40
|
|
|
# Python 2 |
|
41
|
2 |
|
reduce # NOQA |
|
|
|
|
|
|
42
|
2 |
|
except NameError: |
|
43
|
|
|
# Python 3 |
|
44
|
2 |
|
from functools import reduce # NOQA |
|
45
|
|
|
|
|
46
|
|
|
# Set to True to enable tracing for parsing |
|
47
|
2 |
|
TRACE_PARSE = False |
|
48
|
|
|
|
|
49
|
|
|
# Token types for standard operators and parens |
|
50
|
2 |
|
TOKEN_AND = 1 |
|
51
|
2 |
|
TOKEN_OR = 2 |
|
52
|
2 |
|
TOKEN_NOT = 3 |
|
53
|
2 |
|
TOKEN_LPAR = 4 |
|
54
|
2 |
|
TOKEN_RPAR = 5 |
|
55
|
2 |
|
TOKEN_TRUE = 6 |
|
56
|
2 |
|
TOKEN_FALSE = 7 |
|
57
|
2 |
|
TOKEN_SYMBOL = 8 |
|
58
|
|
|
|
|
59
|
2 |
|
TOKEN_TYPES = { |
|
60
|
|
|
TOKEN_AND: 'AND', |
|
61
|
|
|
TOKEN_OR: 'OR', |
|
62
|
|
|
TOKEN_NOT: 'NOT', |
|
63
|
|
|
TOKEN_LPAR: '(', |
|
64
|
|
|
TOKEN_RPAR: ')', |
|
65
|
|
|
TOKEN_TRUE: 'TRUE', |
|
66
|
|
|
TOKEN_FALSE: 'FALSE', |
|
67
|
|
|
TOKEN_SYMBOL: 'SYMBOL', |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
# parsing error code and messages |
|
71
|
2 |
|
PARSE_UNKNOWN_TOKEN = 1 |
|
72
|
2 |
|
PARSE_UNBALANCED_CLOSING_PARENS = 2 |
|
73
|
2 |
|
PARSE_INVALID_EXPRESSION = 3 |
|
74
|
2 |
|
PARSE_INVALID_NESTING = 4 |
|
75
|
2 |
|
PARSE_INVALID_SYMBOL_SEQUENCE = 5 |
|
76
|
2 |
|
PARSE_INVALID_OPERATOR_SEQUENCE = 6 |
|
77
|
|
|
|
|
78
|
2 |
|
PARSE_ERRORS = { |
|
79
|
|
|
PARSE_UNKNOWN_TOKEN: 'Unknown token', |
|
80
|
|
|
PARSE_UNBALANCED_CLOSING_PARENS: 'Unbalanced parenthesis', |
|
81
|
|
|
PARSE_INVALID_EXPRESSION: 'Invalid expression', |
|
82
|
|
|
PARSE_INVALID_NESTING: 'Invalid expression nesting such as (AND xx)', |
|
83
|
|
|
PARSE_INVALID_SYMBOL_SEQUENCE: 'Invalid symbols sequence such as (A B)', |
|
84
|
|
|
PARSE_INVALID_OPERATOR_SEQUENCE: 'Invalid operator sequence without symbols such as AND OR or OR OR', |
|
85
|
|
|
} |
|
86
|
|
|
|
|
87
|
|
|
|
|
88
|
2 |
|
class ParseError(Exception): |
|
89
|
|
|
""" |
|
90
|
|
|
Raised when the parser or tokenizer encounters a syntax error. Instances of |
|
91
|
|
|
this class have attributes token_type, token_string, position, error_code to |
|
92
|
|
|
access the details of the error. str() of the exception instance returns a |
|
93
|
|
|
formatted message. |
|
94
|
|
|
""" |
|
95
|
|
|
|
|
96
|
2 |
|
def __init__(self, token_type=None, token_string='', position=-1, error_code=0): |
|
97
|
|
|
self.token_type = token_type |
|
98
|
|
|
self.token_string = token_string |
|
99
|
|
|
self.position = position |
|
100
|
|
|
self.error_code = error_code |
|
101
|
|
|
|
|
102
|
2 |
|
def __str__(self, *args, **kwargs): |
|
103
|
|
|
emsg = PARSE_ERRORS.get(self.error_code, 'Unknown parsing error') |
|
104
|
|
|
|
|
105
|
|
|
tstr = '' |
|
106
|
|
|
if self.token_string: |
|
107
|
|
|
tstr = ' for token: "%s"' % self.token_string |
|
108
|
|
|
|
|
109
|
|
|
pos = '' |
|
110
|
|
|
if self.position > 0: |
|
111
|
|
|
pos = ' at position: %d' % self.position |
|
112
|
|
|
|
|
113
|
|
|
return '{emsg}{tstr}{pos}'.format(**locals()) |
|
114
|
|
|
|
|
115
|
|
|
|
|
116
|
2 |
|
class BooleanAlgebra(object): |
|
117
|
|
|
""" |
|
118
|
|
|
An algebra is defined by: |
|
119
|
|
|
- the types of its operations and Symbol. |
|
120
|
|
|
- the tokenizer used when parsing expressions from strings. |
|
121
|
|
|
|
|
122
|
|
|
This class also serves as a base class for all boolean expressions, |
|
123
|
|
|
including base elements, functions and variable symbols. |
|
124
|
|
|
""" |
|
125
|
|
|
|
|
126
|
2 |
|
def __init__(self, TRUE_class=None, FALSE_class=None, Symbol_class=None, Function_class=None, |
|
127
|
|
|
NOT_class=None, AND_class=None, OR_class=None, |
|
128
|
|
|
allowed_in_token=('.', ':', '_')): |
|
129
|
|
|
""" |
|
130
|
|
|
The types for TRUE, FALSE, NOT, AND, OR and Symbol define the boolean |
|
131
|
|
|
algebra elements, operations and Symbol variable. They default to the |
|
132
|
|
|
standard classes if not provided. |
|
133
|
|
|
|
|
134
|
|
|
You can customize an algebra by providing alternative subclasses of the |
|
135
|
|
|
standard types. |
|
136
|
|
|
""" |
|
137
|
|
|
# TRUE and FALSE base elements are algebra-level "singleton" instances |
|
138
|
2 |
|
self.TRUE = TRUE_class or _TRUE |
|
139
|
2 |
|
self.TRUE = self.TRUE() |
|
140
|
|
|
|
|
141
|
2 |
|
self.FALSE = FALSE_class or _FALSE |
|
142
|
2 |
|
self.FALSE = self.FALSE() |
|
143
|
|
|
|
|
144
|
|
|
# they cross-reference each other |
|
145
|
2 |
|
self.TRUE.dual = self.FALSE |
|
146
|
2 |
|
self.FALSE.dual = self.TRUE |
|
147
|
|
|
|
|
148
|
|
|
# boolean operation types, defaulting to the standard types |
|
149
|
2 |
|
self.NOT = NOT_class or NOT |
|
150
|
2 |
|
self.AND = AND_class or AND |
|
151
|
2 |
|
self.OR = OR_class or OR |
|
152
|
|
|
|
|
153
|
|
|
# class used for Symbols and Functions |
|
154
|
2 |
|
self.Symbol = Symbol_class or Symbol |
|
155
|
2 |
|
self.Function = Function_class or Function |
|
156
|
|
|
|
|
157
|
2 |
|
tf_nao = { |
|
158
|
|
|
'TRUE': self.TRUE, |
|
159
|
|
|
'FALSE': self.FALSE, |
|
160
|
|
|
'NOT': self.NOT, |
|
161
|
|
|
'AND': self.AND, |
|
162
|
|
|
'OR': self.OR, |
|
163
|
|
|
'Symbol': self.Symbol, |
|
164
|
|
|
'Function': self.Function # TODO: Do we need it? |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
# setup cross references such that all algebra types and |
|
168
|
|
|
# objects hold a named attribute for every other types and |
|
169
|
|
|
# objects, including themselves. |
|
170
|
2 |
|
for obj in tf_nao.values(): |
|
171
|
2 |
|
for name, value in tf_nao.items(): |
|
172
|
2 |
|
setattr(obj, name, value) |
|
173
|
|
|
|
|
174
|
|
|
# Set the set of characters allowed in tokens |
|
175
|
2 |
|
self.allowed_in_token = allowed_in_token |
|
176
|
|
|
|
|
177
|
2 |
|
def definition(self): |
|
178
|
|
|
""" |
|
179
|
|
|
Return a tuple of this algebra defined elements and types as: |
|
180
|
|
|
(TRUE, FALSE, NOT, AND, OR, Symbol) |
|
181
|
|
|
""" |
|
182
|
|
|
return self.TRUE, self.FALSE, self.NOT, self.AND, self.OR, self.Symbol |
|
183
|
|
|
|
|
184
|
2 |
|
def symbols(self, *args): |
|
185
|
|
|
""" |
|
186
|
|
|
Return a tuple of symbols building a new Symbol from each argument. |
|
187
|
|
|
""" |
|
188
|
|
|
return tuple(map(self.Symbol, args)) |
|
189
|
|
|
|
|
190
|
2 |
|
def parse(self, expr, simplify=False): |
|
191
|
|
|
""" |
|
192
|
|
|
Return a boolean expression parsed from `expr` either a unicode string |
|
193
|
|
|
or tokens iterable. |
|
194
|
|
|
|
|
195
|
|
|
Optionally simplify the expression if `simplify` is True. |
|
196
|
|
|
|
|
197
|
|
|
Raise ParseError on errors. |
|
198
|
|
|
|
|
199
|
|
|
If `expr` is a string, the standard `tokenizer` is used for tokenization |
|
200
|
|
|
and the algebra configured Symbol type is used to create Symbol |
|
201
|
|
|
instances from Symbol tokens. |
|
202
|
|
|
|
|
203
|
|
|
If `expr` is an iterable, it should contain 3-tuples of: (token_type, |
|
204
|
|
|
token_string, token_position). In this case, the `token_type` can be |
|
205
|
|
|
a Symbol instance or one of the TOKEN_* constant types. |
|
206
|
|
|
See the `tokenize()` method for detailed specification. |
|
207
|
|
|
""" |
|
208
|
|
|
|
|
209
|
2 |
|
precedence = {self.NOT: 5, self.AND: 10, self.OR: 15, TOKEN_LPAR: 20} |
|
210
|
|
|
|
|
211
|
2 |
|
if isinstance(expr, basestring): |
|
212
|
2 |
|
tokenized = self.tokenize(expr) |
|
213
|
|
|
else: |
|
214
|
|
|
tokenized = iter(expr) |
|
215
|
|
|
|
|
216
|
2 |
|
if TRACE_PARSE: |
|
217
|
|
|
tokenized = list(tokenized) |
|
218
|
|
|
print('tokens:') |
|
219
|
|
|
for t in tokenized: |
|
220
|
|
|
print(t) |
|
221
|
|
|
tokenized = iter(tokenized) |
|
222
|
|
|
|
|
223
|
|
|
# the abstract syntax tree for this expression that will be build as we |
|
224
|
|
|
# process tokens |
|
225
|
|
|
# the first two items are None |
|
226
|
|
|
# symbol items are appended to this structure |
|
227
|
2 |
|
ast = [None, None] |
|
228
|
|
|
|
|
229
|
2 |
|
def is_sym(_t): |
|
230
|
2 |
|
return isinstance(_t, Symbol) or _t in (TOKEN_TRUE, TOKEN_FALSE, TOKEN_SYMBOL) |
|
231
|
|
|
|
|
232
|
2 |
|
def is_operator(_t): |
|
233
|
2 |
|
return _t in (TOKEN_AND, TOKEN_OR) |
|
234
|
|
|
|
|
235
|
2 |
|
prev_token = None |
|
236
|
2 |
|
for token_type, token_string, token_position in tokenized: |
|
237
|
2 |
|
if TRACE_PARSE: |
|
238
|
|
|
print('\nprocessing token_type:', repr(token_type), 'token_string:', repr(token_string), 'token_position:', repr(token_position)) |
|
239
|
|
|
|
|
240
|
2 |
|
if prev_token: |
|
241
|
2 |
|
prev_token_type, _prev_token_string, _prev_token_position = prev_token |
|
242
|
2 |
|
if TRACE_PARSE: |
|
243
|
|
|
print(' prev_token:', repr(prev_token)) |
|
244
|
|
|
|
|
245
|
2 |
|
if is_sym(prev_token_type) and (is_sym(token_type)): # or token_type == TOKEN_LPAR) : |
|
246
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_INVALID_SYMBOL_SEQUENCE) |
|
247
|
|
|
|
|
248
|
2 |
|
if is_operator(prev_token_type) and (is_operator(token_type) or token_type == TOKEN_RPAR): |
|
249
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE) |
|
250
|
|
|
|
|
251
|
|
|
else: |
|
252
|
2 |
|
if is_operator(token_type): |
|
253
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE) |
|
254
|
|
|
|
|
255
|
2 |
|
if token_type == TOKEN_SYMBOL: |
|
256
|
2 |
|
ast.append(self.Symbol(token_string)) |
|
257
|
2 |
|
if TRACE_PARSE: |
|
258
|
|
|
print(' ast: token_type is TOKEN_SYMBOL: append new symbol', repr(ast)) |
|
259
|
|
|
|
|
260
|
2 |
|
elif isinstance(token_type, Symbol): |
|
261
|
|
|
ast.append(token_type) |
|
262
|
|
|
if TRACE_PARSE: |
|
263
|
|
|
print(' ast: token_type is Symbol): append existing symbol', repr(ast)) |
|
264
|
|
|
|
|
265
|
2 |
|
elif token_type == TOKEN_TRUE: |
|
266
|
|
|
ast.append(self.TRUE) |
|
267
|
|
|
if TRACE_PARSE: print(' ast: token_type is TOKEN_TRUE:', repr(ast)) |
|
268
|
|
|
|
|
269
|
2 |
|
elif token_type == TOKEN_FALSE: |
|
270
|
|
|
ast.append(self.FALSE) |
|
271
|
|
|
if TRACE_PARSE: print(' ast: token_type is TOKEN_FALSE:', repr(ast)) |
|
272
|
|
|
|
|
273
|
2 |
|
elif token_type == TOKEN_NOT: |
|
274
|
2 |
|
ast = [ast, self.NOT] |
|
275
|
2 |
|
if TRACE_PARSE: print(' ast: token_type is TOKEN_NOT:', repr(ast)) |
|
276
|
|
|
|
|
277
|
2 |
|
elif token_type == TOKEN_AND: |
|
278
|
2 |
|
ast = self._start_operation(ast, self.AND, precedence) |
|
279
|
2 |
|
if TRACE_PARSE: |
|
280
|
|
|
print(' ast:token_type is TOKEN_AND: start_operation', ast) |
|
281
|
|
|
|
|
282
|
2 |
|
elif token_type == TOKEN_OR: |
|
283
|
2 |
|
ast = self._start_operation(ast, self.OR, precedence) |
|
284
|
2 |
|
if TRACE_PARSE: |
|
285
|
|
|
print(' ast:token_type is TOKEN_OR: start_operation', ast) |
|
286
|
|
|
|
|
287
|
2 |
|
elif token_type == TOKEN_LPAR: |
|
288
|
2 |
|
if prev_token: |
|
289
|
|
|
# Check that an opening parens is preceded by a function |
|
290
|
|
|
# or an opening parens |
|
291
|
2 |
|
if prev_token_type not in (TOKEN_NOT, TOKEN_AND, TOKEN_OR, TOKEN_LPAR): |
|
|
|
|
|
|
292
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_INVALID_NESTING) |
|
293
|
2 |
|
ast = [ast, TOKEN_LPAR] |
|
294
|
|
|
|
|
295
|
2 |
|
elif token_type == TOKEN_RPAR: |
|
296
|
2 |
|
while True: |
|
297
|
2 |
|
if ast[0] is None: |
|
298
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_UNBALANCED_CLOSING_PARENS) |
|
299
|
|
|
|
|
300
|
2 |
|
if ast[1] is TOKEN_LPAR: |
|
301
|
2 |
|
ast[0].append(ast[2]) |
|
302
|
2 |
|
if TRACE_PARSE: print('ast9:', repr(ast)) |
|
303
|
2 |
|
ast = ast[0] |
|
304
|
2 |
|
if TRACE_PARSE: print('ast10:', repr(ast)) |
|
305
|
2 |
|
break |
|
306
|
|
|
|
|
307
|
2 |
|
if isinstance(ast[1], int): |
|
308
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_UNBALANCED_CLOSING_PARENS) |
|
309
|
|
|
|
|
310
|
|
|
# the parens are properly nested |
|
311
|
|
|
# the top ast node should be a function subclass |
|
312
|
2 |
|
if not (inspect.isclass(ast[1]) and issubclass(ast[1], self.Function)): |
|
313
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_INVALID_NESTING) |
|
314
|
|
|
|
|
315
|
2 |
|
subex = ast[1](*ast[2:]) |
|
316
|
2 |
|
ast[0].append(subex) |
|
317
|
2 |
|
if TRACE_PARSE: print('ast11:', repr(ast)) |
|
318
|
2 |
|
ast = ast[0] |
|
319
|
2 |
|
if TRACE_PARSE: print('ast12:', repr(ast)) |
|
320
|
|
|
else: |
|
321
|
|
|
raise ParseError(token_type, token_string, token_position, PARSE_UNKNOWN_TOKEN) |
|
322
|
|
|
|
|
323
|
2 |
|
prev_token = (token_type, token_string, token_position) |
|
324
|
|
|
|
|
325
|
2 |
|
try: |
|
326
|
2 |
|
while True: |
|
327
|
2 |
|
if ast[0] is None: |
|
328
|
2 |
|
if TRACE_PARSE: print('ast[0] is None:', repr(ast)) |
|
329
|
2 |
|
if ast[1] is None: |
|
330
|
2 |
|
if TRACE_PARSE: print(' ast[1] is None:', repr(ast)) |
|
331
|
2 |
|
if len(ast) != 3: |
|
332
|
|
|
raise ParseError(error_code=PARSE_INVALID_EXPRESSION) |
|
333
|
2 |
|
parsed = ast[2] |
|
334
|
2 |
|
if TRACE_PARSE: print(' parsed = ast[2]:', repr(parsed)) |
|
335
|
|
|
|
|
336
|
|
|
else: |
|
337
|
|
|
# call the function in ast[1] with the rest of the ast as args |
|
338
|
2 |
|
parsed = ast[1](*ast[2:]) |
|
339
|
2 |
|
if TRACE_PARSE: print(' parsed = ast[1](*ast[2:]):', repr(parsed)) |
|
340
|
2 |
|
break |
|
341
|
|
|
else: |
|
342
|
2 |
|
if TRACE_PARSE: print('subex = ast[1](*ast[2:]):', repr(ast)) |
|
343
|
2 |
|
subex = ast[1](*ast[2:]) |
|
344
|
2 |
|
ast[0].append(subex) |
|
345
|
2 |
|
if TRACE_PARSE: print(' ast[0].append(subex):', repr(ast)) |
|
346
|
2 |
|
ast = ast[0] |
|
347
|
2 |
|
if TRACE_PARSE: print(' ast = ast[0]:', repr(ast)) |
|
348
|
|
|
except TypeError: |
|
349
|
|
|
raise ParseError(error_code=PARSE_INVALID_EXPRESSION) |
|
350
|
|
|
|
|
351
|
2 |
|
if simplify: |
|
352
|
2 |
|
return parsed.simplify() |
|
353
|
|
|
|
|
354
|
2 |
|
if TRACE_PARSE: print('final parsed:', repr(parsed)) |
|
355
|
2 |
|
return parsed |
|
356
|
|
|
|
|
357
|
2 |
|
def _start_operation(self, ast, operation, precedence): |
|
358
|
|
|
""" |
|
359
|
|
|
Return an AST where all operations of lower precedence are finalized. |
|
360
|
|
|
""" |
|
361
|
2 |
|
if TRACE_PARSE: |
|
362
|
|
|
print(' start_operation:', repr(operation), 'AST:', ast) |
|
363
|
|
|
|
|
364
|
2 |
|
op_prec = precedence[operation] |
|
365
|
2 |
|
while True: |
|
366
|
2 |
|
if ast[1] is None: |
|
367
|
|
|
# [None, None, x] |
|
368
|
2 |
|
if TRACE_PARSE: print(' start_op: ast[1] is None:', repr(ast)) |
|
369
|
2 |
|
ast[1] = operation |
|
370
|
2 |
|
if TRACE_PARSE: print(' --> start_op: ast[1] is None:', repr(ast)) |
|
371
|
2 |
|
return ast |
|
372
|
|
|
|
|
373
|
2 |
|
prec = precedence[ast[1]] |
|
374
|
2 |
|
if prec > op_prec: # op=&, [ast, |, x, y] -> [[ast, |, x], &, y] |
|
375
|
2 |
|
if TRACE_PARSE: print(' start_op: prec > op_prec:', repr(ast)) |
|
376
|
2 |
|
ast = [ast, operation, ast.pop(-1)] |
|
377
|
2 |
|
if TRACE_PARSE: print(' --> start_op: prec > op_prec:', repr(ast)) |
|
378
|
2 |
|
return ast |
|
379
|
|
|
|
|
380
|
2 |
|
if prec == op_prec: # op=&, [ast, &, x] -> [ast, &, x] |
|
381
|
2 |
|
if TRACE_PARSE: print(' start_op: prec == op_prec:', repr(ast)) |
|
382
|
2 |
|
return ast |
|
383
|
|
|
|
|
384
|
2 |
|
if not (inspect.isclass(ast[1]) and issubclass(ast[1], self.Function)): |
|
385
|
|
|
# the top ast node should be a function subclass at this stage |
|
386
|
|
|
raise ParseError(error_code=PARSE_INVALID_NESTING) |
|
387
|
|
|
|
|
388
|
2 |
|
if ast[0] is None: # op=|, [None, &, x, y] -> [None, |, x&y] |
|
389
|
2 |
|
if TRACE_PARSE: print(' start_op: ast[0] is None:', repr(ast)) |
|
390
|
2 |
|
subexp = ast[1](*ast[2:]) |
|
391
|
2 |
|
new_ast = [ast[0], operation, subexp] |
|
392
|
2 |
|
if TRACE_PARSE: print(' --> start_op: ast[0] is None:', repr(new_ast)) |
|
393
|
2 |
|
return new_ast |
|
394
|
|
|
|
|
395
|
|
|
else: # op=|, [[ast, &, x], ~, y] -> [ast, &, x, ~y] |
|
396
|
2 |
|
if TRACE_PARSE: print(' start_op: else:', repr(ast)) |
|
397
|
2 |
|
ast[0].append(ast[1](*ast[2:])) |
|
398
|
2 |
|
ast = ast[0] |
|
399
|
2 |
|
if TRACE_PARSE: print(' --> start_op: else:', repr(ast)) |
|
400
|
|
|
|
|
401
|
2 |
|
def tokenize(self, expr): |
|
402
|
|
|
""" |
|
403
|
|
|
Return an iterable of 3-tuple describing each token given an expression |
|
404
|
|
|
unicode string. |
|
405
|
|
|
|
|
406
|
|
|
This 3-tuple contains (token, token string, position): |
|
407
|
|
|
- token: either a Symbol instance or one of TOKEN_* token types. |
|
408
|
|
|
- token string: the original token unicode string. |
|
409
|
|
|
- position: some simple object describing the starting position of the |
|
410
|
|
|
original token string in the `expr` string. It can be an int for a |
|
411
|
|
|
character offset, or a tuple of starting (row/line, column). |
|
412
|
|
|
|
|
413
|
|
|
The token position is used only for error reporting and can be None or |
|
414
|
|
|
empty. |
|
415
|
|
|
|
|
416
|
|
|
Raise ParseError on errors. The ParseError.args is a tuple of: |
|
417
|
|
|
(token_string, position, error message) |
|
418
|
|
|
|
|
419
|
|
|
You can use this tokenizer as a base to create specialized tokenizers |
|
420
|
|
|
for your custom algebra by subclassing BooleanAlgebra. See also the |
|
421
|
|
|
tests for other examples of alternative tokenizers. |
|
422
|
|
|
|
|
423
|
|
|
This tokenizer has these characteristics: |
|
424
|
|
|
- The `expr` string can span multiple lines, |
|
425
|
|
|
- Whitespace is not significant. |
|
426
|
|
|
- The returned position is the starting character offset of a token. |
|
427
|
|
|
|
|
428
|
|
|
- A TOKEN_SYMBOL is returned for valid identifiers which is a string |
|
429
|
|
|
without spaces. These are valid identifiers: |
|
430
|
|
|
- Python identifiers. |
|
431
|
|
|
- a string even if starting with digits |
|
432
|
|
|
- digits (except for 0 and 1). |
|
433
|
|
|
- dotted names : foo.bar consist of one token. |
|
434
|
|
|
- names with colons: foo:bar consist of one token. |
|
435
|
|
|
These are not identifiers: |
|
436
|
|
|
- quoted strings. |
|
437
|
|
|
- any punctuation which is not an operation |
|
438
|
|
|
|
|
439
|
|
|
- Recognized operators are (in any upper/lower case combinations): |
|
440
|
|
|
- for and: '*', '&', 'and' |
|
441
|
|
|
- for or: '+', '|', 'or' |
|
442
|
|
|
- for not: '~', '!', 'not' |
|
443
|
|
|
|
|
444
|
|
|
- Recognized special symbols are (in any upper/lower case combinations): |
|
445
|
|
|
- True symbols: 1 and True |
|
446
|
|
|
- False symbols: 0, False and None |
|
447
|
|
|
""" |
|
448
|
2 |
|
if not isinstance(expr, basestring): |
|
449
|
|
|
raise TypeError('expr must be string but it is %s.' % type(expr)) |
|
450
|
|
|
|
|
451
|
|
|
# mapping of lowercase token strings to a token type id for the standard |
|
452
|
|
|
# operators, parens and common true or false symbols, as used in the |
|
453
|
|
|
# default tokenizer implementation. |
|
454
|
2 |
|
TOKENS = { |
|
455
|
|
|
'*': TOKEN_AND, '&': TOKEN_AND, 'and': TOKEN_AND, |
|
456
|
|
|
'+': TOKEN_OR, '|': TOKEN_OR, 'or': TOKEN_OR, |
|
457
|
|
|
'~': TOKEN_NOT, '!': TOKEN_NOT, 'not': TOKEN_NOT, |
|
458
|
|
|
'(': TOKEN_LPAR, ')': TOKEN_RPAR, |
|
459
|
|
|
'[': TOKEN_LPAR, ']': TOKEN_RPAR, |
|
460
|
|
|
'true': TOKEN_TRUE, '1': TOKEN_TRUE, |
|
461
|
|
|
'false': TOKEN_FALSE, '0': TOKEN_FALSE, 'none': TOKEN_FALSE |
|
462
|
|
|
} |
|
463
|
|
|
|
|
464
|
2 |
|
position = 0 |
|
465
|
2 |
|
length = len(expr) |
|
466
|
|
|
|
|
467
|
2 |
|
while position < length: |
|
468
|
2 |
|
tok = expr[position] |
|
469
|
|
|
|
|
470
|
2 |
|
sym = tok.isalnum() or tok == '_' |
|
471
|
2 |
|
if sym: |
|
472
|
2 |
|
position += 1 |
|
473
|
2 |
|
while position < length: |
|
474
|
2 |
|
char = expr[position] |
|
475
|
2 |
|
if char.isalnum() or char in self.allowed_in_token: |
|
476
|
2 |
|
position += 1 |
|
477
|
2 |
|
tok += char |
|
478
|
|
|
else: |
|
479
|
2 |
|
break |
|
480
|
2 |
|
position -= 1 |
|
481
|
|
|
|
|
482
|
2 |
|
try: |
|
483
|
2 |
|
yield TOKENS[tok.lower()], tok, position |
|
484
|
2 |
|
except KeyError: |
|
485
|
2 |
|
if sym: |
|
486
|
2 |
|
yield TOKEN_SYMBOL, tok, position |
|
487
|
2 |
|
elif tok not in (' ', '\t', '\r', '\n'): |
|
488
|
|
|
raise ParseError(token_string=tok, position=position, |
|
489
|
|
|
error_code=PARSE_UNKNOWN_TOKEN) |
|
490
|
|
|
|
|
491
|
2 |
|
position += 1 |
|
492
|
|
|
|
|
493
|
|
|
# TODO: explain what this means exactly |
|
494
|
2 |
|
def _rdistributive(self, expr, op_example): |
|
495
|
|
|
""" |
|
496
|
|
|
Recursively flatten the `expr` expression for the `op_example` |
|
497
|
|
|
AND or OR operation instance exmaple. |
|
498
|
|
|
""" |
|
499
|
2 |
|
if expr.isliteral: |
|
500
|
2 |
|
return expr |
|
501
|
|
|
|
|
502
|
2 |
|
expr_class = expr.__class__ |
|
503
|
|
|
|
|
504
|
2 |
|
args = (self._rdistributive(arg, op_example) for arg in expr.args) |
|
|
|
|
|
|
505
|
2 |
|
args = tuple(arg.simplify() for arg in args) |
|
506
|
2 |
|
if len(args) == 1: |
|
507
|
|
|
return args[0] |
|
508
|
|
|
|
|
509
|
2 |
|
expr = expr_class(*args) |
|
510
|
|
|
|
|
511
|
2 |
|
dualoperation = op_example.dual |
|
512
|
2 |
|
if isinstance(expr, dualoperation): |
|
513
|
2 |
|
expr = expr.distributive() |
|
514
|
2 |
|
return expr |
|
515
|
|
|
|
|
516
|
2 |
|
def normalize(self, expr, operation): |
|
517
|
|
|
""" |
|
518
|
|
|
Return a normalized expression transformed to its normal form in the |
|
519
|
|
|
given AND or OR operation. |
|
520
|
|
|
|
|
521
|
|
|
The new expression arguments will satisfy these conditions: |
|
522
|
|
|
- operation(*args) == expr (here mathematical equality is meant) |
|
523
|
|
|
- the operation does not occur in any of its arg. |
|
524
|
|
|
- NOT is only appearing in literals (aka. Negation normal form). |
|
525
|
|
|
|
|
526
|
|
|
The operation must be an AND or OR operation or a subclass. |
|
527
|
|
|
""" |
|
528
|
|
|
# ensure that the operation is not NOT |
|
529
|
2 |
|
assert operation in (self.AND, self.OR,) |
|
530
|
|
|
# Move NOT inwards. |
|
531
|
2 |
|
expr = expr.literalize() |
|
532
|
|
|
# Simplify first otherwise _rdistributive() may take forever. |
|
533
|
2 |
|
expr = expr.simplify() |
|
534
|
2 |
|
operation_example = operation(self.TRUE, self.FALSE) |
|
535
|
2 |
|
expr = self._rdistributive(expr, operation_example) |
|
536
|
|
|
# Canonicalize |
|
537
|
2 |
|
expr = expr.simplify() |
|
538
|
2 |
|
return expr |
|
539
|
|
|
|
|
540
|
2 |
|
def cnf(self, expr): |
|
541
|
|
|
""" |
|
542
|
|
|
Return a conjunctive normal form of the `expr` expression. |
|
543
|
|
|
""" |
|
544
|
2 |
|
return self.normalize(expr, self.AND) |
|
545
|
|
|
|
|
546
|
2 |
|
def dnf(self, expr): |
|
547
|
|
|
""" |
|
548
|
|
|
Return a disjunctive normal form of the `expr` expression. |
|
549
|
|
|
""" |
|
550
|
2 |
|
return self.normalize(expr, self.OR) |
|
551
|
|
|
|
|
552
|
|
|
|
|
553
|
2 |
|
class Expression(object): |
|
554
|
|
|
""" |
|
555
|
|
|
Abstract base class for all boolean expressions, including functions and |
|
556
|
|
|
variable symbols. |
|
557
|
|
|
""" |
|
558
|
|
|
# Defines sort and comparison order between expressions arguments |
|
559
|
2 |
|
sort_order = None |
|
560
|
|
|
|
|
561
|
|
|
# Store arguments aka. subterms of this expressions. |
|
562
|
|
|
# subterms are either literals or expressions. |
|
563
|
2 |
|
args = tuple() |
|
564
|
|
|
|
|
565
|
|
|
# True is this is a literal expression such as a Symbol, TRUE or FALSE |
|
566
|
2 |
|
isliteral = False |
|
567
|
|
|
|
|
568
|
|
|
# True if this expression has been simplified to in canonical form. |
|
569
|
2 |
|
iscanonical = False |
|
570
|
|
|
|
|
571
|
|
|
# these class attributes are configured when a new BooleanAlgebra is created |
|
572
|
2 |
|
TRUE = None |
|
573
|
2 |
|
FALSE = None |
|
574
|
2 |
|
NOT = None |
|
575
|
2 |
|
AND = None |
|
576
|
2 |
|
OR = None |
|
577
|
2 |
|
Symbol = None |
|
578
|
|
|
|
|
579
|
2 |
|
@property |
|
580
|
|
|
def objects(self): |
|
581
|
|
|
""" |
|
582
|
|
|
Return a set of all associated objects with this expression symbols. |
|
583
|
|
|
Include recursively subexpressions objects. |
|
584
|
|
|
""" |
|
585
|
|
|
return set(s.obj for s in self.symbols) |
|
586
|
|
|
|
|
587
|
2 |
|
def get_literals(self): |
|
588
|
|
|
""" |
|
589
|
|
|
Return a list of all the literals contained in this expression. |
|
590
|
|
|
Include recursively subexpressions symbols. |
|
591
|
|
|
This includes duplicates. |
|
592
|
|
|
""" |
|
593
|
|
|
if self.isliteral: |
|
594
|
|
|
return [self] |
|
595
|
|
|
if not self.args: |
|
596
|
|
|
return [] |
|
597
|
|
|
return list(itertools.chain.from_iterable(arg.get_literals() for arg in self.args)) |
|
598
|
|
|
|
|
599
|
2 |
|
@property |
|
600
|
|
|
def literals(self): |
|
601
|
|
|
""" |
|
602
|
|
|
Return a set of all literals contained in this expression. |
|
603
|
|
|
Include recursively subexpressions literals. |
|
604
|
|
|
""" |
|
605
|
|
|
return set(self.get_literals()) |
|
606
|
|
|
|
|
607
|
2 |
|
def literalize(self): |
|
608
|
|
|
""" |
|
609
|
|
|
Return an expression where NOTs are only occurring as literals. |
|
610
|
|
|
Applied recursively to subexpressions. |
|
611
|
|
|
""" |
|
612
|
2 |
|
if self.isliteral: |
|
613
|
2 |
|
return self |
|
614
|
2 |
|
args = tuple(arg.literalize() for arg in self.args) |
|
615
|
2 |
|
if all(arg is self.args[i] for i, arg in enumerate(args)): |
|
616
|
2 |
|
return self |
|
617
|
|
|
|
|
618
|
|
|
return self.__class__(*args) |
|
619
|
|
|
|
|
620
|
2 |
|
def get_symbols(self): |
|
621
|
|
|
""" |
|
622
|
|
|
Return a list of all the symbols contained in this expression. |
|
623
|
|
|
Include recursively subexpressions symbols. |
|
624
|
|
|
This includes duplicates. |
|
625
|
|
|
""" |
|
626
|
|
|
return [s if isinstance(s, Symbol) else s.args[0] for s in self.get_literals()] |
|
627
|
|
|
|
|
628
|
2 |
|
@property |
|
629
|
|
|
def symbols(self,): |
|
630
|
|
|
""" |
|
631
|
|
|
Return a list of all the symbols contained in this expression. |
|
632
|
|
|
Include recursively subexpressions symbols. |
|
633
|
|
|
This includes duplicates. |
|
634
|
|
|
""" |
|
635
|
|
|
return set(self.get_symbols()) |
|
636
|
|
|
|
|
637
|
2 |
|
def subs(self, substitutions, default=None, simplify=False): |
|
638
|
|
|
""" |
|
639
|
|
|
Return an expression where the expression or all subterms equal to a key |
|
640
|
|
|
expression are substituted with the corresponding value expression using |
|
641
|
|
|
a mapping of: {expr->expr to substitute.} |
|
642
|
|
|
|
|
643
|
|
|
Return this expression unmodified if nothing could be substituted. |
|
644
|
|
|
|
|
645
|
|
|
Note that this can be used to tested for expression containment. |
|
646
|
|
|
""" |
|
647
|
|
|
# shortcut: check if we have our whole expression as a possible |
|
648
|
|
|
# subsitution source |
|
649
|
|
|
for expr, substitution in substitutions.items(): |
|
650
|
|
|
if expr == self: |
|
651
|
|
|
return substitution |
|
652
|
|
|
|
|
653
|
|
|
# otherwise, do a proper substitution of sub expressions |
|
654
|
|
|
expr = self._subs(substitutions, default, simplify) |
|
655
|
|
|
return self if expr is None else expr |
|
656
|
|
|
|
|
657
|
2 |
|
def _subs(self, substitutions, default, simplify): |
|
658
|
|
|
""" |
|
659
|
|
|
Return an expression where all subterms equal to a key expression are |
|
660
|
|
|
substituted by the corresponding value expression using a mapping of: |
|
661
|
|
|
{expr->expr to substitute.} |
|
662
|
|
|
""" |
|
663
|
|
|
# track the new list of unchanged args or replaced args through |
|
664
|
|
|
# a substitution |
|
665
|
|
|
new_arguments = [] |
|
666
|
|
|
changed_something = False |
|
667
|
|
|
|
|
668
|
|
|
# shortcut for basic logic True or False |
|
669
|
|
|
if self is self.TRUE or self is self.FALSE: |
|
670
|
|
|
return self |
|
671
|
|
|
|
|
672
|
|
|
# if the expression has no elements, e.g. is empty, do not apply |
|
673
|
|
|
# substitions |
|
674
|
|
|
if not self.args: |
|
675
|
|
|
return default |
|
676
|
|
|
|
|
677
|
|
|
# iterate the subexpressions: either plain symbols or a subexpressions |
|
678
|
|
|
for arg in self.args: |
|
679
|
|
|
# collect substitutions for exact matches |
|
680
|
|
|
# break as soon as we have a match |
|
681
|
|
|
for expr, substitution in substitutions.items(): |
|
682
|
|
|
if arg == expr: |
|
683
|
|
|
new_arguments.append(substitution) |
|
684
|
|
|
changed_something = True |
|
685
|
|
|
break |
|
686
|
|
|
|
|
687
|
|
|
# this will execute only if we did not break out of the |
|
688
|
|
|
# loop, e.g. if we did not change anything and did not |
|
689
|
|
|
# collect any substitutions |
|
690
|
|
|
else: |
|
691
|
|
|
# recursively call _subs on each arg to see if we get a |
|
692
|
|
|
# substituted arg |
|
693
|
|
|
new_arg = arg._subs(substitutions, default, simplify) |
|
694
|
|
|
if new_arg is None: |
|
695
|
|
|
# if we did not collect a substitution for this arg, |
|
696
|
|
|
# keep the arg as-is, it is not replaced by anything |
|
697
|
|
|
new_arguments.append(arg) |
|
698
|
|
|
else: |
|
699
|
|
|
# otherwise, we add the substitution for this arg instead |
|
700
|
|
|
new_arguments.append(new_arg) |
|
701
|
|
|
changed_something = True |
|
702
|
|
|
|
|
703
|
|
|
if not changed_something: |
|
704
|
|
|
return |
|
705
|
|
|
|
|
706
|
|
|
# here we did some substitution: we return a new expression |
|
707
|
|
|
# built from the new_arguments |
|
708
|
|
|
newexpr = self.__class__(*new_arguments) |
|
709
|
|
|
return newexpr.simplify() if simplify else newexpr |
|
710
|
|
|
|
|
711
|
2 |
|
def simplify(self): |
|
712
|
|
|
""" |
|
713
|
|
|
Return a new simplified expression in canonical form built from this |
|
714
|
|
|
expression. The simplified expression may be exactly the same as this |
|
715
|
|
|
expression. |
|
716
|
|
|
|
|
717
|
|
|
Subclasses override this method to compute actual simplification. |
|
718
|
|
|
""" |
|
719
|
2 |
|
return self |
|
720
|
|
|
|
|
721
|
2 |
|
def __hash__(self): |
|
722
|
|
|
""" |
|
723
|
|
|
Expressions are immutable and hashable. The hash of Functions is |
|
724
|
|
|
computed by respecting the structure of the whole expression by mixing |
|
725
|
|
|
the class name hash and the recursive hash of a frozenset of arguments. |
|
726
|
|
|
Hash of elements is based on their boolean equivalent. Hash of symbols |
|
727
|
|
|
is based on their object. |
|
728
|
|
|
""" |
|
729
|
2 |
|
if not self.args: |
|
730
|
|
|
arghash = id(self) |
|
731
|
|
|
else: |
|
732
|
2 |
|
arghash = hash(frozenset(map(hash, self.args))) |
|
733
|
2 |
|
return hash(self.__class__.__name__) ^ arghash |
|
734
|
|
|
|
|
735
|
2 |
|
def __eq__(self, other): |
|
736
|
|
|
""" |
|
737
|
|
|
Test if other element is structurally the same as itself. |
|
738
|
|
|
|
|
739
|
|
|
This method does not make any simplification or transformation, so it |
|
740
|
|
|
will return False although the expression terms may be mathematically |
|
741
|
|
|
equal. Use simplify() before testing equality. |
|
742
|
|
|
|
|
743
|
|
|
For literals, plain equality is used. |
|
744
|
|
|
For functions, it uses the facts that operations are: |
|
745
|
|
|
- commutative and considers different ordering as equal. |
|
746
|
|
|
- idempotent, so args can appear more often in one term than in the other. |
|
747
|
|
|
""" |
|
748
|
2 |
|
if self is other: |
|
749
|
|
|
return True |
|
750
|
|
|
|
|
751
|
2 |
|
if isinstance(other, self.__class__): |
|
752
|
2 |
|
return frozenset(self.args) == frozenset(other.args) |
|
753
|
|
|
|
|
754
|
2 |
|
return NotImplemented |
|
755
|
|
|
|
|
756
|
2 |
|
def __ne__(self, other): |
|
757
|
|
|
return not self == other |
|
758
|
|
|
|
|
759
|
2 |
|
def __lt__(self, other): |
|
760
|
2 |
|
if self.sort_order is not None and other.sort_order is not None: |
|
761
|
2 |
|
if self.sort_order == other.sort_order: |
|
762
|
2 |
|
return NotImplemented |
|
763
|
2 |
|
return self.sort_order < other.sort_order |
|
764
|
2 |
|
return NotImplemented |
|
765
|
|
|
|
|
766
|
2 |
|
def __gt__(self, other): |
|
767
|
2 |
|
lt = other.__lt__(self) |
|
768
|
2 |
|
if lt is NotImplemented: |
|
769
|
2 |
|
return not self.__lt__(other) |
|
770
|
|
|
return lt |
|
771
|
|
|
|
|
772
|
2 |
|
def __and__(self, other): |
|
773
|
|
|
return self.AND(self, other) |
|
774
|
|
|
|
|
775
|
2 |
|
__mul__ = __and__ |
|
776
|
|
|
|
|
777
|
2 |
|
def __invert__(self): |
|
778
|
|
|
return self.NOT(self) |
|
779
|
|
|
|
|
780
|
2 |
|
def __or__(self, other): |
|
781
|
|
|
return self.OR(self, other) |
|
782
|
|
|
|
|
783
|
2 |
|
__add__ = __or__ |
|
784
|
|
|
|
|
785
|
2 |
|
def __bool__(self): |
|
786
|
|
|
raise TypeError('Cannot evaluate expression as a Python Boolean.') |
|
787
|
|
|
|
|
788
|
2 |
|
__nonzero__ = __bool__ |
|
789
|
|
|
|
|
790
|
|
|
|
|
791
|
2 |
|
class BaseElement(Expression): |
|
792
|
|
|
""" |
|
793
|
|
|
Abstract base class for the base elements TRUE and FALSE of the boolean |
|
794
|
|
|
algebra. |
|
795
|
|
|
""" |
|
796
|
2 |
|
sort_order = 0 |
|
797
|
|
|
|
|
798
|
2 |
|
def __init__(self): |
|
799
|
2 |
|
super(BaseElement, self).__init__() |
|
800
|
2 |
|
self.iscanonical = True |
|
801
|
|
|
|
|
802
|
|
|
# The dual Base Element class for this element: TRUE.dual returns |
|
803
|
|
|
# _FALSE() and FALSE.dual returns _TRUE(). This is a cyclic reference |
|
804
|
|
|
# and therefore only assigned after creation of the singletons, |
|
805
|
2 |
|
self.dual = None |
|
806
|
|
|
|
|
807
|
2 |
|
def __lt__(self, other): |
|
808
|
|
|
if isinstance(other, BaseElement): |
|
809
|
|
|
return self == self.FALSE |
|
810
|
|
|
return NotImplemented |
|
811
|
|
|
|
|
812
|
2 |
|
__nonzero__ = __bool__ = lambda s: None |
|
813
|
|
|
|
|
814
|
2 |
|
def pretty(self, indent=0, debug=False): |
|
815
|
|
|
""" |
|
816
|
|
|
Return a pretty formatted representation of self. |
|
817
|
|
|
""" |
|
818
|
|
|
return (' ' * indent) + repr(self) |
|
819
|
|
|
|
|
820
|
|
|
|
|
821
|
2 |
View Code Duplication |
class _TRUE(BaseElement): |
|
|
|
|
|
|
822
|
|
|
""" |
|
823
|
|
|
Boolean base element TRUE. |
|
824
|
|
|
Not meant to be subclassed nor instantiated directly. |
|
825
|
|
|
""" |
|
826
|
|
|
|
|
827
|
2 |
|
def __init__(self): |
|
828
|
2 |
|
super(_TRUE, self).__init__() |
|
829
|
|
|
# assigned at singleton creation: self.dual = FALSE |
|
830
|
|
|
|
|
831
|
2 |
|
def __hash__(self): |
|
832
|
|
|
return hash(True) |
|
833
|
|
|
|
|
834
|
2 |
|
def __eq__(self, other): |
|
835
|
2 |
|
return self is other or other is True or isinstance(other, _TRUE) |
|
836
|
|
|
|
|
837
|
2 |
|
def __str__(self): |
|
838
|
|
|
return '1' |
|
839
|
|
|
|
|
840
|
2 |
|
def __repr__(self): |
|
841
|
|
|
return 'TRUE' |
|
842
|
|
|
|
|
843
|
2 |
|
def __call__(self): |
|
844
|
|
|
return self |
|
845
|
|
|
|
|
846
|
2 |
|
__nonzero__ = __bool__ = lambda s: True |
|
847
|
|
|
|
|
848
|
|
|
|
|
849
|
2 |
View Code Duplication |
class _FALSE(BaseElement): |
|
|
|
|
|
|
850
|
|
|
""" |
|
851
|
|
|
Boolean base element FALSE. |
|
852
|
|
|
Not meant to be subclassed nor instantiated directly. |
|
853
|
|
|
""" |
|
854
|
|
|
|
|
855
|
2 |
|
def __init__(self): |
|
856
|
2 |
|
super(_FALSE, self).__init__() |
|
857
|
|
|
# assigned at singleton creation: self.dual = TRUE |
|
858
|
|
|
|
|
859
|
2 |
|
def __hash__(self): |
|
860
|
|
|
return hash(False) |
|
861
|
|
|
|
|
862
|
2 |
|
def __eq__(self, other): |
|
863
|
2 |
|
return self is other or other is False or isinstance(other, _FALSE) |
|
864
|
|
|
|
|
865
|
2 |
|
def __str__(self): |
|
866
|
|
|
return '0' |
|
867
|
|
|
|
|
868
|
2 |
|
def __repr__(self): |
|
869
|
|
|
return 'FALSE' |
|
870
|
|
|
|
|
871
|
2 |
|
def __call__(self): |
|
872
|
|
|
return self |
|
873
|
|
|
|
|
874
|
2 |
|
__nonzero__ = __bool__ = lambda s: False |
|
875
|
|
|
|
|
876
|
|
|
|
|
877
|
2 |
|
class Symbol(Expression): |
|
878
|
|
|
""" |
|
879
|
|
|
Boolean variable. |
|
880
|
|
|
|
|
881
|
|
|
A Symbol can hold an object used to determine equality between symbols. |
|
882
|
|
|
""" |
|
883
|
|
|
|
|
884
|
2 |
|
sort_order = 5 |
|
885
|
|
|
|
|
886
|
2 |
|
def __init__(self, obj): |
|
887
|
2 |
|
super(Symbol, self).__init__() |
|
888
|
|
|
# Store an associated object. This object determines equality |
|
889
|
2 |
|
self.obj = obj |
|
890
|
2 |
|
self.iscanonical = True |
|
891
|
2 |
|
self.isliteral = True |
|
892
|
|
|
|
|
893
|
2 |
|
def __call__(self, **kwargs): |
|
894
|
|
|
""" |
|
895
|
|
|
Return the evaluated value for this symbol from kwargs |
|
896
|
|
|
""" |
|
897
|
|
|
return kwargs[self.obj] |
|
898
|
|
|
|
|
899
|
2 |
|
def __hash__(self): |
|
900
|
2 |
|
if self.obj is None: # Anonymous Symbol. |
|
901
|
|
|
return id(self) |
|
902
|
2 |
|
return hash(self.obj) |
|
903
|
|
|
|
|
904
|
2 |
|
def __eq__(self, other): |
|
905
|
2 |
|
if self is other: |
|
906
|
2 |
|
return True |
|
907
|
2 |
|
if isinstance(other, self.__class__): |
|
908
|
2 |
|
return self.obj == other.obj |
|
909
|
2 |
|
return NotImplemented |
|
910
|
|
|
|
|
911
|
2 |
|
def __lt__(self, other): |
|
912
|
|
|
comparator = Expression.__lt__(self, other) |
|
913
|
|
|
if comparator is not NotImplemented: |
|
914
|
|
|
return comparator |
|
915
|
|
|
if isinstance(other, Symbol): |
|
916
|
|
|
return self.obj < other.obj |
|
917
|
|
|
return NotImplemented |
|
918
|
|
|
|
|
919
|
2 |
|
def __str__(self): |
|
920
|
2 |
|
return str(self.obj) |
|
921
|
|
|
|
|
922
|
2 |
|
def __repr__(self): |
|
923
|
|
|
obj = "'%s'" % self.obj if isinstance(self.obj, basestring) else repr(self.obj) |
|
924
|
|
|
return '%s(%s)' % (self.__class__.__name__, obj) |
|
925
|
|
|
|
|
926
|
2 |
|
def pretty(self, indent=0, debug=False): |
|
927
|
|
|
""" |
|
928
|
|
|
Return a pretty formatted representation of self. |
|
929
|
|
|
""" |
|
930
|
|
|
debug_details = '' |
|
931
|
|
|
if debug: |
|
932
|
|
|
debug_details += '<isliteral=%r, iscanonical=%r>' % (self.isliteral, self.iscanonical) |
|
933
|
|
|
|
|
934
|
|
|
obj = "'%s'" % self.obj if isinstance(self.obj, basestring) else repr(self.obj) |
|
935
|
|
|
return (' ' * indent) + ('%s(%s%s)' % (self.__class__.__name__, debug_details, obj)) |
|
936
|
|
|
|
|
937
|
|
|
|
|
938
|
2 |
|
class Function(Expression): |
|
939
|
|
|
""" |
|
940
|
|
|
Boolean function. |
|
941
|
|
|
|
|
942
|
|
|
A boolean function takes n (one or more) boolean expressions as arguments |
|
943
|
|
|
where n is called the order of the function and maps them to one of the base |
|
944
|
|
|
elements TRUE or FALSE. Implemented functions are AND, OR and NOT. |
|
945
|
|
|
""" |
|
946
|
|
|
|
|
947
|
2 |
|
def __init__(self, *args): |
|
948
|
2 |
|
super(Function, self).__init__() |
|
949
|
|
|
|
|
950
|
|
|
# Specifies an infix notation of an operator for printing such as | or &. |
|
951
|
2 |
|
self.operator = None |
|
952
|
|
|
|
|
953
|
2 |
|
assert all(isinstance(arg, Expression) for arg in args), \ |
|
954
|
|
|
'Bad arguments: all arguments must be an Expression: %r' % (args,) |
|
955
|
2 |
|
self.args = tuple(args) |
|
956
|
|
|
|
|
957
|
2 |
|
def __str__(self): |
|
958
|
2 |
|
args = self.args |
|
959
|
2 |
|
if len(args) == 1: |
|
960
|
2 |
|
if self.isliteral: |
|
961
|
2 |
|
return '%s%s' % (self.operator, args[0]) |
|
962
|
|
|
return '%s(%s)' % (self.operator, args[0]) |
|
963
|
|
|
|
|
964
|
2 |
|
args_str = [] |
|
965
|
2 |
|
for arg in args: |
|
966
|
2 |
|
if arg.isliteral: |
|
967
|
2 |
|
args_str.append(str(arg)) |
|
968
|
|
|
else: |
|
969
|
2 |
|
args_str.append('(%s)' % arg) |
|
970
|
|
|
|
|
971
|
2 |
|
return self.operator.join(args_str) |
|
972
|
|
|
|
|
973
|
2 |
|
def __repr__(self): |
|
974
|
|
|
return '%s(%s)' % (self.__class__.__name__, ', '.join(map(repr, self.args))) |
|
975
|
|
|
|
|
976
|
2 |
|
def pretty(self, indent=0, debug=False): |
|
977
|
|
|
""" |
|
978
|
|
|
Return a pretty formatted representation of self as an indented tree. |
|
979
|
|
|
|
|
980
|
|
|
If debug is True, also prints debug information for each expression arg. |
|
981
|
|
|
|
|
982
|
|
|
For example:: |
|
983
|
|
|
|
|
984
|
|
|
>>> print(BooleanAlgebra().parse( |
|
985
|
|
|
... u'not a and not b and not (a and ba and c) and c or c').pretty()) |
|
986
|
|
|
OR( |
|
987
|
|
|
AND( |
|
988
|
|
|
NOT(Symbol('a')), |
|
989
|
|
|
NOT(Symbol('b')), |
|
990
|
|
|
NOT( |
|
991
|
|
|
AND( |
|
992
|
|
|
Symbol('a'), |
|
993
|
|
|
Symbol('ba'), |
|
994
|
|
|
Symbol('c') |
|
995
|
|
|
) |
|
996
|
|
|
), |
|
997
|
|
|
Symbol('c') |
|
998
|
|
|
), |
|
999
|
|
|
Symbol('c') |
|
1000
|
|
|
) |
|
1001
|
|
|
""" |
|
1002
|
|
|
debug_details = '' |
|
1003
|
|
|
if debug: |
|
1004
|
|
|
debug_details += '<isliteral=%r, iscanonical=%r' % (self.isliteral, self.iscanonical) |
|
1005
|
|
|
identity = getattr(self, 'identity', None) |
|
1006
|
|
|
if identity is not None: |
|
1007
|
|
|
debug_details += ', identity=%r' % (identity) |
|
1008
|
|
|
|
|
1009
|
|
|
annihilator = getattr(self, 'annihilator', None) |
|
1010
|
|
|
if annihilator is not None: |
|
1011
|
|
|
debug_details += ', annihilator=%r' % (annihilator) |
|
1012
|
|
|
|
|
1013
|
|
|
dual = getattr(self, 'dual', None) |
|
1014
|
|
|
if dual is not None: |
|
1015
|
|
|
debug_details += ', dual=%r' % (dual) |
|
1016
|
|
|
debug_details += '>' |
|
1017
|
|
|
cls = self.__class__.__name__ |
|
1018
|
|
|
args = [a.pretty(indent=indent + 2, debug=debug) for a in self.args] |
|
1019
|
|
|
pfargs = ',\n'.join(args) |
|
1020
|
|
|
cur_indent = ' ' * indent |
|
1021
|
|
|
new_line = '' if self.isliteral else '\n' |
|
1022
|
|
|
return '{cur_indent}{cls}({debug_details}{new_line}{pfargs}\n{cur_indent})'.format(**locals()) |
|
1023
|
|
|
|
|
1024
|
|
|
|
|
1025
|
2 |
|
class NOT(Function): |
|
1026
|
|
|
""" |
|
1027
|
|
|
Boolean NOT operation. |
|
1028
|
|
|
|
|
1029
|
|
|
The NOT operation takes exactly one argument. If this argument is a Symbol |
|
1030
|
|
|
the resulting expression is also called a literal. |
|
1031
|
|
|
|
|
1032
|
|
|
The operator "~" can be used as abbreviation for NOT, e.g. instead of NOT(x) |
|
1033
|
|
|
one can write ~x (where x is some boolean expression). Also for printing "~" |
|
1034
|
|
|
is used for better readability. |
|
1035
|
|
|
|
|
1036
|
|
|
You can subclass to define alternative string representation. |
|
1037
|
|
|
|
|
1038
|
|
|
For example:: |
|
1039
|
|
|
|
|
1040
|
|
|
>>> class NOT2(NOT): |
|
1041
|
|
|
... def __init__(self, *args): |
|
1042
|
|
|
... super(NOT2, self).__init__(*args) |
|
1043
|
|
|
... self.operator = '!' |
|
1044
|
|
|
""" |
|
1045
|
|
|
|
|
1046
|
2 |
|
def __init__(self, arg1): |
|
1047
|
2 |
|
super(NOT, self).__init__(arg1) |
|
1048
|
2 |
|
self.isliteral = isinstance(self.args[0], Symbol) |
|
1049
|
2 |
|
self.operator = '~' |
|
1050
|
|
|
|
|
1051
|
2 |
|
def literalize(self): |
|
1052
|
|
|
""" |
|
1053
|
|
|
Return an expression where NOTs are only occurring as literals. |
|
1054
|
|
|
""" |
|
1055
|
2 |
|
expr = self.demorgan() |
|
1056
|
2 |
|
if isinstance(expr, self.__class__): |
|
1057
|
2 |
|
return expr |
|
1058
|
|
|
return expr.literalize() |
|
1059
|
|
|
|
|
1060
|
2 |
|
def simplify(self): |
|
1061
|
|
|
""" |
|
1062
|
|
|
Return a simplified expr in canonical form. |
|
1063
|
|
|
|
|
1064
|
|
|
This means double negations are canceled out and all contained boolean |
|
1065
|
|
|
objects are in their canonical form. |
|
1066
|
|
|
""" |
|
1067
|
2 |
|
if self.iscanonical: |
|
1068
|
2 |
|
return self |
|
1069
|
|
|
|
|
1070
|
2 |
|
expr = self.cancel() |
|
1071
|
2 |
|
if not isinstance(expr, self.__class__): |
|
1072
|
2 |
|
return expr.simplify() |
|
1073
|
|
|
|
|
1074
|
2 |
|
if expr.args[0] in (self.TRUE, self.FALSE,): |
|
1075
|
|
|
return expr.args[0].dual |
|
1076
|
|
|
|
|
1077
|
2 |
|
expr = self.__class__(expr.args[0].simplify()) |
|
1078
|
2 |
|
expr.iscanonical = True |
|
1079
|
2 |
|
return expr |
|
1080
|
|
|
|
|
1081
|
2 |
|
def cancel(self): |
|
1082
|
|
|
""" |
|
1083
|
|
|
Cancel itself and following NOTs as far as possible. |
|
1084
|
|
|
Returns the simplified expression. |
|
1085
|
|
|
""" |
|
1086
|
2 |
|
expr = self |
|
1087
|
2 |
|
while True: |
|
1088
|
2 |
|
arg = expr.args[0] |
|
1089
|
2 |
|
if not isinstance(arg, self.__class__): |
|
1090
|
2 |
|
return expr |
|
1091
|
2 |
|
expr = arg.args[0] |
|
1092
|
2 |
|
if not isinstance(expr, self.__class__): |
|
1093
|
2 |
|
return expr |
|
1094
|
|
|
|
|
1095
|
2 |
|
def demorgan(self): |
|
1096
|
|
|
""" |
|
1097
|
|
|
Return a expr where the NOT function is moved inward. |
|
1098
|
|
|
This is achieved by canceling double NOTs and using De Morgan laws. |
|
1099
|
|
|
""" |
|
1100
|
2 |
|
expr = self.cancel() |
|
1101
|
2 |
|
if expr.isliteral or not isinstance(expr, self.NOT): |
|
1102
|
2 |
|
return expr |
|
1103
|
|
|
op = expr.args[0] |
|
1104
|
|
|
return op.dual(*(self.__class__(arg).cancel() for arg in op.args)) |
|
|
|
|
|
|
1105
|
|
|
|
|
1106
|
2 |
|
def __call__(self, **kwargs): |
|
1107
|
|
|
""" |
|
1108
|
|
|
Return the evaluated (negated) value for this function. |
|
1109
|
|
|
""" |
|
1110
|
2 |
|
return not self.args[0](**kwargs) |
|
1111
|
|
|
|
|
1112
|
2 |
|
def __lt__(self, other): |
|
1113
|
2 |
|
return self.args[0] < other |
|
1114
|
|
|
|
|
1115
|
2 |
|
def pretty(self, indent=1, debug=False): |
|
1116
|
|
|
""" |
|
1117
|
|
|
Return a pretty formatted representation of self. |
|
1118
|
|
|
Include additional debug details if `debug` is True. |
|
1119
|
|
|
""" |
|
1120
|
|
|
debug_details = '' |
|
1121
|
|
|
if debug: |
|
1122
|
|
|
debug_details += '<isliteral=%r, iscanonical=%r>' % (self.isliteral, self.iscanonical) |
|
1123
|
|
|
if self.isliteral: |
|
1124
|
|
|
pretty_literal = self.args[0].pretty(indent=0, debug=debug) |
|
1125
|
|
|
return (' ' * indent) + '%s(%s%s)' % (self.__class__.__name__, debug_details, pretty_literal) |
|
1126
|
|
|
else: |
|
1127
|
|
|
return super(NOT, self).pretty(indent=indent, debug=debug) |
|
1128
|
|
|
|
|
1129
|
|
|
|
|
1130
|
2 |
|
class DualBase(Function): |
|
1131
|
|
|
""" |
|
1132
|
|
|
Base class for AND and OR function. |
|
1133
|
|
|
|
|
1134
|
|
|
This class uses the duality principle to combine similar methods of AND |
|
1135
|
|
|
and OR. Both operations take 2 or more arguments and can be created using |
|
1136
|
|
|
"|" for OR and "&" for AND. |
|
1137
|
|
|
""" |
|
1138
|
|
|
|
|
1139
|
2 |
|
_pyoperator = None |
|
1140
|
|
|
|
|
1141
|
2 |
|
def __init__(self, arg1, arg2, *args): |
|
1142
|
2 |
|
super(DualBase, self).__init__(arg1, arg2, *args) |
|
1143
|
|
|
|
|
1144
|
|
|
# identity element for the specific operation. |
|
1145
|
|
|
# This will be TRUE for the AND operation and FALSE for the OR operation. |
|
1146
|
2 |
|
self.identity = None |
|
1147
|
|
|
|
|
1148
|
|
|
# annihilator element for this function. |
|
1149
|
|
|
# This will be FALSE for the AND operation and TRUE for the OR operation. |
|
1150
|
2 |
|
self.annihilator = None |
|
1151
|
|
|
|
|
1152
|
|
|
# dual class of this function. |
|
1153
|
|
|
# This means OR.dual returns AND and AND.dual returns OR. |
|
1154
|
2 |
|
self.dual = None |
|
1155
|
|
|
|
|
1156
|
2 |
|
def __contains__(self, expr): |
|
1157
|
|
|
""" |
|
1158
|
|
|
Test if expr is a subterm of this expression. |
|
1159
|
|
|
""" |
|
1160
|
2 |
|
if expr in self.args: |
|
1161
|
|
|
return True |
|
1162
|
|
|
|
|
1163
|
2 |
|
if isinstance(expr, self.__class__): |
|
1164
|
2 |
|
return all(arg in self.args for arg in expr.args) |
|
1165
|
|
|
|
|
1166
|
2 |
|
def simplify(self, sort=True): |
|
1167
|
|
|
""" |
|
1168
|
|
|
Return a new simplified expression in canonical form from this |
|
1169
|
|
|
expression. |
|
1170
|
|
|
|
|
1171
|
|
|
For simplification of AND and OR fthe ollowing rules are used |
|
1172
|
|
|
recursively bottom up: |
|
1173
|
|
|
- Associativity (output does not contain same operations nested) |
|
1174
|
|
|
- Annihilation |
|
1175
|
|
|
- Idempotence |
|
1176
|
|
|
- Identity |
|
1177
|
|
|
- Complementation |
|
1178
|
|
|
- Elimination |
|
1179
|
|
|
- Absorption |
|
1180
|
|
|
- Commutativity (output is always sorted) |
|
1181
|
|
|
|
|
1182
|
|
|
Other boolean objects are also in their canonical form. |
|
1183
|
|
|
""" |
|
1184
|
|
|
# TODO: Refactor DualBase.simplify into different "sub-evals". |
|
1185
|
|
|
|
|
1186
|
|
|
# If self is already canonical do nothing. |
|
1187
|
2 |
|
if self.iscanonical: |
|
1188
|
2 |
|
return self |
|
1189
|
|
|
|
|
1190
|
|
|
# Otherwise bring arguments into canonical form. |
|
1191
|
2 |
|
args = [arg.simplify() for arg in self.args] |
|
1192
|
|
|
|
|
1193
|
|
|
# Create new instance of own class with canonical args. |
|
1194
|
|
|
# TODO: Only create new class if some args changed. |
|
1195
|
2 |
|
expr = self.__class__(*args) |
|
1196
|
|
|
|
|
1197
|
|
|
# Literalize before doing anything, this also applies De Morgan's Law |
|
1198
|
2 |
|
expr = expr.literalize() |
|
1199
|
|
|
|
|
1200
|
|
|
# Associativity: |
|
1201
|
|
|
# (A & B) & C = A & (B & C) = A & B & C |
|
1202
|
|
|
# (A | B) | C = A | (B | C) = A | B | C |
|
1203
|
2 |
|
expr = expr.flatten() |
|
1204
|
|
|
|
|
1205
|
|
|
# Annihilation: A & 0 = 0, A | 1 = 1 |
|
1206
|
2 |
|
if self.annihilator in expr.args: |
|
1207
|
|
|
return self.annihilator |
|
1208
|
|
|
|
|
1209
|
|
|
# Idempotence: A & A = A, A | A = A |
|
1210
|
|
|
# this boils down to removing duplicates |
|
1211
|
2 |
|
args = [] |
|
1212
|
2 |
|
for arg in expr.args: |
|
1213
|
2 |
|
if arg not in args: |
|
1214
|
2 |
|
args.append(arg) |
|
1215
|
2 |
|
if len(args) == 1: |
|
1216
|
|
|
return args[0] |
|
1217
|
|
|
|
|
1218
|
|
|
# Identity: A & 1 = A, A | 0 = A |
|
1219
|
2 |
|
if self.identity in args: |
|
1220
|
|
|
args.remove(self.identity) |
|
1221
|
|
|
if len(args) == 1: |
|
1222
|
|
|
return args[0] |
|
1223
|
|
|
|
|
1224
|
|
|
# Complementation: A & ~A = 0, A | ~A = 1 |
|
1225
|
2 |
|
for arg in args: |
|
1226
|
2 |
|
if self.NOT(arg) in args: |
|
1227
|
|
|
return self.annihilator |
|
1228
|
|
|
|
|
1229
|
|
|
# Elimination: (A & B) | (A & ~B) = A, (A | B) & (A | ~B) = A |
|
1230
|
2 |
|
i = 0 |
|
1231
|
2 |
|
while i < len(args) - 1: |
|
1232
|
2 |
|
j = i + 1 |
|
1233
|
2 |
|
ai = args[i] |
|
1234
|
2 |
|
if not isinstance(ai, self.dual): |
|
1235
|
2 |
|
i += 1 |
|
1236
|
2 |
|
continue |
|
1237
|
2 |
|
while j < len(args): |
|
1238
|
2 |
|
aj = args[j] |
|
1239
|
2 |
|
if not isinstance(aj, self.dual) or len(ai.args) != len(aj.args): |
|
1240
|
2 |
|
j += 1 |
|
1241
|
2 |
|
continue |
|
1242
|
|
|
|
|
1243
|
|
|
# Find terms where only one arg is different. |
|
1244
|
2 |
|
negated = None |
|
1245
|
2 |
|
for arg in ai.args: |
|
1246
|
|
|
# FIXME: what does this pass Do? |
|
1247
|
2 |
|
if arg in aj.args: |
|
1248
|
2 |
|
pass |
|
1249
|
2 |
|
elif self.NOT(arg).cancel() in aj.args: |
|
1250
|
|
|
if negated is None: |
|
1251
|
|
|
negated = arg |
|
1252
|
|
|
else: |
|
1253
|
|
|
negated = None |
|
1254
|
|
|
break |
|
1255
|
|
|
else: |
|
1256
|
2 |
|
negated = None |
|
1257
|
2 |
|
break |
|
1258
|
|
|
|
|
1259
|
|
|
# If the different arg is a negation simplify the expr. |
|
1260
|
2 |
|
if negated is not None: |
|
1261
|
|
|
# Cancel out one of the two terms. |
|
1262
|
|
|
del args[j] |
|
1263
|
|
|
aiargs = list(ai.args) |
|
1264
|
|
|
aiargs.remove(negated) |
|
1265
|
|
|
if len(aiargs) == 1: |
|
1266
|
|
|
args[i] = aiargs[0] |
|
1267
|
|
|
else: |
|
1268
|
|
|
args[i] = self.dual(*aiargs) |
|
1269
|
|
|
|
|
1270
|
|
|
if len(args) == 1: |
|
1271
|
|
|
return args[0] |
|
1272
|
|
|
else: |
|
1273
|
|
|
# Now the other simplifications have to be redone. |
|
1274
|
|
|
return self.__class__(*args).simplify() |
|
1275
|
2 |
|
j += 1 |
|
1276
|
2 |
|
i += 1 |
|
1277
|
|
|
|
|
1278
|
|
|
# Absorption: A & (A | B) = A, A | (A & B) = A |
|
1279
|
|
|
# Negative absorption: A & (~A | B) = A & B, A | (~A & B) = A | B |
|
1280
|
2 |
|
args = self.absorb(args) |
|
1281
|
2 |
|
if len(args) == 1: |
|
1282
|
|
|
return args[0] |
|
1283
|
|
|
|
|
1284
|
|
|
# Commutativity: A & B = B & A, A | B = B | A |
|
1285
|
2 |
|
if sort: |
|
1286
|
2 |
|
args.sort() |
|
1287
|
|
|
|
|
1288
|
|
|
# Create new (now canonical) expression. |
|
1289
|
2 |
|
expr = self.__class__(*args) |
|
1290
|
2 |
|
expr.iscanonical = True |
|
1291
|
2 |
|
return expr |
|
1292
|
|
|
|
|
1293
|
2 |
|
def flatten(self): |
|
1294
|
|
|
""" |
|
1295
|
|
|
Return a new expression where nested terms of this expression are |
|
1296
|
|
|
flattened as far as possible. |
|
1297
|
|
|
|
|
1298
|
|
|
E.g. A & (B & C) becomes A & B & C. |
|
1299
|
|
|
""" |
|
1300
|
2 |
|
args = list(self.args) |
|
1301
|
2 |
|
i = 0 |
|
1302
|
2 |
|
for arg in self.args: |
|
1303
|
2 |
|
if isinstance(arg, self.__class__): |
|
1304
|
2 |
|
args[i:i + 1] = arg.args |
|
1305
|
2 |
|
i += len(arg.args) |
|
1306
|
|
|
else: |
|
1307
|
2 |
|
i += 1 |
|
1308
|
|
|
|
|
1309
|
2 |
|
return self.__class__(*args) |
|
1310
|
|
|
|
|
1311
|
2 |
|
def absorb(self, args): |
|
1312
|
|
|
""" |
|
1313
|
|
|
Given an `args` sequence of expressions, return a new list of expression |
|
1314
|
|
|
applying absorption and negative absorption. |
|
1315
|
|
|
|
|
1316
|
|
|
See https://en.wikipedia.org/wiki/Absorption_law |
|
1317
|
|
|
|
|
1318
|
|
|
Absorption: A & (A | B) = A, A | (A & B) = A |
|
1319
|
|
|
Negative absorption: A & (~A | B) = A & B, A | (~A & B) = A | B |
|
1320
|
|
|
""" |
|
1321
|
2 |
|
args = list(args) |
|
1322
|
2 |
|
if not args: |
|
1323
|
|
|
args = list(self.args) |
|
1324
|
2 |
|
i = 0 |
|
1325
|
2 |
|
while i < len(args): |
|
1326
|
2 |
|
absorber = args[i] |
|
1327
|
2 |
|
j = 0 |
|
1328
|
2 |
|
while j < len(args): |
|
1329
|
2 |
|
if j == i: |
|
1330
|
2 |
|
j += 1 |
|
1331
|
2 |
|
continue |
|
1332
|
2 |
|
target = args[j] |
|
1333
|
2 |
|
if not isinstance(target, self.dual): |
|
1334
|
2 |
|
j += 1 |
|
1335
|
2 |
|
continue |
|
1336
|
|
|
|
|
1337
|
|
|
# Absorption |
|
1338
|
2 |
|
if absorber in target: |
|
1339
|
|
|
del args[j] |
|
1340
|
|
|
if j < i: |
|
1341
|
|
|
i -= 1 |
|
1342
|
|
|
continue |
|
1343
|
|
|
|
|
1344
|
|
|
# Negative absorption |
|
1345
|
2 |
|
neg_absorber = self.NOT(absorber).cancel() |
|
1346
|
2 |
|
if neg_absorber in target: |
|
1347
|
|
|
b = target.subtract(neg_absorber, simplify=False) |
|
1348
|
|
|
if b is None: |
|
1349
|
|
|
del args[j] |
|
1350
|
|
|
if j < i: |
|
1351
|
|
|
i -= 1 |
|
1352
|
|
|
continue |
|
1353
|
|
|
else: |
|
1354
|
|
|
args[j] = b |
|
1355
|
|
|
j += 1 |
|
1356
|
|
|
continue |
|
1357
|
|
|
|
|
1358
|
2 |
|
if isinstance(absorber, self.dual): |
|
1359
|
2 |
|
remove = None |
|
1360
|
2 |
|
for arg in absorber.args: |
|
1361
|
2 |
|
narg = self.NOT(arg).cancel() |
|
1362
|
2 |
|
if arg in target.args: |
|
1363
|
2 |
|
pass |
|
1364
|
2 |
|
elif narg in target.args: |
|
1365
|
|
|
if remove is None: |
|
1366
|
|
|
remove = narg |
|
1367
|
|
|
else: |
|
1368
|
|
|
remove = None |
|
1369
|
|
|
break |
|
1370
|
|
|
else: |
|
1371
|
2 |
|
remove = None |
|
1372
|
2 |
|
break |
|
1373
|
2 |
|
if remove is not None: |
|
1374
|
|
|
args[j] = target.subtract(remove, simplify=True) |
|
1375
|
2 |
|
j += 1 |
|
1376
|
2 |
|
i += 1 |
|
1377
|
|
|
|
|
1378
|
2 |
|
return args |
|
1379
|
|
|
|
|
1380
|
2 |
|
def subtract(self, expr, simplify): |
|
1381
|
|
|
""" |
|
1382
|
|
|
Return a new expression where the `expr` expression has been removed |
|
1383
|
|
|
from this expression if it exists. |
|
1384
|
|
|
""" |
|
1385
|
|
|
args = self.args |
|
1386
|
|
|
if expr in self.args: |
|
1387
|
|
|
args = list(self.args) |
|
1388
|
|
|
args.remove(expr) |
|
1389
|
|
|
elif isinstance(expr, self.__class__): |
|
1390
|
|
|
if all(arg in self.args for arg in expr.args): |
|
1391
|
|
|
args = tuple(arg for arg in self.args if arg not in expr) |
|
1392
|
|
|
if len(args) == 0: |
|
1393
|
|
|
return None |
|
1394
|
|
|
if len(args) == 1: |
|
1395
|
|
|
return args[0] |
|
1396
|
|
|
|
|
1397
|
|
|
newexpr = self.__class__(*args) |
|
1398
|
|
|
if simplify: |
|
1399
|
|
|
newexpr = newexpr.simplify() |
|
1400
|
|
|
return newexpr |
|
1401
|
|
|
|
|
1402
|
2 |
|
def distributive(self): |
|
1403
|
|
|
""" |
|
1404
|
|
|
Return a term where the leading AND or OR terms are switched. |
|
1405
|
|
|
|
|
1406
|
|
|
This is done by applying the distributive laws: |
|
1407
|
|
|
A & (B|C) = (A&B) | (A&C) |
|
1408
|
|
|
A | (B&C) = (A|B) & (A|C) |
|
1409
|
|
|
""" |
|
1410
|
2 |
|
dual = self.dual |
|
1411
|
2 |
|
args = list(self.args) |
|
1412
|
2 |
|
for i, arg in enumerate(args): |
|
1413
|
2 |
|
if isinstance(arg, dual): |
|
1414
|
2 |
|
args[i] = arg.args |
|
1415
|
|
|
else: |
|
1416
|
2 |
|
args[i] = (arg,) |
|
1417
|
|
|
|
|
1418
|
2 |
|
prod = itertools.product(*args) |
|
1419
|
2 |
|
args = tuple(self.__class__(*arg).simplify() for arg in prod) |
|
1420
|
|
|
|
|
1421
|
2 |
|
if len(args) == 1: |
|
1422
|
2 |
|
return args[0] |
|
1423
|
|
|
else: |
|
1424
|
2 |
|
return dual(*args) |
|
1425
|
|
|
|
|
1426
|
2 |
|
def __lt__(self, other): |
|
1427
|
2 |
|
comparator = Expression.__lt__(self, other) |
|
1428
|
2 |
|
if comparator is not NotImplemented: |
|
1429
|
2 |
|
return comparator |
|
1430
|
|
|
|
|
1431
|
2 |
|
if isinstance(other, self.__class__): |
|
1432
|
2 |
|
lenself = len(self.args) |
|
1433
|
2 |
|
lenother = len(other.args) |
|
1434
|
2 |
|
for i in range(min(lenself, lenother)): |
|
1435
|
2 |
|
if self.args[i] == other.args[i]: |
|
1436
|
2 |
|
continue |
|
1437
|
|
|
|
|
1438
|
2 |
|
comparator = self.args[i] < other.args[i] |
|
1439
|
2 |
|
if comparator is not NotImplemented: |
|
1440
|
2 |
|
return comparator |
|
1441
|
|
|
|
|
1442
|
|
|
if lenself != lenother: |
|
1443
|
|
|
return lenself < lenother |
|
1444
|
2 |
|
return NotImplemented |
|
1445
|
|
|
|
|
1446
|
2 |
|
def __call__(self, **kwargs): |
|
1447
|
|
|
""" |
|
1448
|
|
|
Return the evaluation of this expression by calling each of its arg as |
|
1449
|
|
|
arg(**kwargs) and applying its corresponding Python operator (and or or) |
|
1450
|
|
|
to the results. |
|
1451
|
|
|
|
|
1452
|
|
|
Reduce is used as in e.g. AND(a, b, c, d) == AND(a, AND(b, AND(c, d))) |
|
1453
|
|
|
ore.g. OR(a, b, c, d) == OR(a, OR(b, OR(c, d))) |
|
1454
|
|
|
""" |
|
1455
|
2 |
|
return reduce(self._pyoperator, (a(**kwargs) for a in self.args)) |
|
|
|
|
|
|
1456
|
|
|
|
|
1457
|
|
|
|
|
1458
|
2 |
|
class AND(DualBase): |
|
1459
|
|
|
""" |
|
1460
|
|
|
Boolean AND operation, taking 2 or more arguments. |
|
1461
|
|
|
|
|
1462
|
|
|
It can also be created by using "&" between two boolean expressions. |
|
1463
|
|
|
|
|
1464
|
|
|
You can subclass to define alternative string representation. |
|
1465
|
|
|
For example:: |
|
1466
|
|
|
>>> class AND2(AND): |
|
1467
|
|
|
... def __init__(self, *args): |
|
1468
|
|
|
... super(AND2, self).__init__(*args) |
|
1469
|
|
|
... self.operator = 'AND' |
|
1470
|
|
|
""" |
|
1471
|
|
|
|
|
1472
|
2 |
|
sort_order = 10 |
|
1473
|
2 |
|
_pyoperator = and_operator |
|
1474
|
|
|
|
|
1475
|
2 |
|
def __init__(self, arg1, arg2, *args): |
|
1476
|
2 |
|
super(AND, self).__init__(arg1, arg2, *args) |
|
1477
|
2 |
|
self.identity = self.TRUE |
|
1478
|
2 |
|
self.annihilator = self.FALSE |
|
1479
|
2 |
|
self.dual = self.OR |
|
1480
|
2 |
|
self.operator = '&' |
|
1481
|
|
|
|
|
1482
|
|
|
|
|
1483
|
2 |
|
class OR(DualBase): |
|
1484
|
|
|
""" |
|
1485
|
|
|
Boolean OR operation, taking 2 or more arguments |
|
1486
|
|
|
|
|
1487
|
|
|
It can also be created by using "|" between two boolean expressions. |
|
1488
|
|
|
|
|
1489
|
|
|
You can subclass to define alternative string representation. |
|
1490
|
|
|
For example:: |
|
1491
|
|
|
|
|
1492
|
|
|
>>> class OR2(OR): |
|
1493
|
|
|
... def __init__(self, *args): |
|
1494
|
|
|
... super(OR2, self).__init__(*args) |
|
1495
|
|
|
... self.operator = 'OR' |
|
1496
|
|
|
""" |
|
1497
|
|
|
|
|
1498
|
2 |
|
sort_order = 25 |
|
1499
|
2 |
|
_pyoperator = or_operator |
|
1500
|
|
|
|
|
1501
|
2 |
|
def __init__(self, arg1, arg2, *args): |
|
1502
|
2 |
|
super(OR, self).__init__(arg1, arg2, *args) |
|
1503
|
2 |
|
self.identity = self.FALSE |
|
1504
|
2 |
|
self.annihilator = self.TRUE |
|
1505
|
2 |
|
self.dual = self.AND |
|
1506
|
|
|
self.operator = '|' |
|
1507
|
|
|
|