Skip to content

Commit 17432f7

Browse files
Merge pull request #753 from sadielbartholomew/open-intervals-wi-wo-query
Open and half-open intervals for `wi` queries
2 parents a5f91ab + 030e5cf commit 17432f7

File tree

3 files changed

+257
-19
lines changed

3 files changed

+257
-19
lines changed

Changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ version 3.16.2
2424
* Fix bug in `cf.read` when reading UM files that caused LBPROC value
2525
131072 (Mean over an ensemble of parallel runs) to be ignored
2626
(https://github.com/NCAS-CMS/cf-python/issues/737)
27+
* New keyword parameters to `cf.wi`: ``open_lower`` and ``open_upper``
28+
(https://github.com/NCAS-CMS/cf-python/issues/740)
2729
* Fix bug in `cf.aggregate` that sometimes put a null transpose
2830
operation into the Dask graph when one was not needed
2931
(https://github.com/NCAS-CMS/cf-python/issues/754)

cf/query.py

Lines changed: 147 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ def __init__(
207207
exact=True,
208208
rtol=None,
209209
atol=None,
210+
open_lower=False,
211+
open_upper=False,
210212
):
211213
"""**Initialisation**
212214
@@ -249,6 +251,24 @@ def __init__(
249251
250252
.. versionadded:: 3.15.2
251253
254+
open_lower: `bool`, optional
255+
Only applicable to the ``'wi'`` operator.
256+
If True, open the interval at the lower
257+
bound so that value0 is excluded from the
258+
range. By default the interval is closed
259+
so that value0 is included.
260+
261+
.. versionadded:: NEXTVERSION
262+
263+
open_upper: `bool`, optional
264+
Only applicable to the ``'wi'`` operator.
265+
If True, open the interval at the upper
266+
bound so that value1 is excluded from the
267+
range. By default the interval is closed
268+
so that value1 is included.
269+
270+
.. versionadded:: NEXTVERSION
271+
252272
exact: deprecated at version 3.0.0.
253273
Use `re.compile` objects in *value* instead.
254274
@@ -289,6 +309,16 @@ def __init__(
289309
self._rtol = rtol
290310
self._atol = atol
291311

312+
if open_lower or open_upper:
313+
if operator != "wi":
314+
raise ValueError(
315+
"Can only set the 'open_lower' and 'open_upper' "
316+
"parameters for the 'wi' operator"
317+
)
318+
319+
self._open_lower = open_lower
320+
self._open_upper = open_upper
321+
292322
def __dask_tokenize__(self):
293323
"""Return a hashable value fully representative of the object.
294324
@@ -316,6 +346,9 @@ def __dask_tokenize__(self):
316346
if operator == "isclose":
317347
value += (self.rtol, self.atol)
318348

349+
if operator == "wi":
350+
value += (self.open_lower, self.open_upper)
351+
319352
return (self.__class__, operator, self._attr) + value
320353

321354
def __deepcopy__(self, memo):
@@ -452,8 +485,22 @@ def __str__(self):
452485
attr = ".".join(self._attr)
453486
operator = self._operator
454487
compound = self._compound
488+
489+
# For "wi" queries only, open intervals are supported. For "wi" _value
490+
# is a list of two values, with representation from string list form
491+
# of '[a, b]' which corresponds to the standard mathematical notation
492+
# for a closed interval, the default. But an open endpoint is indicated
493+
# by a parenthesis, so adjust repr. to convert square bracket(s).
494+
repr_value = str(self._value)
495+
if self.open_lower:
496+
repr_value = "(" + repr_value[1:]
497+
498+
499+
if self.open_upper:
500+
repr_value = repr_value[:-1] + ")"
501+
455502
if not compound:
456-
out = f"{attr}({operator} {self._value!s}"
503+
out = f"{attr}({operator} {repr_value}"
457504
rtol = self.rtol
458505
if rtol is not None:
459506
out += f" rtol={rtol}"
@@ -596,6 +643,28 @@ def Units(self):
596643

597644
raise AttributeError(f"{self!r} has indeterminate units")
598645

646+
@property
647+
def open_lower(self):
648+
"""True if the interval is open at the (excludes the) lower bound.
649+
650+
.. versionadded:: NEXTVERSION
651+
652+
.. seealso:: `open_upper`
653+
654+
"""
655+
return getattr(self, "_open_lower", False)
656+
657+
@property
658+
def open_upper(self):
659+
"""True if the interval is open at the (excludes the) upper bound.
660+
661+
.. versionadded:: NEXTVERSION
662+
663+
.. seealso:: `open_lower`
664+
665+
"""
666+
return getattr(self, "_open_upper", False)
667+
599668
@property
600669
def rtol(self):
601670
"""The tolerance on relative numerical differences.
@@ -644,8 +713,7 @@ def value(self):
644713
return value
645714

646715
def addattr(self, attr):
647-
"""Return a `Query` object with a new left hand side operand
648-
attribute to be used during evaluation. TODO.
716+
"""Redefine the query to be on an object's attribute.
649717
650718
If another attribute has previously been specified, then the new
651719
attribute is considered to be an attribute of the existing
@@ -803,6 +871,8 @@ def equals(self, other, verbose=None, traceback=False):
803871
"_operator",
804872
"_rtol",
805873
"_atol",
874+
"_open_lower",
875+
"_open_upper",
806876
):
807877
x = getattr(self, attr, None)
808878
y = getattr(other, attr, None)
@@ -905,7 +975,17 @@ def _evaluate(self, x, parent_attr):
905975
if _wi is not None:
906976
return _wi(value)
907977

908-
return (x >= value[0]) & (x <= value[1])
978+
if self.open_lower:
979+
lower_bound = x > value[0]
980+
else:
981+
lower_bound = x >= value[0]
982+
983+
if self.open_upper:
984+
upper_bound = x < value[1]
985+
else:
986+
upper_bound = x <= value[1]
987+
988+
return lower_bound & upper_bound
909989

910990
if operator == "eq":
911991
try:
@@ -1629,9 +1709,21 @@ def isclose(value, units=None, attr=None, rtol=None, atol=None):
16291709
)
16301710

16311711

1632-
def wi(value0, value1, units=None, attr=None):
1712+
def wi(
1713+
value0,
1714+
value1,
1715+
units=None,
1716+
attr=None,
1717+
open_lower=False,
1718+
open_upper=False,
1719+
):
16331720
"""A `Query` object for a "within a range" condition.
16341721
1722+
The condition is a closed interval by default, inclusive of
1723+
both the endpoints, but can be made open or half-open to exclude
1724+
the endpoints on either end with use of the `open_lower` and
1725+
`open_upper` parameters.
1726+
16351727
.. seealso:: `cf.contains`, `cf.eq`, `cf.ge`, `cf.gt`, `cf.ne`,
16361728
`cf.le`, `cf.lt`, `cf.set`, `cf.wo`, `cf.isclose`
16371729
@@ -1643,6 +1735,22 @@ def wi(value0, value1, units=None, attr=None):
16431735
value1:
16441736
The upper bound of the range.
16451737
1738+
open_lower: `bool`, optional
1739+
If True, open the interval at the lower
1740+
bound so that value0 is excluded from the
1741+
range. By default the interval is closed
1742+
so that value0 is included.
1743+
1744+
.. versionadded:: NEXTVERSION
1745+
1746+
open_upper: `bool`, optional
1747+
If True, open the interval at the upper
1748+
bound so that value1 is excluded from the
1749+
range. By default the interval is closed
1750+
so that value1 is included.
1751+
1752+
.. versionadded:: NEXTVERSION
1753+
16461754
units: `str` or `Units`, optional
16471755
The units of *value*. By default, the same units as the
16481756
operand being tested are assumed, if applicable. If
@@ -1671,9 +1779,42 @@ def wi(value0, value1, units=None, attr=None):
16711779
True
16721780
>>> q.evaluate(4)
16731781
False
1782+
>>> q.evaluate(5)
1783+
True
1784+
>>> q.evaluate(7)
1785+
True
1786+
1787+
The interval can be made open on either side or both. Note that,
1788+
as per mathematical interval notation, square brackets indicate
1789+
closed endpoints and parentheses open endpoints in the representation:
1790+
1791+
>>> q = cf.wi(5, 7, open_upper=True)
1792+
>>> q
1793+
<CF Query: (wi [5, 7))>
1794+
>>> q.evaluate(7)
1795+
False
1796+
>>> q = cf.wi(5, 7, open_lower=True)
1797+
>>> q
1798+
<CF Query: (wi (5, 7])>
1799+
>>> q.evaluate(5)
1800+
False
1801+
>>> q = cf.wi(5, 7, open_lower=True, open_upper=True)
1802+
>>> q
1803+
<CF Query: (wi (5, 7))>
1804+
>>> q.evaluate(5)
1805+
False
1806+
>>> q.evaluate(7)
1807+
False
16741808
16751809
"""
1676-
return Query("wi", [value0, value1], units=units, attr=attr)
1810+
return Query(
1811+
"wi",
1812+
[value0, value1],
1813+
units=units,
1814+
attr=attr,
1815+
open_lower=open_lower,
1816+
open_upper=open_upper,
1817+
)
16771818

16781819

16791820
def wo(value0, value1, units=None, attr=None):
@@ -2466,10 +2607,6 @@ def seasons(n=4, start=12):
24662607
.. seealso:: `cf.year`, `cf.month`, `cf.day`, `cf.hour`, `cf.minute`,
24672608
`cf.second`, `cf.djf`, `cf.mam`, `cf.jja`, `cf.son`
24682609
2469-
TODO
2470-
2471-
.. seealso:: `cf.mam`, `cf.jja`, `cf.son`, `cf.djf`
2472-
24732610
:Parameters:
24742611
24752612
n: `int`, optional

0 commit comments

Comments
 (0)