Yupeek/booleano

View on GitHub
src/booleano/operations/operators.py

Summary

Maintainability
B
4 hrs
Test Coverage
# -*- coding: utf-8 -*-

# Copyright (c) 2009 by Gustavo Narea <http://gustavonarea.net/>.

# This file is part of Booleano <http://code.gustavonarea.net/booleano/>.

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, distribute with
# modifications, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the following
# conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# ABOVE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# Except as contained in this notice, the name(s) of the above copyright
# holders shall not be used in advertising or otherwise to promote the sale,
# use or other dealings in this Software without prior written authorization.
"""
Built-in operators.

"""
from __future__ import unicode_literals

import six

from booleano.operations.core import OperationNode
from booleano.operations.operands import Variable

__all__ = ("Not", "And", "Or", "Xor", "Equal", "NotEqual", "LessThan",
           "GreaterThan", "LessEqual", "GreaterEqual", "BelongsTo", "IsSubset")


class Operator(OperationNode):
    """
    Base class for logical operators.

    The operands to be used by the operator must be passed in the constructor.

    """
    _is_leaf = False

    def is_operator(self):
        """
        Check if this node is an operation.

        :rtype: bool

        """
        return True


@six.python_2_unicode_compatible
class UnaryOperator(Operator):
    """
    Base class for unary logical operators.

    """

    def __init__(self, operand):
        """
        Check that ``operand`` supports all the required operations before
        storing it.

        :param operand: The operand handled by this operator.
        :type operand: :class:`booleano.operations.Operand`

        """
        self.operand = operand

    def check_equivalence(self, node):
        """
        Make sure unary operator ``node`` and this unary operator are
        equivalent.

        :param node: The other operator which may be equivalent to this one.
        :type node: UnaryOperator
        :raises AssertionError: If ``node`` is not a unary operator or if it's
            an unary operator but doesn't have the same operand as this one.

        """
        super(UnaryOperator, self).check_equivalence(node)
        assert node.operand == self.operand, 'Operands of unary operations %s and %s are not equivalent' % (
            node,
            self
        )

    def __hash__(self):
        return id(self)

    def __str__(self):
        """
        Return the Unicode representation for this operator and its operand.

        """
        operand = six.text_type(self.operand)
        return u"%s(%s)" % (self.__class__.__name__, operand)

    def __repr__(self):
        """Return the representation for this operator and its operand."""
        operand = repr(self.operand)
        return "<%s %s>" % (self.__class__.__name__, operand)


@six.python_2_unicode_compatible
class BinaryOperator(Operator):
    """
    Base class for binary logical operators.

    In binary operations, the two operands are marked as "master" or "slave".
    The binary operator will make the *master operand* perform the requested
    operation using the Python value of the *slave operand*. This is found by
    the :meth:`organize_operands` method, which can be overridden.

    .. attribute:: master_operand

        The instance attribute that represents the master operand.

    .. attribute:: slave_operand

        The instance attribute that represents the slave operand.

    """

    def __init__(self, left_operand, right_operand):
        """
        Instantiate this operator, finding the master operand among
        ``left_operand`` and ``right_operand``.

        :param left_operand: The left-hand operand handled by this operator.
        :type left_operand: :class:`booleano.operations.Operand`
        :param right_operand: The right-hand operand handled by this operator.
        :type right_operand: :class:`booleano.operations.Operand`

        """
        master, slave = self.organize_operands(left_operand, right_operand)
        self.master_operand = master
        self.slave_operand = slave

    def organize_operands(self, left_operand, right_operand):
        """
        Find the master and slave operands among the ``left_operand`` and
        ``right_operand`` operands.

        :param left_operand: The left-hand operand handled by this operator.
        :type left_operand: :class:`booleano.operations.Operand`
        :param right_operand: The right-hand operand handled by this operator.
        :type right_operand: :class:`booleano.operations.Operand`
        :return: A pair where the first item is the master operand and the
            second one is the slave.
        :rtype: tuple

        In practice, they are only distinguished when one of the operands is a
        variable and the other is a constant. In such situations, the variable
        becomes the master operand and the constant becomes the slave operand.

        When both operands are constant or both are variable, the left-hand
        operand becomes the master and the right-hand operand becomes the slave.

        """
        l_var = isinstance(left_operand, Variable)
        r_var = isinstance(right_operand, Variable)

        if l_var == r_var or l_var:
            # Both operands are variable/constant, OR the left-hand operand is
            # a variable and the right-hand operand is a constant.
            return (left_operand, right_operand)

        # The right-hand operand is the variable and the left-hand operand the
        # constant:
        return (right_operand, left_operand)

    def check_equivalence(self, node):
        """
        Make sure binary operator ``node`` and this binary operator are
        equivalent.

        :param node: The other operator which may be equivalent to this one.
        :type node: BinaryOperator
        :raises AssertionError: If ``node`` is not a binary operator or if it's
            an binary operator but doesn't have the same operands as this one.

        """
        super(BinaryOperator, self).check_equivalence(node)
        same_operands = (
            (
                node.master_operand == self.master_operand and
                node.slave_operand == self.slave_operand
            ) or (
                node.master_operand == self.slave_operand and
                self.master_operand == node.slave_operand
            )
        )
        assert same_operands, 'Operands of binary operations %s and %s are not equivalent' % (
            node,
            self
        )

    def __str__(self):
        """
        Return the Unicode representation for this binary operator, including
        its operands.

        """
        return u"%s(%s, %s)" % (self.__class__.__name__, self.master_operand,
                                self.slave_operand)

    def __repr__(self):
        """
        Return the representation for this binary operator, including its
        operands.

        """
        return "<%s %s %s>" % (self.__class__.__name__,
                               repr(self.master_operand),
                               repr(self.slave_operand))


# Unary operators


class Not(UnaryOperator):
    """
    The logical negation (``~``).

    Negate the boolean representation of an operand.

    """

    def __init__(self, operand):
        """

        :raises booleano.exc.InvalidOperationError: If ``operand`` doesn't have
            a logical value.

        """
        operand.check_logical_support()
        super(Not, self).__init__(operand)

    def __call__(self, context):
        """
        Return the negate of the truth value for the operand.

        :param context: The evaluation context.
        :type context: object

        """
        return not self.operand(context)


# Binary operators


class _ConnectiveOperator(BinaryOperator):
    """
    Logic connective to turn the left-hand and right-hand operands into
    boolean operations, so we can manipulate their truth value easily.

    """

    def __init__(self, left_operand, right_operand):
        """

        :raises booleano.exc.InvalidOperationError: If ``left_operand`` or
            ``right_operand`` doesn't have logical values.

        """
        left_operand.check_logical_support()
        right_operand.check_logical_support()
        super(_ConnectiveOperator, self).__init__(left_operand, right_operand)


class And(_ConnectiveOperator):
    """
    The logical conjunction (``AND``).

    Connective that checks if two operations evaluate to ``True``.

    With this binary operator, the operands can be actual operands or
    operations.

    """

    def __call__(self, context):
        """
        Check if both operands evaluate to ``True``.

        :param context: The evaluation context.
        :type context: objects

        """
        return self.master_operand(context) and self.slave_operand(context)


class Or(_ConnectiveOperator):
    """
    The logical inclusive disjunction (``OR``).

    Connective that check if at least one, out of two operations, evaluate to
    ``True``.

    With this binary operator, the operands can be actual operands or
    operations.

    """

    def __call__(self, context):
        """
        Check if at least one of the operands evaluate to ``True``.

        :param context: The evaluation context.
        :type context: object

        """
        return self.master_operand(context) or self.slave_operand(context)


class Xor(_ConnectiveOperator):
    """
    The logical exclusive disjunction (``XOR``).

    Connective that checks if only one, out of two operations, evaluate to
    ``True``.

    With this binary operator, the operands can be actual operands or
    operations.

    """

    def __call__(self, context):
        """
        Check that only one of the operands evaluate to ``True``.

        :param context: The evaluation context.
        :type context: object

        """
        return self.master_operand(context) ^ self.slave_operand(context)


class Equal(BinaryOperator):
    """
    The equality operator (``==``).

    Checks that two operands are equivalent.

    For example: ``3 == 3``.

    """

    def __init__(self, left_operand, right_operand):
        """

        :param left_operand: The left-hand operand handled by this operator.
        :type left_operand: :class:`booleano.operations.Operand`
        :param right_operand: The right-hand operand handled by this operator.
        :type right_operand: :class:`booleano.operations.Operand`
        :raises booleano.exc.InvalidOperationError: If the master operand
            between ``left_operand`` or ``right_operand`` doesn't support
            equality operations.

        """
        super(Equal, self).__init__(left_operand, right_operand)
        self.master_operand.check_operation("equality")

    def __call__(self, context):
        value = self.slave_operand.to_python(context)
        return self.master_operand.equals(value, context)


