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

Given the datastructure
my @fields = ( [ name1 => { start => 0, len=> 10 } ], [ name2 => { start => 8, len=> 3 } ], [ name3 => { start => 11, len=> 14 } ], );
that describes the fields of a fixed length textfile, write a program that constructs a string that can be fed into unpack to get the values out of a line of data.

I'm currently working on my own implementation. But I know someone will beat me. :=)


holli, /regexed monk/

Replies are listed 'Best First'.
Re: Challenge: Construct an unpack string
by atcroft (Abbot) on Sep 26, 2006 at 17:00 UTC

    Assuming this is being extracted from a single text string, and that the positions overlap as shown,

    Position: 0123456789 0123456789 Position: 0123456789 name1: .......... name2: ... name3: ..............
    then, given the string '0123456789abcdefghijklmnopqrstuvwxyz', the following would seem to extract the data as required:

    Results:

    0123456789 89a abcdefghijklmn

    Does that take conform to what you expected?

      No. It should be
      0123456789 89a bcdefghijklmn
      My solution looks like the following (data structure is slightly modified). Also there is a benchmark against Browser_UK's solution:
      use warnings; use strict; use Benchmark; my $text = "0123456789abcdefghijklmn"; my $unpack = ""; my $position = 0; my @fields = ( { name => 'name1', start => 0, len=> 10 }, { name => 'name2', start => 8, len=> 3 }, { name => 'name3', start => 11, len=> 14 }, ); for ( @fields ) { $unpack .= $_->{start} < $position ? 'X' . ( $position - $_->{st +art} ) : $_->{start} > $position ? 'x' . ( $_->{start} - $positi +on ) : ''; $unpack .= 'A' . $_->{len}; $position = $_->{start}+$_->{len}; } print "$unpack\n"; print join ("*", unpack ($unpack, $text)), "\n"; timethese ( 1_000_000, { 'unpack' => sub { my @a = unpack ($unpack, $text); }, 'substr' => sub { my @a = map { substr $text, $_->{start}, $_->{len} } @fie +lds; } }, );
      Results:
      A10X2A3A14 0123456789*89a*bcdefghijklmn Benchmark: timing 1000000 iterations of substr, unpack... substr: 12 wallclock secs (12.80 usr + -0.02 sys = 12.78 CPU) @ 78 +241.14/s (n=1000000) unpack: 9 wallclock secs ( 9.00 usr + 0.00 sys = 9.00 CPU) @ 11 +1098.77/s (n=1000000)
      So it looks like the unpack version is faster, as my guts have said. The cost of assembling the unpack string can be considered irrelevant, because in a real world it would happen only once per file.


      holli, /regexed monk/

        Here's another benchmark.

        use warnings; use strict; use Benchmark qw[ cmpthese ]; my @fields = ( { name => 'name1', start => 0, len=> 4 }, { name => 'name2', start => 3, len=> 7 }, { name => 'name3', start => 8, len=> 3 }, { name => 'name4', start => 0, len=> 10 }, { name => 'name5', start => 5, len=> 20 }, { name => 'name6', start => 11, len=> 14 }, { name => 'name7', start => 9, len=> 13 }, { name => 'name8', start => 2, len=> 2 }, { name => 'name9', start => 1, len=> 10 }, ); open FH, '<', $ARGV[ 0 ] or die $!; cmpthese -3, { 'unpack' => sub { my $unpack = ""; my $position = 0; for ( @fields ) { $unpack .= $_->{start} < $position ? 'X' . ( $position - $_- +>{start} ) : $_->{start} > $position ? 'x' . ( $_->{start} - $po +sition ) : ''; $unpack .= 'A' . $_->{len}; $position = $_->{start}+$_->{len}; } seek FH, 0, 0; while( my $text = <FH> ) { my @a = unpack $unpack, $text; } }, 'substr' => sub { seek FH, 0, 0; while( my $text = <FH> ) { my @a = map{ substr $text, $_->{start}, $_->{len} } @field +s; } } }; close FH; __END__ C:\test>for /l %i in (1,1,6) do holli data\alpha.1e%i C:\test>holli data\alpha.1e1 Rate unpack substr unpack 28603/s -- -80% substr 140302/s 391% -- C:\test>holli data\alpha.1e2 Rate substr unpack substr 400/s -- -26% unpack 540/s 35% -- C:\test>holli data\alpha.1e3 Rate substr unpack substr 40.3/s -- -27% unpack 54.9/s 36% -- C:\test>holli data\alpha.1e4 Rate substr unpack substr 4.06/s -- -28% unpack 5.61/s 38% -- C:\test>holli data\alpha.1e5 (warning: too few iterations for a reliable count) (warning: too few iterations for a reliable count) s/iter substr unpack substr 2.47 -- -27% unpack 1.80 37% -- C:\test>holli data\alpha.1e6 (warning: too few iterations for a reliable count) (warning: too few iterations for a reliable count) s/iter substr unpack substr 25.3 -- -29% unpack 18.0 40% --

        If your files are bigger than a few lines, the unpack version starts winning quite quickly.

        But by nowhere near as much as the other benchmarks would have you believe because once you factor in reading each line from the file, the IO time which is constant for both approaches, the parsing time becomes much less significant. Worth having, but much less than you thought.


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Challenge: Construct an unpack string
by BrowserUk (Patriarch) on Sep 26, 2006 at 17:28 UTC

    Given the form of the input and that the fields can overlap (assuming that was intentional), using unpack for this is awkward and inefficient.

    You have to parse the input spec. from it's absolute pos/length pairs into the relative form (length/backspace) form required by unpack and build the string template. But the unpack has to parse the string template and convert that back into the position/offset pairs to perform the extractions.

    Much simpler, (and with having benchmarked, I'll bet quicker), to use substr for this directly:


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.

      Actually, if you agree that the following code would be valid for a benchmark, then you might be surprised.

      Code:

      Results (1_000_000 iterations):

      Benchmark: timing 1000000 iterations of map_browseruk, map_browseruk_setup, map_jwkrahn, map_jwkrahn_setup, unpack, unpack_setup... map_browseruk: 16 wallclock secs (14.77 usr + 0.04 sys = 14.81 CPU) @ 67521.94/s (n=1000000) map_browseruk_setup: 9 wallclock secs ( 9.27 usr + 0.03 sys = 9.30 CPU) @ 107526.88/s (n=1000000) map_jwkrahn: 29 wallclock secs (23.17 usr + 0.04 sys = 23.21 CPU) @ 43084.88/s (n=1000000) map_jwkrahn_setup: 23 wallclock secs (20.23 usr + 0.03 sys = 20.26 CPU) @ 49358.34/s (n=1000000) unpack: 4 wallclock secs ( 2.43 usr + 0.00 sys = 2.43 CPU) @ 411522.63/s n=1000000) unpack_setup: 1 wallclock secs ( 0.44 usr + 0.00 sys = 0.44 CPU) @ 2272727.27/s (n=1000000)

      I must say I was surprised. I included the times for "setup"-that is, everything but the operation itself, on the idea that perhaps the setup might have taken longer for the one method than another. Thoughts....?

      Update (26-Sept-2006): Updated formatting of results.

      Update (26-Sept-2006): Fixed typo. (Thanks to BrowserUk for pointing it out.)

      Update (26-Sept-2006): Fixed 2nd typo. (Thanks to chargrill for pointing it out.)

        If you hardcode the template to unpack, of course it will be quicker--but when the input data changes, the template won't without programmer intervention--which kinda negates the value of the benchmark.

        Actually, your entire benchmark is pretty suspect and you are comparing a bunch of entirely different things:

        1. A hardcoded unpack.
        2. Two different implementations of the original challenge: parsing the data according to the contents of the input structure.
        3. Three other bit of random code that do nothing in particlar to do with the problem--your "setup" stuff.

        So, no. I wouldn't agree that this is a valid benchmark :) Sorry.

        Also, please use cmpthese for benchmarking, the output from timethese is all but unintelligable.


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.

        What the hell is Q?

        map sprintf( q[x%d a%d X%d], Q{ $_-[1] }{qw[start len] +}, $_->[1] .............................................^

        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Challenge: Construct an unpack string
by jwkrahn (Abbot) on Sep 26, 2006 at 18:04 UTC
    $ perl -le' my @fields = ( [ name1 => { start => 0, len=> 10 } ], [ name2 => { start => 8, len=> 3 } ], [ name3 => { start => 11, len=> 14 } ], ); my $format = join q[ ], map sprintf( q[x%d a%d X%d], @{ $_->[ 1 ] }{ q +w[start len] }, $_->[ 1 ]{ start } + $_->[ 1 ]{ len } ), @fields; print $format; ' x0 a10 X10 x8 a3 X11 x11 a14 X25

      Why don't you use the @ template instead of x and X?

Re: Challenge: Construct an unpack string
by eff_i_g (Curate) on Sep 26, 2006 at 17:08 UTC
Re: Challenge: Construct an unpack string
by hobbs (Monk) on Sep 27, 2006 at 21:02 UTC
    use strict; use Benchmark qw(timethese); # Input data my @fields = ( [ name1 => { start => 0, len => 10 } ], [ name2 => { start => 8, len => 3 } ], [ name3 => { start => 11, len => 14 } ], ); # This was the first idea I had. my $pos = 0; my $template_1; for my $field (@fields) { my $mismatch = $field->[1]{start} - $pos; if ($mismatch > 0) { $template_1 .= "x$mismatch"; } elsif ($mismatch < 0) { $mismatch = 0 - $mismatch; $template_1 .= "X$mismatch"; } $template_1 .= "A" . $field->[1]{len}; $pos = $field->[1]{start} + $field->[1]{len}; } # This was the idea I had very shortly after, while looking at -f pack my $template_2 = join '', map { "\@$_->[1]{start}A$_->[1]{len}" } @fie +lds; # Benchmark it print "t1: $template_1\nt2: $template_2\n"; my $str = "0123456789abcdefghijklmnopqrstuvwxyz"; timethese( 3_000_000, { t1 => sub { my @a = unpack ($template_1, $str) }, t2 => sub { my @a = unpack ($template_2, $str) } } ); # Templates and Benchmark results # t1: A10X2A3A14 # t2: @0A10@8A3@11A14 # Benchmark: timing 3000000 iterations of t1, t2... # t1: 8 wallclock secs ( 8.82 usr + 0.02 sys = 8.84 CPU) @ 339366.5 +2/s (n=3000000) # t2: 9 wallclock secs ( 9.46 usr + 0.00 sys = 9.46 CPU) @ 317124.7 +4/s (n=3000000)