Brace expansion using ranges

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 {🌵🌵..🌵🌶}

Last updated