Brace expansion using ranges: Difference between revisions

Content added Content deleted
m (→‎{{header|Phix}}: comment -> use new requires() builtin)
(Added a Python implementation)
Line 1,284: Line 1,284:
stops after endpoint-05.txt
stops after endpoint-05.txt
stops after endpoint-08.txt
stops after endpoint-08.txt
</pre>

=={{header|Python}}==
<lang python>
"""Brace range expansion. Requires Python >=3.6.

Here we use regular expressions for parsing and take an object orientated approach
to expansion of range expressions.

NOTE: With my current version of bash (GNU bash, version 5.0.3(1)-release), a ``-``
or ``+`` character in front of a `step` has no effect. This implementation reverses
the range if a ``-`` immediately precedes a step, and does not recognize range
expressions that use a ``+``.

NOTE: This implementation supports stepped ordinal range expressions.
"""

from __future__ import annotations

import itertools
import re

from abc import ABC
from abc import abstractmethod

from typing import Iterable
from typing import Optional


RE_SPEC = [
(
"INT_RANGE",
r"\{(?P<int_start>[0-9]+)..(?P<int_stop>[0-9]+)(?:(?:..)?(?P<int_step>-?[0-9]+))?}",
),
(
"ORD_RANGE",
r"\{(?P<ord_start>[^0-9])..(?P<ord_stop>[^0-9])(?:(?:..)?(?P<ord_step>-?[0-9]+))?}",
),
(
"LITERAL",
r".+?(?=\{|$)",
),
]


RE_EXPRESSION = re.compile(
"|".join(rf"(?P<{name}>{pattern})" for name, pattern in RE_SPEC)
)


class Expression(ABC):
"""Brace expression abstract base class."""

@abstractmethod
def expand(self, prefix: str) -> Iterable[str]:
pass


class Literal(Expression):
"""An expression literal."""

def __init__(self, value: str):
self.value = value

def expand(self, prefix: str) -> Iterable[str]:
return [f"{prefix}{self.value}"]


class IntRange(Expression):
"""An integer range expression."""

def __init__(
self, start: int, stop: int, step: Optional[int] = None, zfill: int = 0
):
self.start, self.stop, self.step = fix_range(start, stop, step)
self.zfill = zfill

def expand(self, prefix: str) -> Iterable[str]:
return (
f"{prefix}{str(i).zfill(self.zfill)}"
for i in range(self.start, self.stop, self.step)
)


class OrdRange(Expression):
"""An ordinal range expression."""

def __init__(self, start: str, stop: str, step: Optional[int] = None):
self.start, self.stop, self.step = fix_range(ord(start), ord(stop), step)

def expand(self, prefix: str) -> Iterable[str]:
return (f"{prefix}{chr(i)}" for i in range(self.start, self.stop, self.step))


def expand(expressions: Iterable[Expression]) -> Iterable[str]:
"""Expand a sequence of ``Expression``s. Each expression builds on the results
of the expressions that come before it in the sequence."""
expanded = [""]

for expression in expressions:
expanded = itertools.chain.from_iterable(
[expression.expand(prefix) for prefix in expanded]
)

return expanded


def zero_fill(start, stop) -> int:
"""Return the target zero padding width."""

def _zfill(s):
if len(s) <= 1 or not s.startswith("0"):
return 0
return len(s)

return max(_zfill(start), _zfill(stop))


def fix_range(start, stop, step):
"""Transform start, stop and step so that we can pass them to Python's
built-in ``range`` function."""
if not step:
# Zero or None. Explicit zero gets changed to default.
if start <= stop:
# Default step for ascending ranges.
step = 1
else:
# Default step for descending ranges.
step = -1

elif step < 0:
# A negative step means we reverse the range.
start, stop = stop, start
step = abs(step)

elif start > stop:
# A descending range with explicit step.
step = -step

# Don't overshoot or fall short.
if (start - stop) % step == 0:
stop += step

