http://www.perlmonks.org?node_id=993918

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

So, I am a perl newb, and am having trouble sorting a mutlidimensional array.

The data is a table, and looks like:

00000(IDR) 86480 22 41.435 40.696 40.728167 0 FRM 3
00002( P ) 35248 24 38.568 39.327 40.641 253 53 FRM 2
.
.
.
00015( B ) 9312 24 45.460 43.808 42.001 409 208 FRM 0

I read it in, line by line, parse it, and want to sort the rows in order of the first column. This is my code:

while (<FH>) { chomp $_; @line_array = split(/[()\t+\s+]/, $_); @full = map {$_ ? $_ : ()} @line_array; @{$data[$readcounter]} = @full; } @data = sort { $a->[0] <=> $b->[0] } @data;

And I get: Modification of a read-only value attempted at...(the sort line)

What am I doing wrong? From the examples and documentation I've looked at, this is the correct way to do it, but apparently for me, its not working...

Thanks.

Replies are listed 'Best First'.
Re: Read Only Error -- Sorting an Array
by NetWallah (Canon) on Sep 16, 2012 at 15:29 UTC
    Please "use strict;" and "use warnings;", particularly if you are a newb.

    Did you increment $readcounter ? Working code:

    use strict; use warnings; my @data; my $readcounter=0; while (<DATA>) { chomp $_; my @line_array = split(/[()\t+\s+]/, $_); my @full = map {$_ ? $_ : ()} @line_array; @{$data[$readcounter++]} = @full; # My preference## $data[$readcounter++] = \@full; # No Copying # -- Better (but more complex next line:) ## $data[$readcounter++] = [ map {$_ ? $_ : ()} split(/[()\t+\s+] +/, $_)]; } @data = sort { $a->[0] <=> $b->[0] } @data; print qq|@{$_}\n| for @data; __DATA__ 00000(IDR) 86480 22 41.435 40.696 40.728167 0 FRM 3 00002( P ) 35248 24 38.568 39.327 40.641 253 53 FRM 2 00001( P ) 35238 24 38.568 39.327 40.641 253 53 FRM 2
    Update: Most perl programmers would avoid the use of a "$readcounter", and write it as:
    push @data, \@full; # Auto-increments, and appends to @data

                 I hope life isn't a big joke, because I don't get it.
                       -SNL

      Wow! That was exactly what I was looking for. Thanks for the helpful hints!

Re: Read Only Error -- Sorting an Array
by tobyink (Canon) on Sep 16, 2012 at 15:38 UTC

    I've not been able to reproduce that error message. Generally that sort of message comes up when you do something like this:

    for my $x ('x') { $x =~ s/x/X/ }

    That is, when you have an alias to a literal, and you try to modify it. You can see a related error message like this:

    'x' =~ s/x/X/;

    Here are a few things wrong with your code...

    • When you're setting variables within a loop, it's almost always a good idea to make them lexical variables scoped to the loop. (i.e. use my.) The use strict 'vars' pragma forces you to declare all variables (except Perl's pre-defined ones like $_) so unless you're writing a 3 line script is always a good idea.

    • One of the nice things about $_ is that it's implicit in so many places. Your first couple of lines within the loop could be written as:

      chomp; my @line_array = split /[()\t+\s+]/;
    • This:

      @full = map {$_ ? $_ : ()} @line_array;

      would be more natural with grep. Use grep to filter a list, and map to transform it.

      @full = grep { $_ } @line_array;

      ... though actually this is probably filtering out more things than you want. You probably don't want to be removing numeral "0" from the list. So...

      @full = grep { defined $_ and length $_ } @line_array;
    • This is not doing what you think it's doing:

      @{$data[$readcounter]} = @full;

      Besides which, $readcounter doesn't seem to be defined or incremented anywhere. What you probably want is:

      push @data, \@full;

    Putting all this together, you get:

    use strict; use Data::Dumper; my @data; while (<DATA>) { chomp; my @line = grep { defined $_ and length $_ } split /[()\t+\s+]/; push @data, \@line; } @data = sort { $a->[0] <=> $b->[0] } @data; print Dumper \@data; __DATA__ 00000(IDR) 86480 22 41.435 40.696 40.728167 0 FRM 3 00015( B ) 9312 24 45.460 43.808 42.001 409 208 FRM 0 00002( P ) 35248 24 38.568 39.327 40.641 253 53 FRM 2

    I'd in fact go further and use Sort::Key to make that sort look a little nicer...

    use strict; use Data::Dumper; use Sort::Key qw(nkeysort_inplace); my @data; while (<DATA>) { chomp; my @line = grep { defined $_ and length $_ } split /[()\t+\s+]/; push @data, \@line; } nkeysort_inplace { $_->[0] } @data; print Dumper \@data; __DATA__ 00000(IDR) 86480 22 41.435 40.696 40.728167 0 FRM 3 00015( B ) 9312 24 45.460 43.808 42.001 409 208 FRM 0 00002( P ) 35248 24 38.568 39.327 40.641 253 53 FRM 2
    perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'

      Your solution with grep is the one I eventually used. With the map implementation, I was throwing away the '0' numerals. Do you know why that is?

      I interpreted map {$_ ? $_ : ()} as saying, this such that this is whitespace. So then, why is it throwing away '0'? And length $_ solves this by keeping things that have a length? Since '0' has length of 1 but '' hasn't a length..?

        map { $_ ? $_ : () }

        means (if we pretend that there's a built-in function is_true()) the same as:

        map { if (is_true($_)) { $_; } else { (); } }

        In Perl, there are five things that are considered false: undef, the empty string, the number zero (including when written 0e0, 0.0, etc), the string "0" (but not strings like "0e0" or "0.0") and objects which overload conversion to boolean to return false.

        The zeros passing through your map are false, thus go down the "else" path.

        So we don't want to test is_true; we want to test whether each string has any characters... i.e. a non-zero length:

        map { length($_) ? $_ : () }

        Any any map which ends with ?$_:() should probably be a grep:

        grep { length($_) }
        perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'
Re: Read Only Error -- Sorting an Array
by choroba (Cardinal) on Sep 16, 2012 at 15:23 UTC
    When running your code, I am not getting the error. Are you sure you posted exactly the code that produced the error?
    BTW, you should use warnings. They will tell you that $readcounter is used only once (you probably wanted $readcounter++? It does not apease the warning, but works.)
    لսႽ† ᥲᥒ⚪⟊Ⴙᘓᖇ Ꮅᘓᖇ⎱ Ⴙᥲ𝇋ƙᘓᖇ
Re: Read Only Error -- Sorting an Array
by Cristoforo (Curate) on Sep 17, 2012 at 01:29 UTC
    Because the leading digits are '0' padded, you can take advantage of that fact. They can be sorted by a simple lexical sort. Just another approach, but a simpler one.  :-)

    map creates an anonymous array reference filled with the fields that have been split and assigned to the array reference. Because you sort the file lines, @sorted collects array references in sorted order;

    #!/usr/bin/perl use strict; use warnings; use Data::Dumper; my @sorted = map [ split /[()\s]+/ ], sort <DATA>; print Dumper \@sorted; __DATA__ 00000(IDR) 86480 22 41.435 40.696 40.728167 0 FRM 3 00015( B ) 9312 24 45.460 43.808 42.001 409 208 FRM 0 00002( P ) 35248 24 38.568 39.327 40.641 253 53 FRM 2
    Output was:
    $VAR1 = [ [ '00000', 'IDR', '86480', '22', '41.435', '40.696', '40.728167', '0', 'FRM', '3' ], [ '00002', 'P', '35248', '24', '38.568', '39.327', '40.641', '253', '53', 'FRM', '2' ], [ '00015', 'B', '9312', '24', '45.460', '43.808', '42.001', '409', '208', 'FRM', '0' ] ];
    Update: Since the you have tabs, perhaps you are dealing with tab separated data, so perhaps a simple split /\t/ would do. If you wanted to get the values in the parentheses, you could easily extract them later. Just a thought. The change would be:
    my @sorted = map{chomp; [ split /\t/ ]} sort <DATA>;
      I like this solution, but it won't work for me because it turns out I needed to do some processing before I sorted. I will definitely keep this in mind for next time.