Skip to content

Commit 39ab3f5

Browse files
committed
from_string now allows singleton intervals to be expressed more compactly like, e.g., "{[0]}".
The string representation of Gaps uses above compactification.
1 parent 5e856c3 commit 39ab3f5

File tree

5 files changed

+40
-22
lines changed

5 files changed

+40
-22
lines changed

src/mind_the_gaps/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66
__all__ = ["Endpoint", "Gaps", "Var", "x"]
77

8-
__version__ = "0.4.0"
8+
__version__ = "0.5.0"

src/mind_the_gaps/gaps.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections.abc import Callable
55
from dataclasses import dataclass, field
66
from functools import total_ordering
7+
from itertools import pairwise
78
from operator import and_, attrgetter, or_, xor
89
from typing import Any, Final, Literal, Protocol, Self
910

@@ -166,6 +167,13 @@ def _merge[T](
166167
return endpoints
167168

168169

170+
def _to_number(text: str) -> int | float:
171+
try:
172+
return int(text)
173+
except ValueError:
174+
return float(text)
175+
176+
169177
@dataclass
170178
class Gaps[T: SupportsLessThan]:
171179
"""A set of mutually exclusive continuous intervals.
@@ -208,36 +216,36 @@ def __post_init__(self) -> None:
208216
i += 1
209217

210218
@classmethod
211-
def from_string(cls, gaps: str) -> Self:
219+
def from_string(cls, gaps: str) -> Self[int | float]:
212220
"""Create gaps from a string.
213221
214-
Values can only be int or float. Uses standard interval notation, i.e., `"{(-inf, 1], [2, 3)}"`.
222+
Values can only be int or float. Uses standard interval notation, i.e.,
223+
`"{(-inf, 1], [2, 3)}"`. If the start and end value of an interval are equal,
224+
the interval may be expressed as, e.g., `"{[0]}"`.
215225
"""
216226
if gaps[0] != "{" or gaps[-1] != "}":
217227
raise ValueError(
218228
"Gap string must start and end with curly braces ('{', '}')."
219229
)
220230

221-
endpoints = gaps[1:-1].replace(" ", "").split(",")
222-
if len(endpoints) == 1:
231+
splits = gaps[1:-1].replace(" ", "").split(",")
232+
if len(splits) == 1 and splits[0] == "":
223233
return cls([])
224234

225-
for i, endpoint in enumerate(endpoints):
226-
if endpoint.startswith(("(", "[")):
227-
boundary = endpoint[0]
228-
value = endpoint[1:]
229-
elif endpoint.endswith((")", "]")):
230-
boundary = endpoint[-1]
231-
value = endpoint[:-1]
235+
endpoints: list[Endpoint[int | float]] = []
236+
for split in splits:
237+
if split.startswith("[") and split.endswith("]"):
238+
value = _to_number(split[1:-1])
239+
endpoints.append(Endpoint(value, "["))
240+
endpoints.append(Endpoint(value, "]"))
241+
elif split.startswith(("(", "[")):
242+
value = _to_number(split[1:])
243+
endpoints.append(Endpoint(value, split[0]))
244+
elif split.endswith((")", "]")):
245+
value = _to_number(split[:-1])
246+
endpoints.append(Endpoint(value, split[-1]))
232247
else:
233-
raise ValueError(f"Invalid endpoint ({endpoint!r}).")
234-
235-
try:
236-
value = int(value)
237-
except ValueError:
238-
value = float(value)
239-
240-
endpoints[i] = Endpoint(value, boundary)
248+
raise ValueError(f"Invalid endpoint ({split!r}).")
241249

242250
return cls(endpoints)
243251

@@ -282,4 +290,12 @@ def __contains__(self, value: T) -> bool:
282290
return False
283291

284292
def __str__(self) -> str:
285-
return f"{{{", ".join(str(endpoint) for endpoint in self.endpoints)}}}"
293+
endpoints = []
294+
for a, b in pairwise(self.endpoints):
295+
if a == b:
296+
endpoints.append(f"[{a.value}]")
297+
else:
298+
endpoints.append(str(a))
299+
if a != b:
300+
endpoints.append(str(b))
301+
return f"{{{", ".join(endpoints)}}}"

tests/test_extended.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"{(2, 3), (6, 7]}",
3131
"{[2, 4), (7, 8]}",
3232
"{}",
33-
"{[2]}",
33+
"{}",
3434
"{(1, 4), (5, 7)}",
3535
"{(1, 2], (3, 5)}",
3636
"{[4, 4]}",

tests/test_from_string.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_from_string_half_open():
2929

3030
def test_from_string_singleton():
3131
assert Gaps.from_string("{[0, 0]}") == Gaps([0, 0])
32+
assert Gaps.from_string("{[0]}") == Gaps([0, 0])
3233

3334

3435
def test_bounded_missing_singleton():

tests/test_gap_init.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def test_gap_str():
7272
assert str(Gaps([Endpoint(0, "("), 1])) == "{(0, 1]}"
7373
assert str(Gaps([0, Endpoint(1, ")")])) == "{[0, 1)}"
7474
assert str(Gaps([Endpoint(0, "("), Endpoint(1, ")")])) == "{(0, 1)}"
75+
assert str(Gaps([0, 0])) == "{[0]}"
7576

7677

7778
def test_gap_not_implemented():

0 commit comments

Comments
 (0)