1"""
2Ops for masked arrays.
3"""
4
5from __future__ import annotations
6
7from typing import TYPE_CHECKING
8
9import numpy as np
10
11from pandas._libs import (
12 lib,
13 missing as libmissing,
14)
15
16if TYPE_CHECKING:
17 from pandas._typing import npt
18
19
20def kleene_or(
21 left: bool | np.ndarray | libmissing.NAType,
22 right: bool | np.ndarray | libmissing.NAType,
23 left_mask: np.ndarray | None,
24 right_mask: np.ndarray | None,
25) -> tuple[npt.NDArray[np.bool_], npt.NDArray[np.bool_]]:
26 """
27 Boolean ``or`` using Kleene logic.
28
29 Values are NA where we have ``NA | NA`` or ``NA | False``.
30 ``NA | True`` is considered True.
31
32 Parameters
33 ----------
34 left, right : ndarray, NA, or bool
35 The values of the array.
36 left_mask, right_mask : ndarray, optional
37 The masks. Only one of these may be None, which implies that
38 the associated `left` or `right` value is a scalar.
39
40 Returns
41 -------
42 result, mask: ndarray[bool]
43 The result of the logical or, and the new mask.
44 """
45 # To reduce the number of cases, we ensure that `left` & `left_mask`
46 # always come from an array, not a scalar. This is safe, since
47 # A | B == B | A
48 if left_mask is None:
49 return kleene_or(right, left, right_mask, left_mask)
50
51 if not isinstance(left, np.ndarray):
52 raise TypeError("Either `left` or `right` need to be an np.ndarray.")
53
54 raise_for_nan(right, method="or")
55
56 if right is libmissing.NA:
57 result = left.copy()
58 else:
59 result = left | right
60
61 if right_mask is not None:
62 # output is unknown where (False & NA), (NA & False), (NA & NA)
63 left_false = ~(left | left_mask)
64 right_false = ~(right | right_mask)
65 mask = (
66 (left_false & right_mask)
67 | (right_false & left_mask)
68 | (left_mask & right_mask)
69 )
70 elif right is True:
71 mask = np.zeros_like(left_mask)
72 elif right is libmissing.NA:
73 mask = (~left & ~left_mask) | left_mask
74 else:
75 # False
76 mask = left_mask.copy()
77
78 return result, mask
79
80
81def kleene_xor(
82 left: bool | np.ndarray | libmissing.NAType,
83 right: bool | np.ndarray | libmissing.NAType,
84 left_mask: np.ndarray | None,
85 right_mask: np.ndarray | None,
86) -> tuple[npt.NDArray[np.bool_], npt.NDArray[np.bool_]]:
87 """
88 Boolean ``xor`` using Kleene logic.
89
90 This is the same as ``or``, with the following adjustments
91
92 * True, True -> False
93 * True, NA -> NA
94
95 Parameters
96 ----------
97 left, right : ndarray, NA, or bool
98 The values of the array.
99 left_mask, right_mask : ndarray, optional
100 The masks. Only one of these may be None, which implies that
101 the associated `left` or `right` value is a scalar.
102
103 Returns
104 -------
105 result, mask: ndarray[bool]
106 The result of the logical xor, and the new mask.
107 """
108 # To reduce the number of cases, we ensure that `left` & `left_mask`
109 # always come from an array, not a scalar. This is safe, since
110 # A ^ B == B ^ A
111 if left_mask is None:
112 return kleene_xor(right, left, right_mask, left_mask)
113
114 if not isinstance(left, np.ndarray):
115 raise TypeError("Either `left` or `right` need to be an np.ndarray.")
116
117 raise_for_nan(right, method="xor")
118 if right is libmissing.NA:
119 result = np.zeros_like(left)
120 else:
121 result = left ^ right
122
123 if right_mask is None:
124 if right is libmissing.NA:
125 mask = np.ones_like(left_mask)
126 else:
127 mask = left_mask.copy()
128 else:
129 mask = left_mask | right_mask
130
131 return result, mask
132
133
134def kleene_and(
135 left: bool | libmissing.NAType | np.ndarray,
136 right: bool | libmissing.NAType | np.ndarray,
137 left_mask: np.ndarray | None,
138 right_mask: np.ndarray | None,
139) -> tuple[npt.NDArray[np.bool_], npt.NDArray[np.bool_]]:
140 """
141 Boolean ``and`` using Kleene logic.
142
143 Values are ``NA`` for ``NA & NA`` or ``True & NA``.
144
145 Parameters
146 ----------
147 left, right : ndarray, NA, or bool
148 The values of the array.
149 left_mask, right_mask : ndarray, optional
150 The masks. Only one of these may be None, which implies that
151 the associated `left` or `right` value is a scalar.
152
153 Returns
154 -------
155 result, mask: ndarray[bool]
156 The result of the logical xor, and the new mask.
157 """
158 # To reduce the number of cases, we ensure that `left` & `left_mask`
159 # always come from an array, not a scalar. This is safe, since
160 # A & B == B & A
161 if left_mask is None:
162 return kleene_and(right, left, right_mask, left_mask)
163
164 if not isinstance(left, np.ndarray):
165 raise TypeError("Either `left` or `right` need to be an np.ndarray.")
166 raise_for_nan(right, method="and")
167
168 if right is libmissing.NA:
169 result = np.zeros_like(left)
170 else:
171 result = left & right
172
173 if right_mask is None:
174 # Scalar `right`
175 if right is libmissing.NA:
176 mask = (left & ~left_mask) | left_mask
177
178 else:
179 mask = left_mask.copy()
180 if right is False:
181 # unmask everything
182 mask[:] = False
183 else:
184 # unmask where either left or right is False
185 left_false = ~(left | left_mask)
186 right_false = ~(right | right_mask)
187 mask = (left_mask & ~right_false) | (right_mask & ~left_false)
188
189 return result, mask
190
191
192def raise_for_nan(value: object, method: str) -> None:
193 if lib.is_float(value) and np.isnan(value):
194 raise ValueError(f"Cannot perform logical '{method}' with floating NaN")