Test Failed
Push — master ( 36ef11...81e955 )
by Jan
03:06 queued 24s
created

ssg.ext.boolean.boolean.Symbol.__eq__()   A

Complexity

Conditions 3

Size

Total Lines 6
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nop 2
dl 0
loc 6
ccs 6
cts 6
cp 1
crap 3
rs 10
c 0
b 0
f 0
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
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable basestring does not seem to be defined.
Loading history...
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
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable reduce does not seem to be defined.
Loading history...
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):
0 ignored issues
show
introduced by
The variable prev_token_type does not seem to be defined for all execution paths.
Loading history...
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)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable arg does not seem to be defined.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable arg does not seem to be defined.
Loading history...
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))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable reduce does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable a does not seem to be defined.
Loading history...
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