I needed a way for a user to specify multiple items and to have each item in an array. Basically, 21, 35, 42-45, 19 needs to be expanded to 21, 35, 42, 43, 44, 45, 19 and have each number (seperated by anything matching \s*,\s*) put in an array.

Update Push integers into @expanded instead of strings in the else block. Thanks jeffa!

Update2 Now splits on \s*,\s* (and \s*-\s* for the ranges). Thanks again to jeffa for the suggestion.

Update3 The sub will now correctly deal with reverse ranges (ex/ 42-13) as well as abbreviated ranges (ex/ 123-5 produces 123, 124, 125). 123-35 also works as expected. If you give it 123-19, it will do a reverse abbreviated range : ) producing 123, 122, 121, 120, 119.

Update4 At jeffa's suggestion, I changed my use of the ternary operator so that it is being used to assign a value instead of in void context.

sub expandRanges { my @pieces = split /\s*,\s*/, $_[0]; my @expanded = (); foreach my $piece (@pieces) { next if $piece !~ /\A[\d\-\s]+\z/; if ($piece =~ /-/) { my @tmp = split /\s*-\s*/, $piece; my $diff = length($tmp[0]) - length($tmp[1]); $tmp[1] = substr($tmp[0], 0, $diff) . $tmp[1] if $diff; push @expanded, $tmp[0] > $tmp[1] ? reverse ($tmp[1]..$tmp[0]) : ($tmp[0]..$tmp[1]) ; } else { push @expanded, int $piece; } } return @expanded; }