return start, stop, step


def parse(expression: str) -> Iterable[Expression]:
"""Generate a sequence of ``Expression``s from the given range expression."""
for match in RE_EXPRESSION.finditer(expression):
kind = match.lastgroup

if kind == "INT_RANGE":
start = match.group("int_start")
stop = match.group("int_stop")
step = match.group("int_step")
zfill = zero_fill(start, stop)

if step is not None:
step = int(step)

yield IntRange(int(start), int(stop), step, zfill=zfill)

elif kind == "ORD_RANGE":
start = match.group("ord_start")
stop = match.group("ord_stop")
step = match.group("ord_step")

if step is not None:
step = int(step)

yield OrdRange(start, stop, step)

elif kind == "LITERAL":
yield Literal(match.group())


def examples():
cases = [
r"simpleNumberRising{1..3}.txt",
r"steppedNumberRising{1..6..2}.txt",
r"steppedNumberDescending{20..9..2}.txt",
r"simpleAlphaDescending-{Z..X}.txt",
r"steppedDownAndPadded-{10..00..5}.txt",
r"minusSignFlipsSequence {030..20..-5}.txt",
r"combined-{Q..P}{2..1}.txt",
r"emoji{🌵..🌶}{🌽..🌾}etc",
r"li{teral",
r"rangeless{random}string",
r"rangeless{}empty",
r"steppedAlphaDescending-{Z..M..2}.txt",
r"reversedSteppedAlphaDescending-{Z..M..-2}.txt",
]

for case in cases:
print(f"{case} ->")
expressions = parse(case)

for itm in expand(expressions):
print(f"{' '*4}{itm}")

print("") # Blank line between cases


if __name__ == "__main__":
examples()
</lang>

{{out}}
<pre>
simpleNumberRising{1..3}.txt ->
simpleNumberRising1.txt
simpleNumberRising2.txt
simpleNumberRising3.txt

steppedNumberRising{1..6..2}.txt ->
steppedNumberRising1.txt
steppedNumberRising3.txt
steppedNumberRising5.txt

steppedNumberDescending{20..9..2}.txt ->
steppedNumberDescending20.txt
steppedNumberDescending18.txt
steppedNumberDescending16.txt
steppedNumberDescending14.txt
steppedNumberDescending12.txt
steppedNumberDescending10.txt

simpleAlphaDescending-{Z..X}.txt ->
simpleAlphaDescending-Z.txt
simpleAlphaDescending-Y.txt
simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt ->
steppedDownAndPadded-10.txt
steppedDownAndPadded-05.txt
steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt ->
minusSignFlipsSequence 020.txt
minusSignFlipsSequence 025.txt
minusSignFlipsSequence 030.txt

combined-{Q..P}{2..1}.txt ->
combined-Q2.txt
combined-Q1.txt
combined-P2.txt
combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc ->
emoji🌵🌽etc
emoji🌵🌾etc
emoji🌶🌽etc
emoji🌶🌾etc

li{teral ->
li{teral

rangeless{random}string ->
rangeless{random}string

rangeless{}empty ->
rangeless{}empty

steppedAlphaDescending-{Z..M..2}.txt ->
steppedAlphaDescending-Z.txt
steppedAlphaDescending-X.txt
steppedAlphaDescending-V.txt
steppedAlphaDescending-T.txt
steppedAlphaDescending-R.txt
steppedAlphaDescending-P.txt
steppedAlphaDescending-N.txt

reversedSteppedAlphaDescending-{Z..M..-2}.txt ->
reversedSteppedAlphaDescending-M.txt
reversedSteppedAlphaDescending-O.txt
reversedSteppedAlphaDescending-Q.txt
reversedSteppedAlphaDescending-S.txt
reversedSteppedAlphaDescending-U.txt
reversedSteppedAlphaDescending-W.txt
reversedSteppedAlphaDescending-Y.txt
</pre>
</pre>