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

Short and Sweet, here's my conundrum:

I'm using Tie::File to open a file, and insert some text after a particular tag. I need to be able to make some decisions though, and in order to do that I need to know what the *next* element in the array is, and act accordingly.

Looking at my copy of the Perl Bookshelf, I'm not finding anything that applies, the closest thing being $#array. I've cribbed my technique from Dominus' treatise on Tie::File, but it's not an exactly parallel task.

Here's the pseudo-code i'm trying to work out:

### Update the main threads file tie @threads, 'Tie::File', $main_threads_file or syslog('err', "update +_threads couldn't open file: $main_threads_file"); if ($thread == $msg_id) { # put it right at the top, it's a new thread unshift @threads, $link; } else { for (@threads) { if (/$parent/) { if (##next element is "<ul>"##) { #insert $link after the next element (after the <ul>) } else { $_ .= "<ul>"; ## the next element after that .= $link ## the next element after *that* .= "</ul>" } last; } } } untie @threads;
The issue here is that i can't figure out how to say "The element after the current one" or "The second element after the current one".

Replies are listed 'Best First'.
Re: Conditonal insert into Array? (Tie::File)
by Limbic~Region (Chancellor) on May 26, 2003 at 18:20 UTC
    914,
    Change your for loop:
    for my $index (0 .. $#threads - 1) { if ($threads[$index] =~ /$parent/) { if ($threads[$index +1] eq '<ul>')

    Cheers - L~R

      Ah! that's so obvious you could've smacked me in the head with "Programming C" and i still wouldn't have gotten it..

      Thanks!

      (this implies that there isn't a way to say "array element after the current one" though, true?)

(jeffa) Re: Conditonal insert into Array? (Tie::File)
by jeffa (Bishop) on May 26, 2003 at 19:00 UTC
    Problem solved, but if it's HTML you are parsing, you really should use a parser: Sometimes i still use regexes or treat the file as an array as you have done, but for any script that is going to be used 'in production', use a parser. Heck, use a parser anyway -- it's harder, but you get better with practice (and smarter too).

    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: Conditonal insert into Array? (Tie::File)
by u914 (Pilgrim) on May 26, 2003 at 20:50 UTC

    Resolution:

    Below is the tested and working code (thanks guys!)

    Note the newline chars (\n), those are required to tell Tie::File that these will be new lines and therefore new records in the array.

    Even though i'm fooling with @array[$i + n] here, there's no chance of running off the end of the array because the test will fail negative before any action is taken on non-existant elements beyond the last record in the array.

    The suggestions about HTML::Parser and so forth are well recieved, but since all i'm doing here is *generating* the html snippets, i don't think the Parser is really the right thing.

    ### Update the main threads file tie @threads, 'Tie::File', $main_threads_file or syslog('err', "update +_threads couldn't open file: $main_threads_file"); if ($thread == $msg_id) { # put it right at the top, it's a new thread unshift @threads, $link; } else { for my $i (0 .. $#threads) { if ($threads[$i] =~ $parent) { if ($threads[$i+1] eq "<ul type=\"disc\">") { # insert $link after the next element (after the <ul>) $threads[$i+1] .= "\n$link"; } else { # insert three new lines/records after the parent $threads[$i] .= "\n<ul type=\"disc\">\n$link\n</ul>\n" +; } last; } } } untie @threads;

    Anyhow, thanks again for everyone's help! I hope this thread is useful to someone, someday.

    Cheers!
    914

      Test Script

      As requested, i've dummied up a short test script to show the functionality of the above snippet...

      The instructions are embedded as comments (i know, should be POD..) in the script, and it needs no inputs beyond fiddling with the indicated 3 variables between runs.

      $msg_id should be incremented with each running of the script.
      $thread should be the last $msg_id, plus one to make a new top-level post, or it should inherit the $thread of the parent if a reply
      $parent can be set to the $thread_$msg_id of any arbitrary existing post to create a reply.

      When replying, the $thread should always match the parent's $thread, but the $msg_id is the current $msg_id, in the incremental sequence. In real life all of this is accomplished by simply writing a number to a file, and incrementing it each time the script is called.

      It will produce it's own output on first running, and should be allowed to continue to work on that file for subsequent runs. The output is best understood if viewed with a web browser.

      In real life, this output file is inserted into another .html file via SSI, and is wrapped in <ol> </ol> tags, which will number all of the top-level posts. Try it manually and you'll see what i mean.

      #!/usr/bin/perl -w use Tie::File; use strict; ### ### This is a complete, self-contained dummy script to ### test the insert functionality of this sub... ### ###################################################################### +#### ### ### These vars should be changed to demonstrate different behaviours ### my $thread = "208"; # is the thread identifier my $msg_id = "211"; # is this message's unique number my $parent = "208_208"; # is the message_id of the parent post ## To create new top-level post set $thread=$msg_id, $parent is ununse +d ## and should be undef ## All replies to given top-level post should carry same $thread value +, ## whereas $msg_id is incremented ## Set $parent to: ($thread . "_" . $msg_id) to make a reply to the ## post which carries that message_id string in the html comment ## $msg_id should be unique for each post ($msg_id++) ## $thread should be unique for each thread ###################################################################### +### ### ### These vars are (relatively) static ### my @threads; # the array we will use my $main_threads_file = "threads.html"; # the threads file we will +work on my $link = "<li><!-- message_id=$thread" . "_" . "$msg_id --><a href= +\"http://link_to_post\">dummy link to message $thread - $msg_id</a>"; ### Update the main threads file tie @threads, 'Tie::File', $main_threads_file or syslog('err', "update +_threads couldn't open file: $main_threads_file"); if ($thread == $msg_id) { # put it right at the top, it's a new thread unshift @threads, $link; } else { for my $i (0 .. $#threads) { if ($threads[$i] =~ $parent) { if ($threads[$i+1] eq "<ul type=\"disc\">") { # insert $link after the next element (after the <ul>) $threads[$i+1] .= "\n$link"; } else { # insert three new lines/records after the parent $threads[$i] .= "\n<ul type=\"disc\">\n$link\n</ul>\n" +; } last; } } } untie @threads;
      This isn't really the final state of the production code for this, as it also needs to understand that sometimes replies should not be listed on the main threads file, but rather only the total *number* of replies, and users must click on the parent link to expand the thread. If anyone's interested i can post the complete code including the flat-threads-file aware stuff...

        Well, that is not exactly how i would have solved the problem, but if it works for you, then good. If you are curious as to how i would solve the problem, then read on.

        Use A Database!

        That's right. Store your data in place that is used to store data. Display your data in a place that is used to display data. Don't mix the two. Repeat. Don't mix content and presentation. You say don't have access to a database? Get one! Between SQLite, MySQL, and PostgreSQL you have no excuses not to "upgrade" your skills. ;)

        Of course, seeing as how you have come along this far with your solution, switching now probably seems impossible. I understand that completely, but here is some code that keeps track of "threads" via DBI.pm and CGI.pm. I used the MySQL database with the following table creation syntax:

        CREATE TABLE node ( id int(10) NOT NULL auto_increment, name varchar(64) NOT NULL default '', parent int(10) default '0', PRIMARY KEY (id) );
        The following CGI script will print a form allowing the user to enter a title for the new "message" and an optional "parent id" to link to. It's id is auto-generated by the database and the user can't change it. Any messages are printed out via a recursive subroutine, but first they are fetched from the database and placed into a datastructure whose structure was concocted by me in a rather quick and dirty fashion. Improvements are always welcome.
        use strict; use warnings; use DBI; use Data::Dumper; use CGI::Pretty qw(:standard *ul); my %node; my $dbh = DBI->connect( qw(DBI:vendor:database:host user pass), {RaiseError=>1} ); print header(),start_html('messages'), h1('messages'),start_form, 'New thread title: ', textfield('name'),br, 'Parent thread: ', textfield('parent'),br, submit('go'),end_form, ; insert_message(param('name'),param('parent')) if param('go'); display_messages(); print end_html; $dbh->disconnect; sub display_messages { my $sth = $dbh->prepare('select id,name,parent from node'); $sth->execute; while (my $row = $sth->fetchrow_hashref()) { $node{$row->{id}} = { name => $row->{name} }; push @{$node{$row->{parent}}->{children}}, $row->{id} if $row->{parent}; } # uncomment this to see the datastructure #print pre(Dumper \%node),hr; print start_ul; display_thread($_) for sort {$a<=>$b} keys %node; print end_ul; } sub display_thread { my $id = shift; my $node = delete $node{$id}; return unless $node; print li("$id: " . $node->{name}); return unless $node->{children}; print start_ul; display_thread($_) for @{$node->{children}}; print end_ul; } sub insert_message { my ($name,$parent) = @_; my $sth = $dbh->prepare(' insert into node(name,parent) values(?,?) '); $sth->execute($name,$parent); } # this sub is not used, but i'll leave it anyway ;) sub get_last_id { return $dbh->selectcol_arrayref('SELECT last_insert_id()')->[0]; }

        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)
        

        If i'm interpreting your code and comments correctly, you are expecting that

        $threads[$i] .= "\n<ul type=\"disc\">\n$link\n</ul>\n";

        will insert 3 (or 4?) new lines after line $i. Whilst this may work, at least some of the time, it is TWWTDI (the wrong way to do it:).

        From the Tie::File pod:

        Inserting records that contain the record separator string is not supported by this module. It will probably produce a reasonable result, but what this result will be may change in a future version. Use 'splice' to insert records or to replace one record with several.

        You should replace that line with

        splice @threads, $i, 0, '<ul type=\"disc\">', "$link",'</ul>';

        Which says 'insert the list (second line above) at position $i, whilst deleting nothing (0)'.

        See perlfunc:splice for more details.


        Examine what is said, not who speaks.
        "Efficiency is intelligent laziness." -David Dunham
        "When I'm working on a problem, I never think about beauty. I think only how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong." -Richard Buckminster Fuller