"""
|
Ops for masked arrays.
|
"""
|
from __future__ import annotations
|
|
import numpy as np
|
|
from pandas._libs import (
|
lib,
|
missing as libmissing,
|
)
|
|
|
def kleene_or(
|
left: bool | np.ndarray | libmissing.NAType,
|
right: bool | np.ndarray | libmissing.NAType,
|
left_mask: np.ndarray | None,
|
right_mask: np.ndarray | None,
|
):
|
"""
|
Boolean ``or`` using Kleene logic.
|
|
Values are NA where we have ``NA | NA`` or ``NA | False``.
|
``NA | True`` is considered True.
|
|
Parameters
|
----------
|
left, right : ndarray, NA, or bool
|
The values of the array.
|
left_mask, right_mask : ndarray, optional
|
The masks. Only one of these may be None, which implies that
|
the associated `left` or `right` value is a scalar.
|
|
Returns
|
-------
|
result, mask: ndarray[bool]
|
The result of the logical or, and the new mask.
|
"""
|
# To reduce the number of cases, we ensure that `left` & `left_mask`
|
# always come from an array, not a scalar. This is safe, since
|
# A | B == B | A
|
if left_mask is None:
|
return kleene_or(right, left, right_mask, left_mask)
|
|
if not isinstance(left, np.ndarray):
|
raise TypeError("Either `left` or `right` need to be a np.ndarray.")
|
|
raise_for_nan(right, method="or")
|
|
if right is libmissing.NA:
|
result = left.copy()
|
else:
|
result = left | right
|
|
if right_mask is not None:
|
# output is unknown where (False & NA), (NA & False), (NA & NA)
|
left_false = ~(left | left_mask)
|
right_false = ~(right | right_mask)
|
mask = (
|
(left_false & right_mask)
|
| (right_false & left_mask)
|
| (left_mask & right_mask)
|
)
|
else:
|
if right is True:
|
mask = np.zeros_like(left_mask)
|
elif right is libmissing.NA:
|
mask = (~left & ~left_mask) | left_mask
|
else:
|
# False
|
mask = left_mask.copy()
|
|
return result, mask
|
|
|
def kleene_xor(
|
left: bool | np.ndarray | libmissing.NAType,
|
right: bool | np.ndarray | libmissing.NAType,
|
left_mask: np.ndarray | None,
|
right_mask: np.ndarray | None,
|
):
|
"""
|
Boolean ``xor`` using Kleene logic.
|
|
This is the same as ``or``, with the following adjustments
|
|
* True, True -> False
|
* True, NA -> NA
|
|
Parameters
|
----------
|
left, right : ndarray, NA, or bool
|
The values of the array.
|
left_mask, right_mask : ndarray, optional
|
The masks. Only one of these may be None, which implies that
|
the associated `left` or `right` value is a scalar.
|
|
Returns
|
-------
|
result, mask: ndarray[bool]
|
The result of the logical xor, and the new mask.
|
"""
|
# To reduce the number of cases, we ensure that `left` & `left_mask`
|
# always come from an array, not a scalar. This is safe, since
|
# A ^ B == B ^ A
|
if left_mask is None:
|
return kleene_xor(right, left, right_mask, left_mask)
|
|
if not isinstance(left, np.ndarray):
|
raise TypeError("Either `left` or `right` need to be a np.ndarray.")
|
|
raise_for_nan(right, method="xor")
|
if right is libmissing.NA:
|
result = np.zeros_like(left)
|
else:
|
result = left ^ right
|
|
if right_mask is None:
|
if right is libmissing.NA:
|
mask = np.ones_like(left_mask)
|
else:
|
mask = left_mask.copy()
|
else:
|
mask = left_mask | right_mask
|
|
return result, mask
|
|
|
def kleene_and(
|
left: bool | libmissing.NAType | np.ndarray,
|
right: bool | libmissing.NAType | np.ndarray,
|
left_mask: np.ndarray | None,
|
right_mask: np.ndarray | None,
|
):
|
"""
|
Boolean ``and`` using Kleene logic.
|
|
Values are ``NA`` for ``NA & NA`` or ``True & NA``.
|
|
Parameters
|
----------
|
left, right : ndarray, NA, or bool
|
The values of the array.
|
left_mask, right_mask : ndarray, optional
|
The masks. Only one of these may be None, which implies that
|
the associated `left` or `right` value is a scalar.
|
|
Returns
|
-------
|
result, mask: ndarray[bool]
|
The result of the logical xor, and the new mask.
|
"""
|
# To reduce the number of cases, we ensure that `left` & `left_mask`
|
# always come from an array, not a scalar. This is safe, since
|
# A & B == B & A
|
if left_mask is None:
|
return kleene_and(right, left, right_mask, left_mask)
|
|
if not isinstance(left, np.ndarray):
|
raise TypeError("Either `left` or `right` need to be a np.ndarray.")
|
raise_for_nan(right, method="and")
|
|
if right is libmissing.NA:
|
result = np.zeros_like(left)
|
else:
|
result = left & right
|
|
if right_mask is None:
|
# Scalar `right`
|
if right is libmissing.NA:
|
mask = (left & ~left_mask) | left_mask
|
|
else:
|
mask = left_mask.copy()
|
if right is False:
|
# unmask everything
|
mask[:] = False
|
else:
|
# unmask where either left or right is False
|
left_false = ~(left | left_mask)
|
right_false = ~(right | right_mask)
|
mask = (left_mask & ~right_false) | (right_mask & ~left_false)
|
|
return result, mask
|
|
|
def raise_for_nan(value, method: str) -> None:
|
if lib.is_float(value) and np.isnan(value):
|
raise ValueError(f"Cannot perform logical '{method}' with floating NaN")
|