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