Stop Writing Classes
See Stop writing classes by Jack Diederich. Short summary: if you instantiate a class, use the class once, and then throw it away, (eg, print(PrettySet(data))
) you should just write a function.
def pretty_set(data: set[int]) -> str:
"""Create a string representation of a set of integers, with the integers
in increasing order, and consecutive integers represented by a start value,
two dots, and the end value.
>>> pretty_set({1, 5, 12, 2, 3, 13, 6, 7})
'{1..3, 5..7, 12, 13}'
"""
... implementation here ...
Minor nit
The empty set will return the string {}
which appears to be an empty dictionary, not an empty set.
Readability
Your code needs blank lines before method definitions.
You need to use type hints consistently. In def _seg_to_str(segment: list):
, segment should be list[int]
, and the return type should be -> str
.
groups = itertools.groupby(enumerate(sorted(self)), lambda x: x[1] - x[0])
is very dense and opaque. It returns something like ((1, [(0, 1), (1, 2), (2, 3)]), (3, [(3, 6), (4, 7), (5, 8)]), (4, [(6, 10), (7, 11)]), (6, [(8, 14), (9, 15), (10, 16)]), (7, [(11, 18)]), (8, [(12, 20), (13, 21), (14, 22), (15, 23), (16, 24), (17, 25)]))
, which is an iterable of tuples of an int and a list of tuples of two ints. No comment in sight as to what those mean, so maintainability is low.
Next, you have [list(map(operator.itemgetter(1), g)) for _, g in groups]
, which is making a nested list of lists ... plus for _, g in groups
is extracting the second item of elements of groups, and operator.itemgetter(1)
is also extracting the second item of something. Again, no comment in sight.
Different implementation
The segments = [list(...) for ...]
statement could be turned into a generator to prevent unnecessary construction of the outer loop, but the inner lists all get unnecessarily created. A lighter weight implementation could be made with a function that takes elements one-at-a-time, collect sequential elements, and yield
string of either single elements or ranges. No tuples or lists need to be constructed.
Perhaps:
def pretty_set(data: set[int]) -> str:
"""Create a string representation of a set of integers, with the integers
in increasing order, and consecutive integers represented by a start value,
two dots, and the end value.
>>> pretty_set({1, 5, 12, 2, 3, 13, 6, 7})
'{1..3, 5..7, 12, 13}'
"""
def segments():
it = iter(sorted(data))
start = end = next(it)
for item in it:
if item == end + 1:
end = item
else:
if end > start + 1:
yield f"{start}..{end}"
else:
yield str(start)
if start != end:
yield str(end)
start = item
end = item
if end > start + 1:
yield f"{start}..{end}"
else:
yield str(start)
if start != end:
yield str(end)
if len(data) < 2:
return str(data)
return "{" + ", ".join(segments()) + "}"
data = {1, 2, 3, 6, 7, 8, 10, 11, 14, 15, 16, 18, 20, 21, 22, 23, 24, 25}
print(pretty_set(data))
It is longer, but perhaps it might be more understandable, and possibly a little more efficient.