xipho has asked for the wisdom of the Perl Monks concerning the following question:

Learned temple-goers,
What's the simplest way to do what the code below so unelegantly does?
sub foo () { my %hash = @_; my @array = ( sort {$a <=> $b} (keys %hash) ); my $j = shift @array; my ($periods, $i); my $string = "($j"; foreach $i (@array) { if ($i == $j + 1) { $periods = 1; } else { if ($periods) { $string .= "..$j, $i"; } else { $string .= ", $i"; } $periods = 0; } $j = $i; } if ($periods) { $string .= "..$j)" } else { $string .= ")"; } return $string; } # a test my @test_nums = (0, 10, 11, 12, 2, 4, 3, 5, 6, 7, 9); my %test; # pass via hash, only because I ultimately need to foreach my $num (@test_nums) { $test{$num} = undef; } #test print &foo(%test);

Replies are listed 'Best First'.
Re: Display array as ranges?
by BrowserUk (Patriarch) on Jan 17, 2005 at 05:29 UTC

    If you want something shorter, try List-to-Range generation

    (Please do not upvote this node! Japhy did all the clever stuff, I just remembered where it was (cos I stole ideas from it:))


    Examine what is said, not who speaks.
    Silence betokens consent.
    Love the truth but pardon error.
      Thanks- exactly along the lines of what I was looking for.
Re: Display array as ranges?
by Zaxo (Archbishop) on Jan 17, 2005 at 05:25 UTC

    You should get rid of the empty prototype, the "()" after the sub's name. It will prevent foo from being called with arguments.

    After Compline,
    Zaxo

Re: Display array as ranges?
by chromatic (Archbishop) on Jan 17, 2005 at 08:19 UTC

    I like the regex solution linked elsewhere, but how about something a little bit functional?

    #!/usr/bin/perl use strict; use warnings; use Test::More tests => 4; sub range { my @numbers = sort { $a <=> $b } @_; my ($in_range, @range); while (@numbers > 1) { my $current = shift @numbers; my $gap = $numbers[0] - $current; for my $action ( [ sub { $_[0] && $_[1] > 1 }, [ ', ', 0 ] ], [ sub { ! $_[0] && $_[1] == 1 }, [ '..', 1 ] ], [ sub { ! $_[0] }, [ ', ', 0 ] ], ) { my ($condition, $values) = @$action; next unless $condition->( $in_range, $gap ); push @range, $current, $values->[0]; $in_range = $values->[1]; last; } } return join('', @range, @numbers); } my $result = range( 0, 10, 11, 12, 2, 4, 3, 5, 6, 7, 9 ); like( $result, qr/^0/, 'range() should sort results' ); like( $result, qr/12$/, '... including end' ); like( $result, qr/2\.\./, '... finding start of range' ); is( $result, '0, 2..7, 9..12', '... and rangifying numbers' );
Re: Display array as ranges?
by Errto (Vicar) on Jan 17, 2005 at 07:36 UTC
    Ok, well, I spent way too much time on this and came up with a really convoluted solution. But it has two nice features: a) an OO module, and b) ability to add more values later. Here goes.
    package Subsequence; use overload '""' => \&list_form; sub new { my $package = shift || __PACKAGE__; return bless {ranges => {}, vals => {}}, $package; } sub insert_list { my $self = shift; my $vals = $self->{vals}; foreach my $val (sort { $a <=> $b } @_) { next if exists $vals->{$val}; if (exists $vals->{$val - 1}) { $vals->{$val} = $vals->{$val - 1}; $vals->{$val}->{end} = $val; if (exists $vals->{$val + 1}) { $vals->{$val}->{end} = $vals->{val + 1}->{end}; my $item = $vals->{val + 1}; for my $i ($val + 1 .. $vals->{$val}->{end}) { $vals->{$i} = $vals-{$val}; } delete $self->{ranges}->{$item}; } } elsif (exists $vals->{$val + 1}) { $vals->{$val} = $vals->{$val + 1}; $vals->{$val}->{start} = $val; } else { my $item = { start => $val, $end => $val }; $vals->{$val} = $item; $self->{ranges}->{$item} = $item; } } return $self; } sub list_form { my $self = shift; my $ranges = $self->{ranges}; return "(" . ( join ", ", map { $_->{start} == $_->{end} ? $_->{start} : "$_->{start}..$_->{end}" } sort {$a->{start} <=> $b->{start} } values %$ranges ) . ")"; } 1;
    and used as
    use Subsequence; my @test_nums = (0, 10, 11, 12, 2, 4, 3, 5, 6, 7, 9); my $x = Subsequence->new->insert_list(@test_nums); print $x, "\n"; $x->insert_list(1, 13); print $x, "\n";
    This is ugly I know, but the first solution I wrote up using only arrays was even uglier, though it had the advantage of not sorting every time on retrieval. But I just couldn't figure out how to make it work generically, which was really frustrating.
Re: Display array as ranges?
by xipho (Scribe) on Jan 17, 2005 at 05:09 UTC
    Sigh. This is hardly homework- given that I'm self taught and finished taking classes several years ago. Being a perl-newb I wrote the above code in around 10 minutes. It works fine for me just as it is, and the question is posted about as simply as can be. If you run the code or even briefly examine it you'll see what I'm looking for.
Re: Display array as ranges?
by davido (Cardinal) on Jan 17, 2005 at 04:49 UTC

    Though my familiarity with homework assignments posted here helps to fill me in on what your code does, it might be a good idea to tell us just for the record so that we will know what your assignment is, exactly. You wouldn't want us to guess at the specification and get it wrong so you'll get a lower grade.

    Since this is almost certanly homework, and it would be highly unethical for you to turn in verbatum answers given to you by other PerlMonks, perhaps we should ask you, in what ways do you think it can be improved? And more importantly, is there a particular issue that we can help you to work out better?

    I'm all for providing specific help to specific questions, even if it is homework. But you've got to ask first.


    Dave

Re: Display array as ranges?
by Random_Walk (Prior) on Jan 17, 2005 at 12:08 UTC

    Here is another way to do it. As we have the values in a hash we can easily see if we also have $value+1. It does not come close to the elegance of the regex linked above. sub bar is a drop in replacement for sub foo.

    sub bar { my %hash = @_; foreach (keys %hash) { if (exists $hash{$_+1}) { $hash{$_}="." }else{ $hash{$_}="" } } my @array = ( sort {$a <=> $b} (keys %hash) ); my $string = shift @array; $string.=$hash{$string}; foreach (@array) { $string .= ", $_$hash{$_}"; } $string=~s/(, (\d+)\.)(, (\d+)\.)*/, $2../g; $string=~s/\.\., /../g; print "\n($string)\n"; } # updated ! this is a nicer way to print the output sub bar { my %hash = @_; foreach (keys %hash) { if (exists $hash{$_+1}) { $hash{$_}="." }else{ $hash{$_}="" } } my @array = ( sort {$a <=> $b} (keys %hash) ); my $last=pop @array; my $lastdot=0; print "\n("; foreach (@array) { if ($hash{$_}) { next if $lastdot; print "$_.."; $lastdot++; } else { print "$_, "; $lastdot=0; } } print "$last)\n"; }

    Cheers,
    R.

    Pereant, qui ante nos nostra dixerunt!