Brace expansion using ranges
- Task
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 (at least) each of the ten example lines shown below.
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 current versions of macOS:
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 reverseSteppedNumberRising{1..6..-2}.txt -> reverseSteppedNumberRising5.txt reverseSteppedNumberRising3.txt reverseSteppedNumberRising1.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
- Metrics
- Counting
- Word frequency
- Letter frequency
- Jewels and stones
- I before E except after C
- Bioinformatics/base count
- Count occurrences of a substring
- Count how many vowels and consonants occur in a string
- Remove/replace
- XXXX redacted
- Conjugate a Latin verb
- Remove vowels from a string
- String interpolation (included)
- Strip block comments
- Strip comments from a string
- Strip a set of characters from a string
- Strip whitespace from a string -- top and tail
- Strip control codes and extended characters from a string
- Anagrams/Derangements/shuffling
- Word wheel
- ABC problem
- Sattolo cycle
- Knuth shuffle
- Ordered words
- Superpermutation minimisation
- Textonyms (using a phone text pad)
- Anagrams
- Anagrams/Deranged anagrams
- Permutations/Derangements
- Find/Search/Determine
- ABC words
- Odd words
- Word ladder
- Semordnilap
- Word search
- Wordiff (game)
- String matching
- Tea cup rim text
- Alternade words
- Changeable words
- State name puzzle
- String comparison
- Unique characters
- Unique characters in each string
- Extract file extension
- Levenshtein distance
- Palindrome detection
- Common list elements
- Longest common suffix
- Longest common prefix
- Compare a list of strings
- Longest common substring
- Find common directory path
- Words from neighbour ones
- Change e letters to i in words
- Non-continuous subsequences
- Longest common subsequence
- Longest palindromic substrings
- Longest increasing subsequence
- Words containing "the" substring
- Sum of the digits of n is substring of n
- Determine if a string is numeric
- Determine if a string is collapsible
- Determine if a string is squeezable
- Determine if a string has all unique characters
- Determine if a string has all the same characters
- Longest substrings without repeating characters
- Find words which contains all the vowels
- Find words which contain the most consonants
- Find words which contains more than 3 vowels
- Find words whose first and last three letters are equal
- Find words with alternating vowels and consonants
- Formatting
- Substring
- Rep-string
- Word wrap
- String case
- Align columns
- Literals/String
- Repeat a string
- Brace expansion
- Brace expansion using ranges
- Reverse a string
- Phrase reversals
- Comma quibbling
- Special characters
- String concatenation
- Substring/Top and tail
- Commatizing numbers
- Reverse words in a string
- Suffixation of decimal numbers
- Long literals, with continuations
- Numerical and alphabetical suffixes
- Abbreviations, easy
- Abbreviations, simple
- Abbreviations, automatic
- Song lyrics/poems/Mad Libs/phrases
- Mad Libs
- Magic 8-ball
- 99 bottles of beer
- The Name Game (a song)
- The Old lady swallowed a fly
- The Twelve Days of Christmas
- Tokenize
- Text between
- Tokenize a string
- Word break problem
- Tokenize a string with escaping
- Split a character string based on change of character
- Sequences
11l
F intFromString(s) -> Int?
X.try
R Int(s)
X.catch ValueError
R N
F parseRange(r)
I r.empty {R [‘{}’]}
V sp = r.split(‘..’)
I sp.len == 1 {R [‘{’r‘}’]}
V first = sp[0]
V last = sp[1]
V incr = I sp.len == 2 {‘1’} E sp[2]
Int? val1 = intFromString(first)
Int? val2 = intFromString(last)
Int? val3 = intFromString(incr)
I val3 == N {R [‘{’r‘}’]}
V n3 = val3
V numeric = val1 != N & val2 != N
Int n1
Int n2
I numeric
n1 = val1
n2 = val2
E
I (val1 != N & val2 == N) | (val1 == N & val2 != N)
R [‘{’r‘}’]
I first.len != 1 | last.len != 1
R [‘{’r‘}’]
n1 = first[0].code
n2 = last[0].code
V width = 1
I numeric
width = max(first.len, last.len)
I n3 == 0
R I numeric {[String(n1).zfill(width)]} E [first]
V asc = n1 < n2
I n3 < 0
asc = !asc
swap(&n1, &n2)
n3 = -n3
[String] result
V i = n1
I asc
L i <= n2
result.append(I numeric {String(i).zfill(width)} E Char(code' i))
i += n3
E
L i >= n2
result.append(I numeric {String(i).zfill(width)} E Char(code' i))
i -= n3
R result
F rangeExpand(s)
V result = [‘’]
V rng = ‘’
V inRng = 0B
L(c) s
I c == ‘{’ & !inRng
inRng = 1B
rng = ‘’
E I c == ‘}’ & inRng
V rngRes = parseRange(rng)
[String] res
L(r) result
L(rr) rngRes
res.append(r‘’rr)
result = move(res)
inRng = 0B
E I inRng
rng ‘’= c
E
L(&s) result
s ‘’= c
I inRng
L(&s) result
s ‘’= ‘{’rng
R result
-V 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’,
‘li{teral’,
‘rangeless{}empty’,
‘rangeless{random}string’,
‘mixedNumberAlpha{5..k}’,
‘steppedAlphaRising{P..Z..2}.txt’,
‘stops after endpoint-{02..10..3}.txt’]
L(s) examples
print(s" ->\n ", end' ‘’)
V res = rangeExpand(s)
print(res.join("\n "))
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 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
AutoHotkey
Brace_expansion_using_ranges(line){
needle := "^.*\K{(?P<Start>[^{}]+?)\..(?P<End>[^{}]+?)(?:\..(?P<Incr>[^{}]+?))?}"
while true
{
while pos := RegExMatch(line, needle, m, A_Index=1?1:pos+StrLen(m))
{
char := false, step := "", output := ""
reverse := InStr(mIncr, "-") ? true : false
if mStart is number
pad1 := pad(mStart), pad2 := pad(mEnd), pad := StrLen(pad1)>=StrLen(pad2) ? pad1 : pad2
else
mStart := Ord(mStart), mEnd := Ord(mEnd), char := true
mIncr := (mIncr?Abs(mIncr):1) * (mStart>mEnd?-1:1)
loop % Abs((mStart-mEnd)/mIncr) + 1
{
step := mStart + (A_Index-1) * mIncr
step := pad <> "" ? SubStr(pad . step, 1-StrLen(pad)) : step
step := char ? Chr(step) : step
Rep := StrReplace(line, m, step)
output := reverse ? rep "`n" output : output .= Rep "`n"
}
output := Trim(Output, "`n")
}
if RegExMatch(output, needle)
line := output
else
break
}
return output ? output : line
}
pad(num){
if RegExMatch(num, "`am)^(0+)(?=[1-9]|0$)", m)
loop % StrLen(num)
pad .= "0"
return pad
}
Examples:
data=
(
simpleNumberRising{1..3}.txt
simpleAlphaDescending-{Z..X}.txt
steppedDownAndPadded-{10..00..5}.txt
minusSignFlipsSequence {030..20..-5}.txt
reverseSteppedNumberRising{1..6..-2}.txt
combined-{Q..P}{2..1}.txt
emoji{🌵..🌶}{🌽..🌾}etc
li{teral
rangeless{}empty
rangeless{random}string
)
for i, line in StrSplit(data, "`n", "`r")
result .= line " ->`n" RegExReplace(Brace_expansion_using_ranges(line), "`am)^", "`t") "`n`n"
MsgBox, 262144, , % result
return
- 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 reverseSteppedNumberRising{1..6..-2}.txt -> reverseSteppedNumberRising5.txt reverseSteppedNumberRising3.txt reverseSteppedNumberRising1.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
F#
// Brace expansion using ranges. Nigel Galloway: October 6th., 2021
let fUC, fUR=System.Text.Rune.GetUnicodeCategory,(fun n->System.Text.Rune.GetRuneAt(n,0))
let fV(n,i,g,e,l,s)=let l=if l="" then 1 else int l in match l with 0->None |_->Some(n,i,g,e,int l,s)
let(|Valid|_|)(n:System.Text.RegularExpressions.Match)=let fN(g:string)=n.Groups.[g].Value in if n.Success then fV(fN "n",fN "i",fN "g",fN "e",fN "l",fN "s") else None
let fN(g:string)=let mutable g=g.EnumerateRunes() in if g.MoveNext() && not(g.MoveNext()) then true else false
let(|I|_|)(n,g)=if fN n && fN g then (let n,g=fUR n,fUR g in if fUC n=fUC g then Some(n,g) else None) else None
let(|G|_|)(n:string,g:string)=try let n,g=(int n,int g) in Some(n,g) with _->None
let(|E|_|)(n:string,g:string)=if n.[0]='0' || g.[0]='0' then match (n,g) with G(e,l)->Some(e,l,max n.Length g.Length) |_->None else None
let fL n=let fN i g e l=let n=[i..(if i>g then -l else l)..g] in if e="-" then List.rev n else n
let fG n g=let n,buf=string n, System.Text.StringBuilder() in (for _ in 1..g-n.Length do buf.Append 0); buf.Append n; buf.ToString()
match System.Text.RegularExpressions.Regex.Match(n,@"^(?<n>.*?){(?<i>.*?)\.\.(?<g>.*?)(\.\.(?<e>[-]+)?(?<l>[0-9]*?))?}(?<s>.*)$") with
Valid(n,i,g,e,l,s)->match (i,g) with I(i,g)->Some(fN i.Value g.Value e l|>Seq.map(fun g->sprintf "%s%A%s" n (System.Text.Rune(g)) s))
|E(i,g,z)->Some(fN i g e l|>Seq.map(fun g->sprintf "%s%s%s" n (fG g z) s))
|G(i,g)->Some(fN i g e l|>Seq.map(fun g->sprintf "%s%A%s" n (string g) s)) |_->None
|_->None
let rec expBraces n=seq{match fL n with Some n->yield!(n|>Seq.collect(expBraces)) |_->yield n}
let tests=["simpleNumberRising{1..3}.txt";"steppedNumberRising{1..6..2}.txt";"reverseSteppedNumberRising{1..6..-2}.txt";"steppedNumberDescending{20..9..2}.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{random}string";"rangeless{}empty";"steppedAlphaDescending-{Z..M..2}.txt";"reversedSteppedAlphaDescending-{Z..M..-2}.txt"]
tests|>List.iter(fun g->printfn $"%s{g}->"; for n in expBraces g do printfn $" %s{n}")
- Output:
simpleNumberRising{1..3}.txt-> simpleNumberRising1.txt simpleNumberRising2.txt simpleNumberRising3.txt steppedNumberRising{1..6..2}.txt-> steppedNumberRising1.txt steppedNumberRising3.txt steppedNumberRising5.txt reverseSteppedNumberRising{1..6..-2}.txt-> reverseSteppedNumberRising5.txt reverseSteppedNumberRising3.txt reverseSteppedNumberRising1.txt steppedNumberDescending{20..9..2}.txt-> steppedNumberDescending"20".txt steppedNumberDescending"18".txt steppedNumberDescending"16".txt steppedNumberDescending"14".txt steppedNumberDescending"12".txt steppedNumberDescending"10".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-N.txt reversedSteppedAlphaDescending-P.txt reversedSteppedAlphaDescending-R.txt reversedSteppedAlphaDescending-T.txt reversedSteppedAlphaDescending-V.txt reversedSteppedAlphaDescending-X.txt reversedSteppedAlphaDescending-Z.txt
Go
package main
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
func sign(n int) int {
switch {
case n < 0:
return -1
case n > 0:
return 1
}
return 0
}
func abs(n int) int {
if n < 0 {
return -n
}
return n
}
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
t := n1
d := abs(n1-n2) % (-n3)
n1 = n2 - d*sign(n2-n1)
n2 = t
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",
"reverseSteppedNumberRising{1..6..-2}.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",
"steppedNumberRising{1..6..2}.txt",
"steppedNumberDescending{20..9..2}",
"steppedAlphaDescending-{Z..M..2}.txt",
"reversedSteppedAlphaDescending-{Z..M..-2}.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
(() => {
"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] = 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",
"reverseSteppedNumberRising{1..6..-2}.txt",
"combined-{Q..P}{2..1}.txt",
"emoji{🌵..🌶}{🌽..🌾}etc",
"li{teral",
"rangeless{}empty",
"rangeless{random}string"
];
return tests.map(s => {
const
expanded = braceExpandWithRange(s)
.join("\n\t");
return `${s} -> \n\t${expanded}`;
})
.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], 10)
)
);
return map(
compose(justifyRight(w)("0"), str)
)(
(
0 > by ? (
reverse
) : identity
)(
enumFromThenTo(from)(
from + (
to < from ? (
-abs(by)
) : abs(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(
([v, r]) => parse(
fmapP(v)(p)
)(r)
)
);
// 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(
([x, r]) => parse(f(x))(r)
)
);
// 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(
first(f)
)
);
// item :: () -> Parser Char
const item = () =>
// A single character.
Parser(s => {
const [h, ...t] = s;
return Boolean(h) ? [
Tuple(h)(t)
] : [];
});
// 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 someP = q =>
liftA2P(
x => xs => [x].concat(xs)
)(q)(many(q));
return Parser(
s => parse(
0 < s.length ? (
altP(someP(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 => 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 => {
const [h, ...t] = s;
return Boolean(h) ? (
test(h) ? [
Tuple(h)(t)
] : []
) : [];
});
// 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(
([v, r]) => parse(q)(r).flatMap(
first(xs => v.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 manyP = q =>
altP(some(q))(pureP([]));
return Parser(
s => parse(
liftA2P(
x => xs => [x].concat(xs)
)(p)(manyP(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,
*[Symbol.iterator]() {
for (const k in this) {
if (!isNaN(k)) {
yield this[k];
}
}
}
});
// abs :: Num -> Num
const abs =
// Absolute value of a given number
// without the sign.
x => 0 > x ? (
-x
) : x;
// 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]
const concat = xs =>
xs.flat(1);
// enumFromThenTo :: Int -> Int -> Int -> [Int]
const enumFromThenTo = m =>
// Integer values enumerated from m to n
// with a step defined by (nxt - m).
nxt => n => {
const d = nxt - m;
return Array.from({
length: (Math.floor(n - nxt) / d) + 2
}, (_, i) => m + (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.
([x, y]) => Tuple(f(x))(y);
// 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));
// fromEnum :: Enum a => a -> Int
const fromEnum = x =>
typeof x !== "string" ? (
x.constructor === Object ? (
x.value
) : parseInt(Number(x), 10)
) : x.codePointAt(0);
// identity :: a -> a
const identity = x =>
// The identity function. (`id`, in Haskell)
x;
// 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;
// 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.");
};
// reverse :: [a] -> [a]
const reverse = xs =>
xs.slice(0).reverse();
// 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.
(...args) => {
const
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 reverseSteppedNumberRising{1..6..-2}.txt -> reverseSteppedNumberRising5.txt reverseSteppedNumberRising3.txt reverseSteppedNumberRising1.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
jq
Works with jq, the C implementation of jq
Works with gojq, the Go implementation of jq
This implementation relies on "reluctant" regex parsing.
Range expressions of the form {x..y}, where x and y are single characters, are allowed, even if exactly one of them is a digit.
When expanding an expression with more than one range, the program as given below produces an ordering based on expansion of the left-most range first. A trivial change in two places is sufficient to produce the alternative ordering.
# Left-pad with 0s
def lpad($len): tostring | ($len - length) as $l | ("0" * $l) + .;
def expand:
# The key to success here is reluctance (".*?")
def cap:
capture("(?<head>^.*?)[{](?<from>[0-9]+|.)[.][.](?<to>[0-9]+|.)"
+ "([.][.](?<sign>-)?(?<increment>[0-9]))?[}](?<tail>.*)$");
def ton: if . == null then . else tonumber end;
# Produce a stream of integers, handling implicit descent.
# $i and $j should be integers.
# If $i and $j are distinct, then expand($i;$j;null;null) will include both,
# otherwise just $i.
def expand($i; $j; $sign; $increment):
(if $increment == null then 1 else $increment end) as $inc
| if $sign == null
then if $i <= $j
then range($i; $j + 1; $inc)
else range($i; $j - 1; - $inc)
end
else [expand($i; $j; null; $increment)] | reverse[]
end ;
# Produce a stream of single characters, handling implicit descent
def explode($x; $y; $sign; $increment):
($x|explode[0]) as $x
| ($y|explode[0]) as $y
| expand($x; $y; $sign; $increment)
| [.] | implode;
# The number of leading 0s of the input string
def leadingZeros: match("^0*") | .string | length;
def padding($x; $y):
($x | leadingZeros) as $a
| ($y | leadingZeros) as $b
| [if $a > 0 then ($x|length) else 0 end,
if $b > 0 then ($y|length) else 0 end]
| max;
( cap as $c
| if ($c.from|test("[0-9]+")) and ($c.to|test("[0-9]+"))
then padding($c.from; $c.to) as $padding
| $c.head
+ ( expand($c.from|tonumber;
$c.to|tonumber;
$c.sign;
$c.increment | ton) | lpad($padding))
+ ($c.tail | expand)
elif ($c.from|length == 1) and ($c.to|length == 1)
then $c.head + explode($c.from; $c.to; $c.sign; $c.increment|ton)
+ ($c.tail | expand)
else ""
end )
// . ;
def examples:
"simpleNumberRising{1..3}.txt",
"simpleAlphaDescending-{Z..X}.txt",
"steppedDownAndPadded-{10..00..5}.txt",
"minusSignFlipsSequence {030..20..-5}.txt",
"reverseSteppedNumberRising{1..6..-2}.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",
"steppedNumberRising{1..6..2}.txt",
"steppedNumberDescending{20..9..2}",
"steppedAlphaDescending-{Z..M..2}.txt",
"reversedSteppedAlphaDescending-{Z..M..-2}.txt"
;
examples
| "\(.) ->",
" \(expand)", ""
- 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 reverseSteppedNumberRising{1..6..-2}.txt -> reverseSteppedNumberRising5.txt reverseSteppedNumberRising3.txt reverseSteppedNumberRising1.txt combined-{Q..P}{2..1}.txt -> combined-Q2.txt combined-P2.txt combined-Q1.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} -> mixedNumberAlpha5 mixedNumberAlpha6 mixedNumberAlpha7 mixedNumberAlpha8 mixedNumberAlpha9 mixedNumberAlpha: mixedNumberAlpha; mixedNumberAlpha< mixedNumberAlpha= mixedNumberAlpha> mixedNumberAlpha? mixedNumberAlpha@ mixedNumberAlphaA mixedNumberAlphaB mixedNumberAlphaC mixedNumberAlphaD mixedNumberAlphaE mixedNumberAlphaF mixedNumberAlphaG mixedNumberAlphaH mixedNumberAlphaI mixedNumberAlphaJ mixedNumberAlphaK mixedNumberAlphaL mixedNumberAlphaM mixedNumberAlphaN mixedNumberAlphaO mixedNumberAlphaP mixedNumberAlphaQ mixedNumberAlphaR mixedNumberAlphaS mixedNumberAlphaT mixedNumberAlphaU mixedNumberAlphaV mixedNumberAlphaW mixedNumberAlphaX mixedNumberAlphaY mixedNumberAlphaZ mixedNumberAlpha[ mixedNumberAlpha\ mixedNumberAlpha] mixedNumberAlpha^ mixedNumberAlpha_ mixedNumberAlpha` mixedNumberAlphaa mixedNumberAlphab mixedNumberAlphac mixedNumberAlphad mixedNumberAlphae mixedNumberAlphaf mixedNumberAlphag mixedNumberAlphah mixedNumberAlphai mixedNumberAlphaj mixedNumberAlphak 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 steppedNumberRising{1..6..2}.txt -> steppedNumberRising1.txt steppedNumberRising3.txt steppedNumberRising5.txt steppedNumberDescending{20..9..2} -> steppedNumberDescending20 steppedNumberDescending18 steppedNumberDescending16 steppedNumberDescending14 steppedNumberDescending12 steppedNumberDescending10 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-N.txt reversedSteppedAlphaDescending-P.txt reversedSteppedAlphaDescending-R.txt reversedSteppedAlphaDescending-T.txt reversedSteppedAlphaDescending-V.txt reversedSteppedAlphaDescending-X.txt reversedSteppedAlphaDescending-Z.txt
Julia
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
Nim
import options, strutils, unicode
func intFromString(s: string): Option[int] =
## Try to parse an int. Return some(int) if parsing
## was successful, return none(int) if it failed.
try:
let n = s.parseInt()
result = some(n)
except ValueError:
result = none(int)
func parseRange(r: string): seq[string] =
if r.len == 0: return @["{}"] # rangeless, empty.
let sp = r.split("..")
if sp.len == 1: return @['{' & r & '}']
let first = sp[0]
let last = sp[1]
let incr = if sp.len == 2: "1" else: sp[2]
let val1 = intFromString(first)
let val2 = intFromString(last)
let val3 = intFromString(incr)
if val3.isNone(): return @['{' & r & '}'] # increment isn't a number.
var n3 = val3.get()
let numeric = val1.isSome and val2.isSome
var n1, n2: int
if numeric:
n1 = val1.get()
n2 = val2.get()
else:
if val1.isSome and val2.isNone or val1.isNone and val2.isSome:
return @['{' & r & '}'] # mixed numeric/alpha not expanded.
if first.runeLen != 1 or last.runeLen != 1:
return @['{' & r & '}'] # start/end are not both single alpha.
n1 = first.toRunes[0].int
n2 = last.toRunes[0].int
var width = 1
if numeric:
width = if first.len < last.len: last.len else: first.len
if n3 == 0:
# Zero increment.
return if numeric: @[n1.intToStr(width)] else: @[first]
var asc = n1 < n2
if n3 < 0:
asc = not asc
swap n1, n2
n3 = -n3
var i = n1
if asc:
while i <= n2:
result.add if numeric: i.intToStr(width) else: $Rune(i)
inc i, n3
else:
while i >= n2:
result.add if numeric: i.intToStr(width) else: $Rune(i)
dec i, n3
func rangeExpand(s: string): seq[string] =
result = @[""]
var rng = ""
var inRng = false
for c in s:
if c == '{' and not inRng:
inRng = true
rng = ""
elif c == '}' and inRng:
let rngRes = rng.parseRange()
var res: seq[string]
for i in 0..result.high:
for j in 0..rngRes.high:
res.add result[i] & rngRes[j]
result = move(res)
inRng = false
elif inRng:
rng.add c
else:
for s in result.mitems: s.add c
if inRng:
for s in result.mitems: s.add '{' & rng # unmatched braces.
when isMainModule:
const 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:
stdout.write s, " →\n "
let res = rangeExpand(s)
stdout.write res.join("\n ")
echo '\n'
- 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
requires("0.8.2") -- (is_integer() is new, plus "==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
Python
"""Brace expansion using ranges. Requires Python >= 3.6.
Here we use regular expressions for parsing and take an object orientated approach
to expansion of range expressions.
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
if start < stop:
step = abs(step)
else:
start -= 1
stop -= 1
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"simpleAlphaDescending-{Z..X}.txt",
r"steppedDownAndPadded-{10..00..5}.txt",
r"minusSignFlipsSequence {030..20..-5}.txt",
r"reverseSteppedNumberRising{1..6..-2}.txt",
r"combined-{Q..P}{2..1}.txt",
r"emoji{🌵..🌶}{🌽..🌾}etc",
r"li{teral",
r"rangeless{}empty",
r"rangeless{random}string",
# Extra examples, not from the task description.
r"steppedNumberRising{1..6..2}.txt",
r"steppedNumberDescending{20..9..2}.txt",
r"steppedAlphaDescending-{Z..M..2}.txt",
r"reverseSteppedAlphaRising{A..F..-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()
- 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 reverseSteppedNumberRising{1..6..-2}.txt -> reverseSteppedNumberRising5.txt reverseSteppedNumberRising3.txt reverseSteppedNumberRising1.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 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 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 reverseSteppedAlphaRising{A..F..-2}.txt -> reverseSteppedAlphaRisingE.txt reverseSteppedAlphaRisingC.txt reverseSteppedAlphaRisingA.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
Raku
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 != +@return {
$cnt = +@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 {🌵🌵..🌵🌶}
Rust
use itertools::Itertools;
use regex::Regex;
use std::cmp::max;
fn pad_zeros_needed(string: &str) -> usize {
let len = string.len();
return if len > 1 && string.chars().collect_vec()[0] == '0' {
len
} else {
0
};
}
fn ranged(string: &str) -> Vec<String> {
let re = Regex::new(r"\{|\}|\.\.").unwrap();
let mut rang = re.split(string).filter(|s| !s.is_empty()).collect_vec();
if rang.len() < 2 {
return vec![string.to_string()];
}
let mut delta = if rang.len() > 2 {
rang[2].parse::<i32>().unwrap_or(1)
} else {
1
};
if delta < 0 {
(rang[0], rang[1], delta) = (rang[1], rang[0], -delta);
}
let rangchars = rang.iter().map(|s| s.chars().collect_vec()).collect_vec();
if "0123456789-".contains(rangchars[0][0]) {
let x: i32 = rang[0].parse::<i32>().unwrap_or(-10_000_000);
let y: i32 = rang[1].parse::<i32>().unwrap_or(-10_000_000);
if x < -1000000 || y < -1000000 {
return vec![string.to_string()];
}
let pad = max(pad_zeros_needed(rang[0]), pad_zeros_needed(rang[1]));
if x < y {
return (x..=y)
.step_by(delta as usize)
.map(|i| format!("{:0>pad$}", i))
.collect_vec();
} else {
let mut r = (y..=x)
.step_by(delta as usize)
.map(|i| format!("{:0>pad$}", i))
.collect_vec();
r.reverse();
return r;
}
} else {
let x = rangchars[0][rangchars[0].len() - 1];
let y = rangchars[1][0];
let mut z = rangchars[0].clone();
z.remove(z.len() - 1);
if x < y {
return (x..=y)
.step_by(delta as usize)
.map(|i| {
let mut zc = z.clone();
zc.push(i);
zc.into_iter().collect::<String>()
})
.collect_vec();
} else {
let mut r = (y..=x)
.step_by(delta as usize)
.map(|i| {
let mut zc = z.clone();
zc.push(i);
zc.into_iter().collect::<String>()
})
.collect_vec();
r.reverse();
return r;
}
}
}
fn splatrange(s: &str) -> Vec<String> {
let re = Regex::new(r"([^\{]*)(\{[^}]+\.\.[^\}]+\})(.*)").unwrap();
let mut c = match re.captures(s) {
Some(v) => v.iter().map(|s| s.unwrap().as_str()).collect_vec(),
None => vec![""],
};
c.remove(0);
if c.len() < 3 {
return vec![s.to_string()];
}
let mut results: Vec<String> = vec![];
for b in ranged(&c[2]) {
for x in ranged(&c[1]) {
results.push(c[0].to_string() + x.as_str() + b.as_str());
}
}
return results;
}
fn main() {
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!(
"{}->\n{}",
test,
splatrange(test)
.iter()
.map(|s| format!(" {}\n", s))
.join("")
);
}
}
- 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 combined-P2 combined-Q1 combined-P1 emoji{🌵..🌶}{🌽..🌾}etc-> emoji🌵🌽 emoji🌶🌽 emoji🌵🌾 emoji🌶🌾 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
Wren
Added a few more examples to the minimum number needed for the task.
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
var d = (n1 - n2).abs % (-n3)
n1 = n2 - d * (n2 - n1).sign
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",
"reverseSteppedNumberRising{1..6..-2}.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",
"steppedNumberRising{1..6..2}.txt",
"steppedNumberDescending{20..9..2}",
"steppedAlphaDescending-{Z..M..2}.txt",
"reversedSteppedAlphaDescending-{Z..M..-2}.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 reverseSteppedNumberRising{1..6..-2}.txt -> reverseSteppedNumberRising5.txt reverseSteppedNumberRising3.txt reverseSteppedNumberRising1.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 steppedNumberRising{1..6..2}.txt -> steppedNumberRising1.txt steppedNumberRising3.txt steppedNumberRising5.txt steppedNumberDescending{20..9..2} -> steppedNumberDescending20 steppedNumberDescending18 steppedNumberDescending16 steppedNumberDescending14 steppedNumberDescending12 steppedNumberDescending10 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-N.txt reversedSteppedAlphaDescending-P.txt reversedSteppedAlphaDescending-R.txt reversedSteppedAlphaDescending-T.txt reversedSteppedAlphaDescending-V.txt reversedSteppedAlphaDescending-X.txt reversedSteppedAlphaDescending-Z.txt