Brace expansion using ranges
Write and test a function which expands one or more Unix-style numeric and alphabetic range braces embedded in a larger string.
The brace strings used by Unix shells permit expansion of both:
- Recursive comma-separated lists (covered by the related task: Brace_expansion, and can be ignored here)
- 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
Generate and display here the expansion of 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
<lang go>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() }
}</lang>
- Output:
Same as Wren entry.
JavaScript
<lang javascript>(() => {
'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();
})();</lang>
- 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
Julia currently does not handle certain trigraphs in strings correctly, such as 🌶, so that test is omitted. <lang julia>padzeros(str) = (len = length(str)) > 1 && str[1] == '0' ? len : 0
function ranged(str)
rang = split(str[2:end-1], "..") 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", "li{teral", "rangeless{}empty", "rangeless{random}string", ] println(test, "->\n", [" " * x * "\n" for x in splatrange(test)]...)
end
</lang>
- 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 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
Also implements some of the string list functions described on the bash-hackers page.
<lang perl6>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 +$start ~~ Numeric and +$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 $start !~~ /\d/ and $end !~~ /\d/ { $incr = - $incr if $start gt $end;
my @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: ' ' } } @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
# Emoji lists emoji{☃,☄}{★,🇺🇸,☆}lists
# Doesn't end exactly on endpoint 'stops after endpoint-{02..10..3}.txt'
# Simple list 'simple {I,want,my,money,back} list'
# Prefix, list _{I,want,my,money,back}
# Postfix, list {I,want,my,money,back}!
# List, no prefix, postfix {I,want,my,money,back}
# fail by design 'alphanumeric mix{a..1}.txt' > -> $test { say "$test ->"; my @strings = expand $test; my $cnt = -1; while $cnt != +@strings { $cnt = +@strings; @strings.=map: { |.&expand } }
say (' ' xx * Z~ @strings).join: "\n"; say ;
} </lang>
- 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 emoji{☃,☄}{★,🇺🇸,☆}lists -> emoji☃★lists emoji☃🇺🇸lists emoji☃☆lists emoji☄★lists emoji☄🇺🇸lists emoji☄☆lists stops after endpoint-{02..10..3}.txt -> stops after endpoint-02.txt stops after endpoint-05.txt stops after endpoint-08.txt simple {I,want,my,money,back} list -> simple I list simple want list simple my list simple money list simple back list _{I,want,my,money,back} -> _I _want _my _money _back {I,want,my,money,back}! -> I! want! my! money! back! {I,want,my,money,back} -> I want my money back alphanumeric mix{a..1}.txt -> alphanumeric mix{a..1}.txt
Wren
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).
<lang ecmascript>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()
}</lang>
- 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