# (x <> y) <=> ~(x == y)
class NotEqual(Equal):
    """
    The "not equal to" operator (``!=``).

    Checks that two operands are not equivalent.

    For example: ``3 != 2``.

    """

    def __call__(self, context):
        return not super(NotEqual, self).__call__(context)


class _InequalityOperator(BinaryOperator):
    """
    Handle inequalities (``<``, ``>``) and switch the operation if the operands
    are rearranged.

    """

    def __init__(self, left_operand, right_operand, comparison):
        """
        Switch the ``comparison`` if the operands are rearranged.

        :param left_operand: The original left-hand operand in the inequality.
        :param right_operand: The original right-hand operand in the
            inequality.
        :param comparison: The symbol for the particular inequality (i.e.,
            "<" or ">").
        :raises InvalidOperationError: If the master operand doesn't support
            inequalities.

        If the operands are rearranged by :meth:`organize_operands`, then
        the operation must be switched (e.g., from "<" to ">").

        This will also "compile" the comparison operation; otherwise, it'd have
        to be calculated on a per evaluation basis.

        """
        super(_InequalityOperator, self).__init__(left_operand, right_operand)

        self.master_operand.check_operation("inequality")

        if left_operand != self.master_operand:
            # The operands have been rearranged! Let's invert the comparison:
            if comparison == "<":
                comparison = ">"
            else:
                comparison = "<"

        # "Compiling" the comparison:
        if comparison == ">":
            self.comparison = self._greater_than
        else:
            self.comparison = self._less_than

    def __call__(self, context):
        return self.comparison(context)

    def _greater_than(self, context):
        """Check if the master operand is greater than the slave"""
        value = self.slave_operand.to_python(context)
        return self.master_operand.greater_than(value, context)

    def _less_than(self, context):
        """Check if the master operand is less than the slave"""
        value = self.slave_operand.to_python(context)
        return self.master_operand.less_than(value, context)


class LessThan(_InequalityOperator):
    """
    The "less than" operator (``<``).

    For example: ``2 < 3``.

    """

    def __init__(self, left_operand, right_operand):
        super(LessThan, self).__init__(left_operand, right_operand, "<")


class GreaterThan(_InequalityOperator):
    """
    The "greater than" operator (``>``).

    For example: ``3 > 2``.

    """

    def __init__(self, left_operand, right_operand):
        super(GreaterThan, self).__init__(left_operand, right_operand, ">")


# (x <= y) <=> ~(x > y)
class LessEqual(GreaterThan):
    """
    The "less than or equal to" operator (``<=``).

    For example: ``2 <= 3``.

    """

    def __call__(self, context):
        return not super(LessEqual, self).__call__(context)


# (x >= y) <=> ~(x < y)
class GreaterEqual(LessThan):
    """
    The "greater than or equal to" operator (``>=``).

    For example: ``2 >= 2``.

    """

    def __call__(self, context):
        return not super(GreaterEqual, self).__call__(context)


class _SetOperator(BinaryOperator):
    """
    Base class for set-related operators.

    """

    def __init__(self, left_operand, right_operand):
        """

        :raises booleano.exc.InvalidOperationError: If ``right_operand``
            doesn't support membership operations.

        """
        super(_SetOperator, self).__init__(left_operand, right_operand)
        self.master_operand.check_operation("membership")

    def organize_operands(self, left_operand, right_operand):
        """Set the set (right-hand operand) as the master operand."""
        return (right_operand, left_operand)


class BelongsTo(_SetOperator):
    """
    The "belongs to" operator (``∈``).

    For example: ``"valencia" ∈ {"caracas", "maracay", "valencia"}``.

    """

    def __call__(self, context):
        value = self.slave_operand.to_python(context)
        return self.master_operand.belongs_to(value, context)


class IsSubset(_SetOperator):
    """
    The "is a subset of" operator (``⊂``).

    For example: ``{"valencia", "aragua"} ⊂ {"caracas", "aragua", "valencia"}``.

    """

    def __call__(self, context):
        value = self.slave_operand.to_python(context)
        return self.master_operand.is_subset(value, context)