Replies are listed 'Best First'.
(jeffa) Re: Expand Ranges in Lists of Numbers
by jeffa (Bishop) on Jun 08, 2003 at 20:00 UTC
    If [your original code] had more error-checking [and features like it does now], i wouldn't say that you are doing too much work. Using eval here gives pretty much the exact same results and requires hardly any effort on the coder's behalf ... just get rid of spaces and change all dashes to two dots:
    use Data::Dumper; print Dumper [evalexp('21, 35, 42-45, 19')]; sub evalexp { my $str = shift; $str =~ s/\s//g; $str =~ s/-/../g; return eval $str; }
    By the way, in your example, this line:
    push @expanded, $piece;
    pushes "strings" onto the array. You probably should "cast" $piece with int:
    push @expanded, int $piece;
    instead. (The ranges don't suffer from this side effect [caused by split i believe] because of the .. operator.) Data::Dumper reveals such details.

    Happy coding. :)

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    B--B--B--B--B--B--B--B--
    H---H---H---H---H---H---
    (the triplet paradiddle with high-hat)
    
      Thanks, I didn't even think of doing it that way! :-) I would change
      $str =~ s/\s//g;
      to
      $str =~ s/[^\d-,]//g;
      though when handling untrusted data.

      As they say, TMTOWTDI.

Re: Expand Ranges in Lists of Numbers
by atcroft (Abbot) on Jun 08, 2003 at 20:25 UTC

    Very interesting, and potentially very, very useful. It might also be useful to have the reverse function, compacting a list of numbers into a list containing ranges.

    The code below seems to do this, with the caveats that:

    1. if numbers appear twice in the list, only one instance is returned,
    2. the original ordering is lost, as the values in this code are sorted to ease processing, and
    3. the code is a sample, and someone more skilled would probably be able to do the same better or more efficiently

    With that in mind, here is my contribution:

    sub compactRanges { # # @pieces contains the list of numbers # %parts will contain a compact form, such that $start..$end # is represented as $parts{$start} = $end # $seperator contains the character(s) placed between the # values # %wrap_negatives contains a flag indicated if negative # values should be wrapped in some way, and the leading # and trailing values if so # my (@pieces) = @_; my $seperator = '..'; my %wrap_negatives = ( 'flag' => 1, 'leading' => '[', 'trailing' => ']' ); my (%parts); @pieces = sort { $a <=> $b } @pieces; { my ($i); my ($recent); $recent = $parts{$i} = $i = shift (@pieces); while (@pieces) { $i = shift (@pieces); $recent = $i if ( $i - 1 > $parts{$recent} ); $parts{$recent} = $i; } } foreach my $k ( sort { $a <=> $b } keys(%parts) ) { my $str = ( ( $wrap_negatives{'flag'} ) and ( $k < 0 ) ? $wrap_negatives{'leading'} : '' ) . $k . ( ( $wrap_negatives{'flag'} ) and ( $k < 0 ) ? $wrap_negatives{'trailing'} : '' ); $str .= $seperator . ( ( $wrap_negatives{'flag'} ) and ( $parts{$k} < 0 ) ? $wrap_negatives{'leading'} : '' ) . $parts{$k} . ( ( $wrap_negatives{'flag'} ) and ( $parts{$k} < 0 ) ? $wrap_negatives{'trailing'} : '' ) if ( $parts{$k} != $k ); push ( @pieces, $str ); } return (@pieces); }
Re: Expand Ranges in Lists of Numbers
by valdez (Monsignor) on Jun 08, 2003 at 21:49 UTC
      Thanks! I didn't know Dominus did a Perl QOTW. I guess I'll have to subscribe...  ; )
(jeffa) Re: Expand Ranges in Lists of Numbers
by jeffa (Bishop) on Jun 09, 2003 at 01:14 UTC
    Hate to flog a dead horse (not that your code is a dead horse - i just like that expression), but this:
    $tmp[0] > $tmp[1] ? push @expanded, reverse ($tmp[1]..$tmp[0]) : push @expanded, ($tmp[0]..$tmp[1]);
    is bad style because the ternery operator is being used in void context. You would be better using an if-else statement, because it is more readable. If you prefer the brevity of the ternery operator, then please use it as an assignment operator:
    push @expanded, $tmp[0] > $tmp[1] ? reverse ($tmp[1]..$tmp[0]) : ($tmp[0]..$tmp[1]) ;
    I like the changes, by the way. Now we have a fully flogged dead horse. :D

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    B--B--B--B--B--B--B--B--
    H---H---H---H---H---H---
    (the triplet paradiddle with high-hat)
    
      ( $tmp[0] > $tmp[1] ? sub { push @expanded, reverse ($tmp[1]..$tmp[0]) } : sub { push @expanded, ($tmp[0]..$tmp[1]) } )->();
      :->

      Makeshifts last the longest.

      bad style because the ternery operator is being used in void context.
      I know, I'm guilty. I have a tendency to use the ternary operator like that quite often and I've been trying to kick the habit, but the habit keeps kicking back (so to speak). Anyway, I'll change it in the code.

      Sometimes I wish one line blocks for if-else statements didn't require brackets, like C's. Oh well.

      I like the changes, by the way. Now we have a fully flogged dead horse. :D
      Thanks, I like the funky reverse abbreviated ranges. I always liked beating things that couldn't fight back... ; )

        "Sometimes I wish one line blocks for if-else statements didn't require brackets..."

        I use to say the same thing until i realized they don't:
        print "that's the answer" if $answer == 42; die "that's not your file!" unless -o $file;
        A caveat i teach my C++ lab students (i am a graduate teaching assistant at MTSU) is that even though braces are optional for one line blocks, they should get in the habit of using them always. Why? Because 9 times outta 10 you will want to add a second line in the near future. Murphy's law or something ... ;)

        jeffa

        L-LL-L--L-LL-L--L-LL-L--
        -R--R-RR-R--R-RR-R--R-RR
        B--B--B--B--B--B--B--B--
        H---H---H---H---H---H---
        (the triplet paradiddle with high-hat)
        
Re: Expand Ranges in Lists of Numbers
by svsingh (Priest) on Jun 08, 2003 at 15:46 UTC
    Nice. This can come in handy. My only suggestion (minor) would be to sort the list before you return it. Unless there's a reason you left that out or know that your input will always be in order. Thanks.
      Well, I'd prefer to leave the array in the order given (just in case the user wants it that way), but if you wanted it sorted you could do one of two things:
      1. Replace the line return @expanded; with return sort @expanded;
      2. When calling it, just sort the results. Ex/ my @results = sort expandRange($string);
Re: Expand Ranges in Lists of Numbers
by zentara (Cardinal) on Jun 09, 2003 at 20:39 UTC
    Heres another snippet for consideration.
    #!/usr/bin/perl use warnings; my $ranges = '1,3-7,9-22'; print "$ranges\n"; $ranges =~ s/-/../g; my @array = map {$_} eval $ranges; print "@array\n";
Re: Expand Ranges in Lists of Numbers
by belg4mit (Prior) on Jun 09, 2003 at 15:39 UTC
    This is not unlike a previously unreleased utility of mine, which I had intended to post somewhere long long ago, and is now at range.

    --
    I'm not belgian but I play one on TV.