I'm working on modernizing Rosetta Code's infrastructure. Starting with communications. Please accept this time-limited open invite to RC's Slack.. --Michael Mol (talk) 20:59, 30 May 2020 (UTC)

Brace expansion using ranges

From Rosetta Code
Brace expansion using ranges is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.
Task[edit]

Write and test a function which expands one or more Unix-style numeric and alphabetic range braces embedded in a larger string.

Specification

The brace strings used by Unix shells permit expansion of both:

  1. Recursive comma-separated lists (covered by the related task: Brace_expansion, and can be ignored here)
  2. ordered numeric and alphabetic ranges, which are the object of this task.


The general pattern of brace ranges is:

{<START>..<END>}

and, in more recent shells:

{<START>..<END>..<INCR>}

(See https://wiki.bash-hackers.org/syntax/expansion/brace)


Expandable ranges of this kind can be ascending or descending:

simpleNumber{1..3}.txt
simpleAlpha-{Z..X}.txt

and may have a third INCR element specifying ordinal intervals larger than one. The increment value can be preceded by a - minus sign, but not by a + sign.

The effect of the minus sign is to always to reverse the natural order suggested by the START and END values.

Any level of zero-padding used in either the START or END value of a numeric range is adopted in the expansions.

steppedDownAndPadded-{10..00..5}.txt
minusSignFlipsSequence {030..20..-5}.txt

A single string may contain more than one expansion range:

combined-{Q..P}{2..1}.txt

Alphabetic range values are limited to a single character for START and END but these characters are not confined to the ASCII alphabet.

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

Unmatched braces are simply ignored, as are empty braces, and braces which contain no range (or list).

li{teral
rangeless{}empty
rangeless{random}string
Tests

Generate and display here the expansion of (at least) each of the nine example lines shown above.

The JavaScript implementation below uses parser combinators, aiming to encode a more or less full and legible description of the
<PREAMBLE><AMBLE><POSTSCRIPT>
range brace grammar, but you should use any resource that suggests itself in your language, including parser libraries.

(The grammar of range expansion, unlike that of nested list expansion, is not recursive, so even regular expressions should prove serviceable here).

The output of the JS implementation, which aims to match the brace expansion behaviour of the default zsh shell on macOS Catalina is:

simpleNumberRising{1..3}.txt -> 
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.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{}empty -> 
    rangeless{}empty

rangeless{random}string -> 
    rangeless{random}string
Related tasks



Go[edit]

Translation of: Wren
package main
 
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
 
func parseRange(r string) []string {
if r == "" {
return []string{"{}"} // rangeless, empty
}
sp := strings.Split(r, "..")
if len(sp) == 1 {
return []string{"{" + r + "}"} // rangeless, random value
}
sta := sp[0]
end := sp[1]
inc := "1"
if len(sp) > 2 {
inc = sp[2]
}
n1, ok1 := strconv.Atoi(sta)
n2, ok2 := strconv.Atoi(end)
n3, ok3 := strconv.Atoi(inc)
if ok3 != nil {
return []string{"{" + r + "}"} // increment isn't a number
}
numeric := (ok1 == nil) && (ok2 == nil)
if !numeric {
if (ok1 == nil && ok2 != nil) || (ok1 != nil && ok2 == nil) {
return []string{"{" + r + "}"} // mixed numeric/alpha not expanded
}
if utf8.RuneCountInString(sta) != 1 || utf8.RuneCountInString(end) != 1 {
return []string{"{" + r + "}"} // start/end are not both single alpha
}
n1 = int(([]rune(sta))[0])
n2 = int(([]rune(end))[0])
}
width := 1
if numeric {
if len(sta) < len(end) {
width = len(end)
} else {
width = len(sta)
}
}
if n3 == 0 { // zero increment
if numeric {
return []string{fmt.Sprintf("%0*d", width, n1)}
} else {
return []string{sta}
}
}
var res []string
asc := n1 < n2
if n3 < 0 {
asc = !asc
n1, n2 = n2, n1
n3 = -n3
}
i := n1
if asc {
for ; i <= n2; i += n3 {
if numeric {
res = append(res, fmt.Sprintf("%0*d", width, i))
} else {
res = append(res, string(rune(i)))
}
}
} else {
for ; i >= n2; i -= n3 {
if numeric {
res = append(res, fmt.Sprintf("%0*d", width, i))
} else {
res = append(res, string(rune(i)))
}
}
}
return res
}
 
func rangeExpand(s string) []string {
res := []string{""}
rng := ""
inRng := false
for _, c := range s {
if c == '{' && !inRng {
inRng = true
rng = ""
} else if c == '}' && inRng {
rngRes := parseRange(rng)
rngLen := len(rngRes)
var res2 []string
for i := 0; i < len(res); i++ {
for j := 0; j < rngLen; j++ {
res2 = append(res2, res[i]+rngRes[j])
}
}
res = res2
inRng = false
} else if inRng {
rng += string(c)
} else {
for i := 0; i < len(res); i++ {
res[i] += string(c)
}
}
}
if inRng {
for i := 0; i < len(res); i++ {
res[i] += "{" + rng // unmatched braces
}
}
return res
}
 
func main() {
examples := []string{
"simpleNumberRising{1..3}.txt",
"simpleAlphaDescending-{Z..X}.txt",
"steppedDownAndPadded-{10..00..5}.txt",
"minusSignFlipsSequence {030..20..-5}.txt",
"combined-{Q..P}{2..1}.txt",
"emoji{🌵..🌶}{🌽..🌾}etc",
"li{teral",
"rangeless{}empty",
"rangeless{random}string",
"mixedNumberAlpha{5..k}",
"steppedAlphaRising{P..Z..2}.txt",
"stops after endpoint-{02..10..3}.txt",
}
for _, s := range examples {
fmt.Print(s, "->\n ")
res := rangeExpand(s)
fmt.Println(strings.Join(res, "\n "))
fmt.Println()
}
}
Output:
Same as Wren entry.

JavaScript[edit]

(() => {
'use strict';
 
// --------------- BRACE-RANGE EXPANSION ---------------
 
// braceExpandWithRange :: String -> [String]
const braceExpandWithRange = s => {
// A list containing either the expansions
// of s, if there are any, or s itself.
const
expansions = parse(some(
braceRangeExpansion()
))(s);
return 0 < expansions.length ? (() => {
const [parsed, residue] = Array.from(
expansions[0]
);
return suffixAdd(
parsed.reduce(
uncurry(suffixMultiply),
['']
)
)([residue.join('')])
})() : [s];
};
 
 
// ----------- BRACE-RANGE EXPANSION PARSER ------------
 
// braceRangeExpansion :: [String]
const braceRangeExpansion = () =>
// List of strings expanded from a
// a unix shell {<START>..<END>} or
// {<START>..<END>..<INCR>} expression.
// See https://wiki.bash-hackers.org/syntax/expansion/brace
fmapP(([preamble, amble, postscript]) =>
suffixAdd(
suffixMultiply(preamble)(amble)
)(postscript)
)(sequenceP([
affixLeaf(),
fmapP(xs => [xs])(
between(char('{'))(char('}'))(
altP(
numericSequence()
)(
characterSequence()
)
)
),
affixLeaf()
]));
 
 
// ----------------------- TESTS -----------------------
// main :: IO ()
const main = () => {
const tests = [
'simpleNumberRising{1..3}.txt',
'simpleAlphaDescending-{Z..X}.txt',
'steppedDownAndPadded-{10..00..5}.txt',
'minusSignFlipsSequence {030..20..-5}.txt',
'combined-{Q..P}{2..1}.txt',
'emoji{🌵..🌶}{🌽..🌾}etc',
'li{teral',
'rangeless{}empty',
'rangeless{random}string'
];
 
return tests.map(
s => s + ' -> ' + '\n\t' + (
braceExpandWithRange(s).join('\n\t')
)
).join('\n\n');
};
 
 
// ----------- BRACE-RANGE COMPONENT PARSERS -----------
 
// affixLeaf :: () -> Parser String
const affixLeaf = () =>
// A sequence of literal (non-syntactic)
// characters before or after a pair of braces.
fmapP(cs => [
[cs.join('')]
])(
many(choice([noneOf('{\\'), escape()]))
);
 
 
// characterSequence :: () -> Parser [Char]
const characterSequence = () =>
// A rising or descending alphabetic
// sequence of characters.
fmapP(ab => {
const [from, to] = ab;
return from !== to ? (
enumFromThenToChar(from)(
(from < to ? succ : pred)(from)
)(to)
) : [from];
})(
ordinalRange(satisfy(
c => !'0123456789'.includes(c)
))
);
 
 
// enumerationList :: ((Bool, String), String) ->
// ((Bool, String), String) ->
// ((Bool, String), String) -> [String]
const enumerationList = triple => {
// An ordered list of numeric strings either
// rising or descending, in numeric order, and
// possibly prefixed with zeros.
const
w = padWidth(triple[0][1])(triple[1][1]),
[from, to, by] = triple.map(
sn => (sn[0] ? negate : identity)(
parseInt(sn[1])
)
);
return map(
compose(justifyRight(w)('0'), str)
)(
0 > by ? (
enumFromThenTo(to)(to - by)(from)
) : enumFromThenTo(from)(
from + (to >= from ? by : -by)
)(to)
);
};
 
 
// numericPart :: () -> Parser (Bool, String)
const numericPart = () =>
// The Bool is True if the string is
// negated by a leading '-'
// The String component contains the digits.
bindP(
option('')(char('-'))
)(sign => bindP(
some(digit())
)(ds => pureP(
Tuple(Boolean(sign))(concat(ds))
)));
 
 
// numericSequence :: () -> Parser [String]
const numericSequence = () =>
// An ascending or descending sequence
// of numeric strings, possibly
// left-padded with zeros.
fmapP(enumerationList)(sequenceP([
ordinalRange(numericPart()),
numericStep()
]));
 
 
// numericStep :: () -> Parser (Bool, Int)
const numericStep = () =>
// The size of increment for a numeric
// series. Descending if the Bool is True.
// Defaults to (False, 1).
option(Tuple(false)(1))(
bindP(
string('..')
)(_ => bindP(
numericPart()
)(pureP))
);
 
 
// ordinalRange :: Enum a =>
// Parser a -> Parser (a, a)
const ordinalRange = p =>
// A pair of enumerable values of the same
// type, representing the start and end of
// a range.
bindP(
p
)(from => bindP(
string('..')
)(_ => bindP(
p
)(compose(pureP, append([from])))));
 
 
// padWidth :: String -> String -> Int
const padWidth = cs =>
// The length of the first of cs and cs1 to
// start with a zero. Otherwise (if neither
// starts with a zero) then 0.
cs1 => [cs, cs1].reduce(
(a, x) => (0 < a) || (1 > x.length) ? (
a
) : '0' !== x[0] ? a : x.length,
0
);
 
 
// suffixAdd :: [String] -> [String] -> [String]
const suffixAdd = xs =>
ys => xs.flatMap(
flip(append)(ys)
);
 
 
// suffixMultiply :: [String] -> [String] -> [String]
const suffixMultiply = xs =>
apList(xs.map(append));
 
 
// ------------ GENERIC PARSER COMBINATORS -------------
 
// Parser :: String -> [(a, String)] -> Parser a
const Parser = f =>
// A function lifted into a Parser object.
({
type: 'Parser',
parser: f
});
 
 
// altP (<|>) :: Parser a -> Parser a -> Parser a
const altP = p =>
// p, or q if p doesn't match.
q => Parser(s => {
const xs = parse(p)(s);
return 0 < xs.length ? (
xs
) : parse(q)(s);
});
 
 
// apP <*> :: Parser (a -> b) -> Parser a -> Parser b
const apP = pf =>
// A new parser obtained by the application
// of a Parser-wrapped function,
// to a Parser-wrapped value.
p => Parser(
s => parse(pf)(s).flatMap(
vr => parse(
fmapP(vr[0])(p)
)(vr[1])
)
);
 
 
// between :: Parser open -> Parser close ->
// Parser a -> Parser a
const between = pOpen =>
// A version of p which matches between
// pOpen and pClose (both discarded).
pClose => p => bindP(
pOpen
)(_ => bindP(
p
)(x => bindP(
pClose
)(_ => pureP(x))));
 
 
// bindP (>>=) :: Parser a ->
// (a -> Parser b) -> Parser b
const bindP = p =>
// A new parser obtained by the application of
// a function to a Parser-wrapped value.
// The function must enrich its output, lifting it
// into a new Parser.
// Allows for the nesting of parsers.
f => Parser(
s => parse(p)(s).flatMap(
tpl => parse(f(tpl[0]))(tpl[1])
)
);
 
 
// char :: Char -> Parser Char
const char = x =>
// A particular single character.
satisfy(c => x == c);
 
 
// choice :: [Parser a] -> Parser a
const choice = ps =>
// A parser constructed from a
// (left to right) list of alternatives.
ps.reduce(uncurry(altP), emptyP());
 
 
// digit :: Parser Char
const digit = () =>
// A single digit.
satisfy(isDigit);
 
 
// emptyP :: () -> Parser a
const emptyP = () =>
// The empty list.
Parser(_ => []);
 
 
// escape :: Parser String
const escape = () =>
fmapP(xs => xs.join(''))(
sequenceP([char('\\'), item()])
);
 
 
// fmapP :: (a -> b) -> Parser a -> Parser b
const fmapP = f =>
// A new parser derived by the structure-preserving
// application of f to the value in p.
p => Parser(
s => parse(p)(s).flatMap(
vr => Tuple(f(vr[0]))(vr[1])
)
);
 
 
// item :: () -> Parser Char
const item = () =>
// A single character.
Parser(
s => 0 < s.length ? [
Tuple(s[0])(
s.slice(1)
)
] : []
);
 
 
// liftA2P :: (a -> b -> c) ->
// Parser a -> Parser b -> Parser c
const liftA2P = op =>
// The binary function op, lifted
// to a function over two parsers.
p => apP(fmapP(op)(p));
 
 
// many :: Parser a -> Parser [a]
const many = p => {
// Zero or more instances of p.
// Lifts a parser for a simple type of value
// to a parser for a list of such values.
const some_p = p =>
liftA2P(
x => xs => [x].concat(xs)
)(p)(many(p));
return Parser(
s => parse(
0 < s.length ? (
altP(some_p(p))(pureP([]))
) : pureP([])
)(s)
);
};
 
 
// noneOf :: String -> Parser Char
const noneOf = s =>
// Any character not found in the
// exclusion string.
satisfy(c => !s.includes(c));
 
 
// option :: a -> Parser a -> Parser a
const option = x =>
// Either p or the default value x.
p => altP(p)(pureP(x));
 
 
// parse :: Parser a -> String -> [(a, String)]
const parse = p =>
// The result of parsing s with p.
s => {
//showLog('s', s)
return p.parser([...s]);
};
 
 
// pureP :: a -> Parser a
const pureP = x =>
// The value x lifted, unchanged,
// into the Parser monad.
Parser(s => [Tuple(x)(s)]);
 
 
// satisfy :: (Char -> Bool) -> Parser Char
const satisfy = test =>
// Any character for which the
// given predicate returns true.
Parser(
s => 0 < s.length ? (
test(s[0]) ? [
Tuple(s[0])(s.slice(1))
] : []
) : []
);
 
 
// sepBy1 :: Parser a -> Parser b -> Parser [a]
const sepBy1 = p =>
// One or more occurrences of p, as
// separated by (discarded) instances of sep.
sep => bindP(
p
)(x => bindP(
many(bindP(
sep
)(_ => bindP(
p
)(pureP))))(
xs => pureP([x].concat(xs))));
 
 
// sequenceP :: [Parser a] -> Parser [a]
const sequenceP = ps =>
// A single parser for a list of values, derived
// from a list of parsers for single values.
Parser(
s => ps.reduce(
(a, q) => a.flatMap(
vr => parse(q)(snd(vr)).flatMap(
first(xs => fst(vr).concat(xs))
)
),
[Tuple([])(s)]
)
);
 
 
// some :: Parser a -> Parser [a]
const some = p => {
// One or more instances of p.
// Lifts a parser for a simple type of value
// to a parser for a list of such values.
const many_p = p =>
altP(some(p))(pureP([]));
return Parser(
s => parse(
liftA2P(
x => xs => [x].concat(xs)
)(p)(many_p(p))
)(s)
);
};
 
 
// string :: String -> Parser String
const string = s =>
// A particular string.
fmapP(cs => cs.join(''))(
sequenceP([...s].map(char))
);
 
 
// ----------------- GENERAL FUNCTIONS -----------------
 
// Tuple (,) :: a -> b -> (a, b)
const Tuple = a =>
b => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
 
 
// apList (<*>) :: [(a -> b)] -> [a] -> [b]
const apList = fs =>
// The sequential application of each of a list
// of functions to each of a list of values.
// apList([x => 2 * x, x => 20 + x])([1, 2, 3])
// -> [2, 4, 6, 21, 22, 23]
xs => fs.flatMap(f => xs.map(f));
 
 
// append (++) :: [a] -> [a] -> [a]
const append = xs =>
// A list obtained by the
// concatenation of two others.
ys => xs.concat(ys);
 
 
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
// A function defined by the right-to-left
// composition of all the functions in fs.
fs.reduce(
(f, g) => x => f(g(x)),
x => x
);
 
 
// concat :: [[a]] -> [a]
// concat :: [String] -> String
const concat = xs => (
ys => 0 < ys.length ? (
ys.every(Array.isArray) ? (
[]
) : ''
).concat(...ys) : ys
)(list(xs));
 
 
// enumFromThenTo :: Int -> Int -> Int -> [Int]
const enumFromThenTo = x1 =>
x2 => y => {
const d = x2 - x1;
return Array.from({
length: Math.floor(y - x2) / d + 2
}, (_, i) => x1 + (d * i));
};
 
 
// enumFromThenToChar :: Char -> Char -> Char -> [Char]
const enumFromThenToChar = x1 =>
x2 => y => {
const [i1, i2, iY] = Array.from([x1, x2, y])
.map(x => x.codePointAt(0)),
d = i2 - i1;
return Array.from({
length: (Math.floor(iY - i2) / d) + 2
}, (_, i) => String.fromCodePoint(i1 + (d * i)));
};
 
 
// first :: (a -> b) -> ((a, c) -> (b, c))
const first = f =>
// A simple function lifted to one which applies
// to a tuple, transforming only its first item.
xy => Tuple(f(xy[0]))(
xy[1]
);
 
 
// flip :: (a -> b -> c) -> b -> a -> c
const flip = op =>
// The binary function op with
// its arguments reversed.
1 < op.length ? (
(a, b) => op(b, a)
) : (x => y => op(y)(x));
 
 
// fst :: (a, b) -> a
const fst = tpl =>
// First member of a pair.
tpl[0];
 
 
// fromEnum :: Enum a => a -> Int
const fromEnum = x =>
typeof x !== 'string' ? (
x.constructor === Object ? (
x.value
) : parseInt(Number(x))
) : x.codePointAt(0);
 
 
// identity :: a -> a
const identity = x =>
// The identity function. (`id`, in Haskell)
x;
 
 
// isAlpha :: Char -> Bool
const isAlpha = c =>
/[A-Za-z\u00C0-\u00FF]/.test(c);
 
 
// isDigit :: Char -> Bool
const isDigit = c => {
const n = c.codePointAt(0);
return 48 <= n && 57 >= n;
};
 
 
// justifyRight :: Int -> Char -> String -> String
const justifyRight = n =>
// The string s, preceded by enough padding (with
// the character c) to reach the string length n.
c => s => n > s.length ? (
s.padStart(n, c)
) : s;
 
 
// list :: StringOrArrayLike b => b -> [a]
const list = xs =>
// xs itself, if it is an Array,
// or an Array derived from xs.
Array.isArray(xs) ? (
xs
) : Array.from(xs || []);
 
 
// map :: (a -> b) -> [a] -> [b]
const map = f =>
// The list obtained by applying f
// to each element of xs.
// (The image of xs under f).
xs => [...xs].map(f);
 
 
// maxBound :: a -> a
const maxBound = x => {
const e = x.enum;
return Boolean(e) ? (
e[e[x.max]]
) : {
'number': Number.MAX_SAFE_INTEGER,
'string': String.fromCodePoint(0x10FFFF),
'boolean': true
} [typeof x];
};
 
// minBound :: a -> a
const minBound = x => {
const e = x.enum;
return Boolean(e) ? (
e[e[0]]
) : {
'number': Number.MIN_SAFE_INTEGER,
'string': String.fromCodePoint(0),
'boolean': false
} [typeof x];
};
 
 
// negate :: Num -> Num
const negate = n =>
-n;
 
 
// pred :: Enum a => a -> a
const pred = x => {
const t = typeof x;
return 'number' !== t ? (() => {
const [i, mn] = [x, minBound(x)].map(fromEnum);
return i > mn ? (
toEnum(x)(i - 1)
) : Error('succ :: enum out of range.');
})() : x > Number.MIN_SAFE_INTEGER ? (
x - 1
) : Error('succ :: Num out of range.');
};
 
 
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
 
 
// snd :: (a, b) -> b
const snd = tpl =>
// Second member of a pair.
tpl[1];
 
 
// str :: a -> String
const str = x =>
Array.isArray(x) && x.every(
v => ('string' === typeof v) && (1 === v.length)
) ? (
x.join('')
) : x.toString();
 
 
// succ :: Enum a => a -> a
const succ = x => {
const t = typeof x;
return 'number' !== t ? (() => {
const [i, mx] = [x, maxBound(x)].map(fromEnum);
return i < mx ? (
toEnum(x)(1 + i)
) : Error('succ :: enum out of range.')
})() : x < Number.MAX_SAFE_INTEGER ? (
1 + x
) : Error('succ :: Num out of range.')
};
 
 
// toEnum :: a -> Int -> a
const toEnum = e =>
// The first argument is a sample of the type
// allowing the function to make the right mapping
x => ({
'number': Number,
'string': String.fromCodePoint,
'boolean': Boolean,
'object': v => e.min + v
} [typeof e])(x);
 
 
// uncurry :: (a -> b -> c) -> ((a, b) -> c)
const uncurry = f =>
// A function over a pair, derived
// from a curried function.
function () {
const
args = arguments,
xy = Boolean(args.length % 2) ? (
args[0]
) : args;
return f(xy[0])(xy[1]);
};
 
// MAIN ---
return main();
})();
Output:
simpleNumberRising{1..3}.txt -> 
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.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{}empty -> 
    rangeless{}empty

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

Julia[edit]

padzeros(str) = (len = length(str)) > 1 && str[1] == '0' ? len : 0
 
function ranged(str)
rang = filter(!isempty, split(str, r"\{|\}|\.\."))
delta = length(rang) > 2 ? parse(Int, rang[3]) : 1
if delta < 0
rang[1], rang[2], delta = rang[2], rang[1], -delta
end
if '0' <= rang[1][1] <= '9' || rang[1][1] == '-'
try x, y = parse(Int, rang[1]), parse(Int, rang[2]) catch; return [str] end
pad = max(padzeros(rang[1]), padzeros(rang[2]))
return [string(x, pad=pad) for x in range(x, step=(x < y) ? delta : -delta, stop=y)]
else
x, y, z = rang[1][end], rang[2][end], rang[1][1:end-1]
return [z * string(x) for x in range(x, step=(x < y) ? delta : -delta, stop=y)]
end
end
 
function splatrange(s)
m = match(r"([^\{]*)(\{[^}]+\.\.[^\}]+\})(.*)", s)
m == nothing && return [s]
c = m.captures
return vec([a * b for b in splatrange(c[3]), a in [c[1] * x for x in ranged(c[2])]])
end
 
for test in [
"simpleNumberRising{1..3}.txt",
"simpleAlphaDescending-{Z..X}.txt",
"steppedDownAndPadded-{10..00..5}.txt",
"minusSignFlipsSequence {030..20..-5}.txt",
"combined-{Q..P}{2..1}.txt",
"emoji{🌵..🌶}{🌽..🌾}etc",
"li{teral",
"rangeless{}empty",
"rangeless{random}string",
"mixedNumberAlpha{5..k}",
"steppedAlphaRising{P..Z..2}.txt",
"stops after endpoint-{02..10..3}.txt",
]
println(test, "->\n", [" " * x * "\n" for x in splatrange(test)]...)
end
 
Output:
simpleNumberRising{1..3}.txt->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.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{}empty->
    rangeless{}empty

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

mixedNumberAlpha{5..k}->
    mixedNumberAlpha{5..k}

steppedAlphaRising{P..Z..2}.txt->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

stops after endpoint-{02..10..3}.txt->
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt

Phix[edit]

Translation of: Wren

Requires 0.8.2+ (is_integer() is new, ==sign(inc) found me a long-buried compiler bug)

function parse_range(string r)
sequence sp = split(r,"..")&{"1"},
res = {}
if length(sp)>=3 then
string {strange,ending,step} = sp
integer inc = to_integer(step)
if inc!=0 then
bool ns = is_integer(strange),
ne = is_integer(ending)
if ns=ne then
if ns then
integer s = to_integer(strange),
e = to_integer(ending),
w = max(length(strange),length(ending))
if inc<0 then {s,e,inc} = {e,s,-inc} end if
if s>e then inc *= -1 end if
integer zfill = (length(strange)>1 and strange[1]='0') or
(length(ending)>1 and ending[1]='0')
string fmt = iff(zfill?sprintf("%%0%dd",{w}):"%d")
for k=s to e by inc do
res = append(res,sprintf(fmt,k))
end for
return res
elsif length(strange)=length(ending) then
bool ok = (length(strange)=1)
if not ok then
object s32 = utf8_to_utf32(strange,-1),
e32 = utf8_to_utf32(ending,-1)
if sequence(s32) and length(s32)=1
and sequence(e32) and length(e32)=1 then
ok = true
end if
end if
if ok then
if strange>ending then inc *= -1 end if
while true do
res = append(res,strange)
integer sdx = length(strange)
while true do
integer ch = strange[sdx]+inc
if ch<=#FF and ch>=#00 then
strange[sdx] = ch
exit
end if
strange[sdx] = iff(inc<0?#FF:#00)
sdx -= 1
end while
if compare(strange,ending)==sign(inc) then exit end if
if length(res)>10 then ?9/0 end if -- (sanity check)
end while
return res
end if -- ([utf8] strings not single char)
end if -- (neither numeric nor same-length alpha)
end if -- (mixed numeric/alpha)
end if -- (non-numeric increment)
end if -- (rangeless)
return {"{"&r&"}"}
end function
 
function range_expand(string s)
sequence res = {""}
string range = ""
bool in_range = false
for k=1 to length(s) do
integer c = s[k]
if c == '{' and not in_range then
in_range = true
range = ""
elsif c == '}' and in_range then
sequence range_res = parse_range(range),
prev_res = res
res = {}
for i=1 to length(prev_res) do
for j=1 to length(range_res) do
res = append(res, prev_res[i] & range_res[j])
end for
end for
in_range = false
elsif in_range then
range &= c
else
for i=1 to length(res) do
res[i] &= c
end for
end if
end for
if in_range then
for i=1 to length(res) do
res[i] &= "{" & range // unmatched braces
end for
end if
return res
end function
 
constant examples = {
"simpleNumberRising{1..3}.txt",
"simpleAlphaDescending-{Z..X}.txt",
"steppedDownAndPadded-{10..00..5}.txt",
"minusSignFlipsSequence {030..20..-5}.txt",
"combined-{Q..P}{2..1}.txt",
"emoji{🌵..🌶}{🌽..🌾}etc",
"multi char emoji ranges fail {🌵🌵..🌵🌶}",
"li{teral",
"rangeless{}empty",
"rangeless{random}string",
"mixedNumberAlpha{5..k}",
"steppedAlphaRising{P..Z..2}.txt",
"stops after endpoint-{02..10..3}.txt"
}
 
for i=1 to length(examples) do
string s = examples[i]
printf(1,"%s ->\n  %s\n",{s,join(range_expand(s),"\n ")})
end for
Output:

Note that, as usual, unicode output does not look good on a windows console for tests 6 & 7 (linux output shown)

simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.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
multi char emoji ranges fail {🌵🌵..🌵🌶} ->
    multi char emoji ranges fail {🌵🌵..🌵🌶}
li{teral ->
    li{teral
rangeless{}empty ->
    rangeless{}empty
rangeless{random}string ->
    rangeless{random}string
mixedNumberAlpha{5..k} ->
    mixedNumberAlpha{5..k}
steppedAlphaRising{P..Z..2}.txt ->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt
stops after endpoint-{02..10..3}.txt ->
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt

Raku[edit]

Works with: Rakudo version 2020.08.1

Also implements some of the string list functions described on the bash-hackers page.

my $range = rx/ '{' $<start> = <-[.]>+? '..' $<end> = <-[.]>+? ['..' $<incr> = ['-'?\d+] ]? '}' /;
my $list = rx/ ^ $<prefix> = .*? '{' (<-[,}]>+) +%% ',' '}' $<postfix> = .* $/;
 
sub expand (Str $string) {
my @return = $string;
if $string ~~ $range {
quietly my ($start, $end, $incr) = $/<start end incr>».Str;
$incr ||= 1;
($end, $start) = $start, $end if $incr < 0;
$incr.=abs;
 
if try all( +$start, +$end ) ~~ Numeric {
$incr = - $incr if $start > $end;
 
my ($sl, $el) = 0, 0;
$sl = $start.chars if $start.starts-with('0');
$el = $end.chars if $end.starts-with('0');
 
my @this = $start < $end ?? (+$start, * + $incr^ * > +$end) !! (+$start, * + $incr^ * < +$end);
@return = @this.map: { $string.subst($range, sprintf("%{'0' ~ max $sl, $el}d", $_) ) }
}
elsif try +$start ~~ Numeric or +$end ~~ Numeric {
return $string #fail
}
else {
my @this;
if $start.chars + $end.chars > 2 {
return $string if $start.succ eq $start or $end.succ eq $end; # fail
@this = $start lt $end ?? ($start, (*.succ xx $incr).tail^ * gt $end) !! ($start, (*.pred xx $incr).tail^ * lt $end);
}
else {
$incr = -$incr if $start gt $end;
@this = $start lt $end ?? ($start, (*.ord + $incr).chr^ * gt $end) !! ($start, (*.ord + $incr).chr^ * lt $end);
}
@return = @this.map: { $string.subst($range, sprintf("%s", $_) ) }
}
}
if $string ~~ $list {
my $these = $/[0]».Str;
my ($prefix, $postfix) = $/<prefix postfix>».Str;
if ($prefix ~ $postfix).chars {
@return = $these.map: { $string.subst($list, $prefix ~ $_ ~ $postfix) } if $these.elems > 1
}
else {
@return = $these.join: ' '
}
}
my $cnt = 1;
while $cnt != [email protected]return {
$cnt = [email protected]return;
@return.=map: { |.&expand }
}
@return
}
 
for qww<
# Required tests
 
simpleNumberRising{1..3}.txt
simpleAlphaDescending-{Z..X}.txt
steppedDownAndPadded-{10..00..5}.txt
minusSignFlipsSequence{030..20..-5}.txt
combined-{Q..P}{2..1}.txt
emoji{🌵..🌶}{🌽..🌾}etc
li{teral
rangeless{}empty
rangeless{random}string
 
# Test some other features
 
'stop point not in sequence-{02..10..3}.txt'
steppedAlphaRising{P..Z..2}.txt
'simple {just,give,me,money} list'
{thatʼs,what,I,want}
'emoji {☃,☄}{★,🇺🇸,☆} lists'
'alphanumeric mix{ab7..ac1}.txt'
'alphanumeric mix{0A..0C}.txt'
 
# fail by design
 
'mixed terms fail {7..C}.txt'
'multi char emoji ranges fail {🌵🌵..🌵🌶}'
> -> $test {
say "$test ->";
say (' ' xx * Z~ expand $test).join: "\n";
say '';
}
 
Output:
simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.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 ->
    minusSignFlipsSequence020.txt
    minusSignFlipsSequence025.txt
    minusSignFlipsSequence030.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{}empty ->
    rangeless{}empty

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

stop point not in sequence-{02..10..3}.txt ->
    stop point not in sequence-02.txt
    stop point not in sequence-05.txt
    stop point not in sequence-08.txt

steppedAlphaRising{P..Z..2}.txt ->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

simple {just,give,me,money} list ->
    simple just list
    simple give list
    simple me list
    simple money list

{thatʼs,what,I,want} ->
    thatʼs what I want

emoji {☃,☄}{★,🇺🇸,☆} lists ->
    emoji ☃★ lists
    emoji ☃🇺🇸 lists
    emoji ☃☆ lists
    emoji ☄★ lists
    emoji ☄🇺🇸 lists
    emoji ☄☆ lists

alphanumeric mix{ab7..ac1}.txt ->
    alphanumeric mixab7.txt
    alphanumeric mixab8.txt
    alphanumeric mixab9.txt
    alphanumeric mixac0.txt
    alphanumeric mixac1.txt

alphanumeric mix{0A..0C}.txt ->
    alphanumeric mix0A.txt
    alphanumeric mix0B.txt
    alphanumeric mix0C.txt

mixed terms fail {7..C}.txt ->
    mixed terms fail {7..C}.txt

multi char emoji ranges fail {🌵🌵..🌵🌶} ->
    multi char emoji ranges fail {🌵🌵..🌵🌶}

Wren[edit]

Library: Wren-fmt

Added three further examples to test:

  • Mixed number/alpha ranges which apparently are not expanded.
  • Stepped alpha ranges which appear to be allowed.
  • Stepped ranges which stop after the endpoint (Raku example).


import "/fmt" for Fmt
 
var parseRange = Fn.new { |r|
if (r == "") return ["{}"] // rangeless, empty
var sp = r.split("..")
if (sp.count == 1) return ["{%(r)}"] // rangeless, random value
var sta = sp[0]
var end = sp[1]
var inc = (sp.count == 2) ? "1" : sp[2]
var n1 = Num.fromString(sta)
var n2 = Num.fromString(end)
var n3 = Num.fromString(inc)
if (!n3) return ["{%(r)}"] // increment isn't a number
var numeric = n1 && n2
if (!numeric) {
if ((n1 && !n2) || (!n1 && n2)) return ["{%(r)}"] // mixed numeric/alpha not expanded
if (sta.count != 1 || end.count != 1) return ["{%(r)}"] // start/end are not both single alpha
n1 = sta.codePoints[0]
n2 = end.codePoints[0]
}
var width = 1
if (numeric) width = (sta.count < end.count) ? end.count : sta.count
if (n3 == 0) return (numeric) ? [Fmt.dz(width, n1)] : [sta] // zero increment
var res = []
var asc = n1 < n2
if (n3 < 0) {
asc = !asc
var t = n1
n1 = n2
n2 = t
n3 = -n3
}
var i = n1
if (asc) {
while (i <= n2) {
res.add( (numeric) ? Fmt.dz(width, i) : String.fromCodePoint(i) )
i = i + n3
}
} else {
while (i >= n2) {
res.add(( numeric) ? Fmt.dz(width, i) : String.fromCodePoint(i) )
i = i - n3
}
}
return res
}
 
var rangeExpand = Fn.new { |s|
var res = [""]
var rng = ""
var inRng = false
for (c in s) {
if (c == "{" && !inRng) {
inRng = true
rng = ""
} else if (c == "}" && inRng) {
var rngRes = parseRange.call(rng)
var rngCount = rngRes.count
var res2 = []
for (i in 0...res.count) {
for (j in 0...rngCount) res2.add(res[i] + rngRes[j])
}
res = res2
inRng = false
} else if (inRng) {
rng = rng + c
} else {
for (i in 0...res.count) res[i] = res[i] + c
}
}
if (inRng) for (i in 0...res.count) res[i] = res[i] + "{" + rng // unmatched braces
return res
}
 
var examples = [
"simpleNumberRising{1..3}.txt",
"simpleAlphaDescending-{Z..X}.txt",
"steppedDownAndPadded-{10..00..5}.txt",
"minusSignFlipsSequence {030..20..-5}.txt",
"combined-{Q..P}{2..1}.txt",
"emoji{🌵..🌶}{🌽..🌾}etc",
"li{teral",
"rangeless{}empty",
"rangeless{random}string",
"mixedNumberAlpha{5..k}",
"steppedAlphaRising{P..Z..2}.txt",
"stops after endpoint-{02..10..3}.txt"
]
 
for (s in examples) {
System.write("%(s) ->\n ")
var res = rangeExpand.call(s)
System.print(res.join("\n "))
System.print()
}
Output:
simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.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{}empty ->
    rangeless{}empty

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

mixedNumberAlpha{5..k} ->
    mixedNumberAlpha{5..k}

steppedAlphaRising{P..Z..2}.txt ->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

stops after endpoint-{02..10..3}.txt ->
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt