# Copyright 2023 Goldman Sachs
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Boolean expression type in the PyLegend expression language.
``PyLegendBoolean`` represents a boolean-valued column or computed
expression within a PyLegend query. It supports the standard logical
operators (``&``, ``|``, ``~``, ``^``), comparison operators
(``<``, ``<=``, ``>``, ``>=``), and conditional branching via
:meth:`~PyLegendBoolean.case`.
Instances are never constructed directly. They are produced by
operations on TDS frame columns — for example, comparing a column
to a literal (``frame["Age"] > 30``) or accessing a boolean column
through a row accessor (``row.get_boolean("is_active")``).
``PyLegendBoolean`` also inherits general-purpose methods from
``PyLegendPrimitive``, including equality / inequality tests, null checks,
string conversion, and ``in_list``.
"""
from pylegend._typing import (
PyLegendSequence,
PyLegendDict,
PyLegendUnion,
)
from pylegend.core.language.shared.primitives.primitive import PyLegendPrimitive, PyLegendPrimitiveOrPythonPrimitive
from pylegend.core.language.shared.expression import (
PyLegendExpression,
PyLegendExpressionBooleanReturn,
PyLegendExpressionStringReturn,
PyLegendExpressionNumberReturn,
PyLegendExpressionIntegerReturn,
PyLegendExpressionFloatReturn,
PyLegendExpressionDecimalReturn,
PyLegendExpressionDateReturn,
PyLegendExpressionDateTimeReturn,
PyLegendExpressionStrictDateReturn,
)
from decimal import Decimal
from pylegend.core.language.shared.literal_expressions import (
PyLegendBooleanLiteralExpression,
convert_literal_to_literal_expression,
)
from pylegend.core.language.shared.operations.boolean_operation_expressions import (
PyLegendBooleanOrExpression,
PyLegendBooleanAndExpression,
PyLegendBooleanNotExpression,
PyLegendBooleanLessThanExpression,
PyLegendBooleanLessThanEqualExpression,
PyLegendBooleanGreaterThanExpression,
PyLegendBooleanGreaterThanEqualExpression,
PyLegendBooleanXorExpression,
PyLegendBooleanCaseExpression,
PyLegendNumberCaseExpression,
PyLegendIntegerCaseExpression,
PyLegendFloatCaseExpression,
PyLegendDecimalCaseExpression,
PyLegendStringCaseExpression,
PyLegendDateCaseExpression,
PyLegendDateTimeCaseExpression,
PyLegendStrictDateCaseExpression
)
from pylegend.core.sql.metamodel import (
Expression,
QuerySpecification
)
from pylegend.core.tds.pandas_api.frames.helpers.series_helper import grammar_method
from pylegend.core.tds.tds_frame import FrameToSqlConfig
from pylegend.core.tds.tds_frame import FrameToPureConfig
from datetime import date, datetime
__all__: PyLegendSequence[str] = [
"PyLegendBoolean"
]
class PyLegendBoolean(PyLegendPrimitive):
__value: PyLegendExpressionBooleanReturn
def __init__(
self,
value: PyLegendExpressionBooleanReturn
) -> None:
self.__value = value
def to_sql_expression(
self,
frame_name_to_base_query_map: PyLegendDict[str, QuerySpecification],
config: FrameToSqlConfig
) -> Expression:
return self.__value.to_sql_expression(frame_name_to_base_query_map, config)
def to_pure_expression(self, config: FrameToPureConfig) -> str:
return self.__value.to_pure_expression(config)
def value(self) -> PyLegendExpressionBooleanReturn:
return self.__value
[docs]
@grammar_method
def case(
self,
if_true: "PyLegendPrimitiveOrPythonPrimitive",
if_false: "PyLegendPrimitiveOrPythonPrimitive",
) -> PyLegendPrimitive:
"""
Conditional expression — SQL ``CASE WHEN … THEN … ELSE … END``.
Evaluate this boolean expression and return *if_true* where it
is ``True``, or *if_false* where it is ``False``.
Both branches must resolve to the **same type family** (e.g.
both numeric, both string, both date). The return type of the
resulting expression matches that family.
Parameters
----------
if_true : int, float, bool, str, date, datetime, Decimal, or PyLegendPrimitive
Value returned when the condition is ``True``.
if_false : int, float, bool, str, date, datetime, Decimal, or PyLegendPrimitive
Value returned when the condition is ``False``.
Returns
-------
PyLegendPrimitive
An expression whose concrete type depends on the branch
values (e.g. ``PyLegendInteger`` if both branches are
integers, ``PyLegendString`` if both are strings).
Raises
------
TypeError
If *if_true* or *if_false* is not a supported primitive
type, or if the two branches have incompatible types.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Classify orders by size
frame["size"] = (frame["Order Id"] > 10250).case("large", "small")
frame.head(5).to_pandas()
# Numeric branch — compute a bonus
frame["bonus"] = (frame["Order Id"] > 10250).case(100, 0)
frame.head(5).to_pandas()
"""
from pylegend.core.language.shared.primitives import (
PyLegendString, PyLegendNumber, PyLegendInteger, PyLegendFloat, PyLegendDecimal,
PyLegendDate, PyLegendDateTime, PyLegendStrictDate
)
def resolve_param(param: PyLegendPrimitiveOrPythonPrimitive,
name: str) -> "PyLegendExpression":
if isinstance(param, (bool, int, float, str, date, datetime, Decimal)):
return convert_literal_to_literal_expression(param)
elif isinstance(param, PyLegendPrimitive):
return param.value()
else:
raise TypeError(
f"case {name} parameter should be a primitive value or PyLegendPrimitive expression."
f" Got value {param} of type: {type(param)}"
)
true_expr = resolve_param(if_true, "if_true")
false_expr = resolve_param(if_false, "if_false")
if isinstance(true_expr, PyLegendExpressionBooleanReturn) \
and isinstance(false_expr, PyLegendExpressionBooleanReturn):
return PyLegendBoolean(PyLegendBooleanCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionStringReturn) \
and isinstance(false_expr, PyLegendExpressionStringReturn):
return PyLegendString(PyLegendStringCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionDateTimeReturn) \
and isinstance(false_expr, PyLegendExpressionDateTimeReturn):
return PyLegendDateTime(PyLegendDateTimeCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionStrictDateReturn) \
and isinstance(false_expr, PyLegendExpressionStrictDateReturn):
return PyLegendStrictDate(PyLegendStrictDateCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionDateReturn) \
and isinstance(false_expr, PyLegendExpressionDateReturn):
return PyLegendDate(PyLegendDateCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionDecimalReturn) \
and isinstance(false_expr, PyLegendExpressionDecimalReturn):
return PyLegendDecimal(PyLegendDecimalCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionFloatReturn) \
and isinstance(false_expr, PyLegendExpressionFloatReturn):
return PyLegendFloat(PyLegendFloatCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionIntegerReturn) \
and isinstance(false_expr, PyLegendExpressionIntegerReturn):
return PyLegendInteger(PyLegendIntegerCaseExpression(
self.__value, true_expr, false_expr
))
elif isinstance(true_expr, PyLegendExpressionNumberReturn) \
and isinstance(false_expr, PyLegendExpressionNumberReturn):
return PyLegendNumber(PyLegendNumberCaseExpression(
self.__value, true_expr, false_expr
))
else:
raise TypeError(
f"case if_true and if_false parameters must be of the same type."
f" Got if_true of type: {type(if_true)} and if_false of type: {type(if_false)}."
f" Supported types are: bool, int, float, Decimal, str, date, datetime, PyLegendPrimitive"
)
[docs]
@grammar_method
def __or__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Logical OR (``|``).
Combine two boolean expressions with a logical OR.
Parameters
----------
other : bool or PyLegendBoolean
The right-hand operand.
Returns
-------
PyLegendBoolean
``True`` where either operand is ``True``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Orders with id < 10250 OR id > 10260
cond = (frame["Order Id"] < 10250) | (frame["Order Id"] > 10260)
frame[cond].head(3).to_pandas()
"""
PyLegendBoolean.__validate__param_to_be_bool(other, "Boolean OR (|) parameter")
other_op = PyLegendBooleanLiteralExpression(other) if isinstance(other, bool) else other.__value
return PyLegendBoolean(PyLegendBooleanOrExpression(self.__value, other_op))
[docs]
@grammar_method
def __ror__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Reflected logical OR (``bool | expr``).
Called when a Python ``bool`` literal is on the left side of
``|``. Behaves identically to :meth:`__or__` with swapped
operand order.
Parameters
----------
other : bool or PyLegendBoolean
The left-hand operand.
Returns
-------
PyLegendBoolean
``True`` where either operand is ``True``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
"""
PyLegendBoolean.__validate__param_to_be_bool(other, "Boolean OR (|) parameter")
other_op = PyLegendBooleanLiteralExpression(other) if isinstance(other, bool) else other.__value
return PyLegendBoolean(PyLegendBooleanOrExpression(other_op, self.__value))
[docs]
@grammar_method
def __and__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Logical AND (``&``).
Combine two boolean expressions with a logical AND.
Parameters
----------
other : bool or PyLegendBoolean
The right-hand operand.
Returns
-------
PyLegendBoolean
``True`` where both operands are ``True``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Orders with id >= 10250 AND id <= 10260
cond = (frame["Order Id"] >= 10250) & (frame["Order Id"] <= 10260)
frame[cond].head(3).to_pandas()
"""
PyLegendBoolean.__validate__param_to_be_bool(other, "Boolean AND (&) parameter")
other_op = PyLegendBooleanLiteralExpression(other) if isinstance(other, bool) else other.__value
return PyLegendBoolean(PyLegendBooleanAndExpression(self.__value, other_op))
[docs]
@grammar_method
def __rand__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Reflected logical AND (``bool & expr``).
Called when a Python ``bool`` literal is on the left side of
``&``. Behaves identically to :meth:`__and__` with swapped
operand order.
Parameters
----------
other : bool or PyLegendBoolean
The left-hand operand.
Returns
-------
PyLegendBoolean
``True`` where both operands are ``True``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
"""
PyLegendBoolean.__validate__param_to_be_bool(other, "Boolean AND (&) parameter")
other_op = PyLegendBooleanLiteralExpression(other) if isinstance(other, bool) else other.__value
return PyLegendBoolean(PyLegendBooleanAndExpression(other_op, self.__value))
[docs]
@grammar_method
def __invert__(self) -> "PyLegendBoolean":
"""
Logical NOT (``~``).
Negate this boolean expression.
Returns
-------
PyLegendBoolean
``True`` where the original expression is ``False``, and
vice-versa.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Negate a condition
frame[~(frame["Order Id"] > 10255)].head(3).to_pandas()
"""
return PyLegendBoolean(PyLegendBooleanNotExpression(self.__value))
[docs]
@grammar_method
def __lt__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Less than comparison (``<``).
Parameters
----------
other : bool or PyLegendBoolean
The right-hand operand.
Returns
-------
PyLegendBoolean
``True`` where ``self < other``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Compare two boolean expressions: is (id > 10260) < (id > 10250)?
frame["lt_check"] = (frame["Order Id"] > 10260) < (frame["Order Id"] > 10250)
frame.head(3).to_pandas()
"""
return self._create_binary_expression(other, PyLegendBooleanLessThanExpression, "less than (<)")
[docs]
@grammar_method
def __le__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Less than or equal comparison (``<=``).
Parameters
----------
other : bool or PyLegendBoolean
The right-hand operand.
Returns
-------
PyLegendBoolean
``True`` where ``self <= other``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
frame["le_check"] = (frame["Order Id"] > 10260) <= (frame["Order Id"] > 10250)
frame.head(3).to_pandas()
"""
return self._create_binary_expression(other, PyLegendBooleanLessThanEqualExpression, "less than equal (<=)")
[docs]
@grammar_method
def __gt__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Greater than comparison (``>``).
Parameters
----------
other : bool or PyLegendBoolean
The right-hand operand.
Returns
-------
PyLegendBoolean
``True`` where ``self > other``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
frame["gt_check"] = (frame["Order Id"] > 10250) > (frame["Order Id"] > 10260)
frame.head(3).to_pandas()
"""
return self._create_binary_expression(other, PyLegendBooleanGreaterThanExpression, "greater than (>)")
[docs]
@grammar_method
def __ge__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Greater than or equal comparison (``>=``).
Parameters
----------
other : bool or PyLegendBoolean
The right-hand operand.
Returns
-------
PyLegendBoolean
``True`` where ``self >= other``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
frame["ge_check"] = (frame["Order Id"] > 10250) >= (frame["Order Id"] > 10260)
frame.head(3).to_pandas()
"""
return self._create_binary_expression(
other,
PyLegendBooleanGreaterThanEqualExpression,
"greater than equal (>=)")
[docs]
@grammar_method
def __xor__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Logical XOR (``^``).
Exclusive OR — ``True`` when exactly one operand is ``True``.
Translates to SQL ``<>`` and Pure ``->xor()``.
Parameters
----------
other : bool or PyLegendBoolean
The right-hand operand.
Returns
-------
PyLegendBoolean
``True`` where exactly one operand is ``True``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# XOR two conditions
cond = (frame["Order Id"] > 10255) ^ (frame["Order Id"] < 10250)
frame[cond].head(3).to_pandas()
"""
return self._create_binary_expression(other, PyLegendBooleanXorExpression, "xor (^)")
[docs]
@grammar_method
def __rxor__(self, other: PyLegendUnion[bool, "PyLegendBoolean"]) -> "PyLegendBoolean":
"""
Reflected logical XOR (``bool ^ expr``).
Called when a Python ``bool`` literal is on the left side of
``^``. Behaves identically to :meth:`__xor__` with swapped
operand order.
Parameters
----------
other : bool or PyLegendBoolean
The left-hand operand.
Returns
-------
PyLegendBoolean
``True`` where exactly one operand is ``True``.
Raises
------
TypeError
If *other* is not a ``bool`` or ``PyLegendBoolean``.
"""
return self._create_binary_expression(other, PyLegendBooleanXorExpression, "xor (^)", reverse=True)
def _create_binary_expression(
self,
other: PyLegendUnion[bool, "PyLegendBoolean"],
expression_class: type,
operation_name: str,
reverse: bool = False
) -> "PyLegendBoolean":
PyLegendBoolean.__validate__param_to_be_bool(other, f"Boolean {operation_name} parameter")
other_op = PyLegendBooleanLiteralExpression(other) if isinstance(other, bool) else other.__value
if reverse:
return PyLegendBoolean(expression_class(other_op, self.__value))
return PyLegendBoolean(expression_class(self.__value, other_op))
@staticmethod
def __validate__param_to_be_bool(param: PyLegendUnion[bool, "PyLegendBoolean"], desc: str) -> None:
if not isinstance(param, (bool, PyLegendBoolean)):
raise TypeError(desc + " should be a bool or a boolean expression (PyLegendBoolean)."
" Got value " + str(param) + " of type: " + str(type(param)))