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

Dear Monks,
I am looking for a “true once” Boolean value, to test within a loop. I need something that will give me a TRUE value the first time, and FALSE the remaining times or vice-versa. I know that this need is shared by somebody else (Re: Being Popular) and I wonder if somebody has any idioms to offer.
Here is a practical example. I need to create a MySQL INSERT statement, exploiting the special syntax to have multiple values within the same instruction.
INSERT INTO tablename VALUES (value1, value2, value3), (value1, value2, value3), (value1,value2, value3);
To create such statement, I am parsing the raw data from an input file, and I have the problem of inserting commas after each set of values, except the last one (or we could say before each set, except the first one).
I came up with two solutions. The first one has a TRUE scalar that becomes FALSE upon being called:
my $should_print = 1; while (...) { print "'\n" if ($should_print? --$should_print : 1); # munging ... }
A little tricky, but it works. The first call will change the TRUE value to a FALSE one, thus preventing the print from happening. Each successive call will return 1 (TRUE) with the help of the ternary operator, unleashing the print.
The second solution will get over the TRUE-once need by means of grep and map, and although the result is exactly the same, it looks like something that I should send to an Obfuscated Code Contest.
(Thinking again, though, also the first solution, where a Boolean test returns the opposite of a scalar value, would be a good candidate for an OCC.)
The following program illustrates the two solutions. I would be glad to find out if there is anything easier to read or to implement.
Personally, I like the while loop, but I am keeping an open mind on alternatives. Any hints?
Thanks
gmax

#!/usr/bin/perl -w use strict; print <<MYSQL; CREATE DATABASE IF NOT EXISTS chess; USE chess; DROP TABLE IF EXISTS eco_class; # create table statement CREATE TABLE eco_class ( eco_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, eco CHAR(3) NOT NULL, opening VARCHAR(50) NOT NULL, variation VARCHAR(50), moves VARCHAR(200) NOT NULL, KEY eco(eco), KEY opening(opening), KEY variation (variation), KEY moves(moves)); # create the header for the insert statement INSERT INTO eco_class VALUES MYSQL my @line; # input fields my $should_print =1; # flag for first line with_while(); # comment to test the alternative #with_map(); # uncomment to test the alternative print ";\n"; sub with_while { while (<DATA>) { chomp; # removes trailing newline next if /^\s*$/; # skips blank lines # skips the first line print ",\n" if ($should_print? --$should_print : 1); @line = split /;/; # gets individual records print "(NULL, ", quotrim($line[0]), ", ", quotrim($line[1]), ", "; # the third field may be missing print scalar @line < 4 ? "NULL, " . quotrim($line[2]) : quotrim($line[2]) . ", " . quotrim($line[3]), ")"; } } sub with_map { print join ",\n" , grep { defined $_ } map { chomp; # removes trailing newline if (/^\s*$/ ) { # skips blank lines $_ = undef; } else { @line = split /;/; # gets individual records $_ = "(NULL, " . quotrim($line[0]) . ", " . quotrim($line[1]) . ", "; $_ .= scalar @line < 4 ? "NULL, " . quotrim($line[2]) : quotrim($line[2]) . ", " . quotrim($line[3]), ")"; $_ .= ")"; } } <DATA>; } sub quotrim { # quote and trim :-> $_[0] =~ s/\"/\\"/g; $_[0] =~ s/^\s+//; $_[0] =~ s/\s+$//; return '"' . $_[0] . '"'; } #sample data follows __DATA__ A00;Polish (Sokolsky) opening;1.b4 A00;Polish;Tuebingen Variation ;1.b4 Nh6 A00;Polish;Outflank Variation ;1.b4 c6 A00;Benko's Opening;1.g3 A00;Lasker simul special;1.g3 h5 A00;Benko's Opening;reversed Alekhine;1.g3 e5 2.Nf3 A00;Grob's attack;1.g4 A00;Grob;Spike attack;1.g4 d5 2.Bg2 c6 3.g5 A00;Grob;Fritz gambit;1.g4 d5 2.Bg2 Bxg4 3.c4 A00;Grob;Romford counter-gambit;1.g4 d5 2.Bg2 Bxg4 3.c4 d4 A00;Clemenz (Mead's, Basman's or de Klerk's) Opening;1.h3 A00;Global Opening;1.h3 e5 2.a3 A00;Amar (Paris) Opening;1.Nh3 A00;Amar gambit;1.Nh3 d5 2.g3 e5 3.f4 Bxh3 4.Bxh3 exf4

Replies are listed 'Best First'.
Re: A TRUE-once variable
by gmax (Abbot) on Dec 06, 2001 at 13:31 UTC
    WOW!
    I wasn't expecting such abundance of answers!
    Thanks to everybody, folks! It's a real pleasure asking something in this Monastery.
    Actually, I would say that the very idea of posting a question here has a truly beneficial effect, long before I start writing my stuff. Knowing that I am going to submit my thoughts to such attentive and wise public has an healthy effect on my code, pushing me to clean it and to check every detail before posting. When I first decided to ask for your advice, my code was much messier than this. Just preparing for posting had the positive side effect of improving the unclean code to the shape that you have seen. I leave to your imagination to decide how it was before :-)

    And now, some cumulative comments on your useful contributions:
    Lucky and japhy offered basically the same advice
    print unless $should_not_print++; print if !$seen++;
    This idea has crossed my mind, but I rejected it on the grounds that I am incrementing a variable without need. Influence from C and Pascal, I guess, where you learn that by incrementing an integer to no end you can eventually reach the upper boundary of the integer itself and turn it into 0 again. This was something that used to be true when integers had an upper boundary of 0xffff. It is not the case in Perl, and I can see that these solutions have some grace that is lacking in my implementation. However, I have some sort of internal taboo against this incrementing, and I can't resolve myself to adopt it. Excessively careful or simply fool? I don't know the internals of Perl well enough to decide. Pending further knowledge, I would avoid it.

    jeffa offered an OO solution.
    It does exactly what I need, at the price of calling a method. I have been using OOP for long time, and I am fully convinced that this should be the right approach.

    A further improvement came from clintp
    The Tie solution is definitely my favorite. The same idea as jeffa, but a tied scalar in this context improves the semantics of the program:
    tie my $first, "Trueonce"; while (<DATA>) { print if !$first; }
    I love it. Thanks a lot.
    Rhandom gave me food for thought, inviting me to rephrase my implementation. It's a healthy thing and I'll try not to forget it.
    I tried this approach in a similar program before, and it worked fine. I abandoned it because I had problems when dealing with large quantities of data. It was a different case and has little to share with this one, because I have to keep my data within the amount of memory that my database can handle at once (16 MB). The bottom line is that I discarded the array path because of memory problems that I had in a similar case. I should think deeper.

    perrin offered a solution on the same tune. I like his splice approach. The same remarks as the previous one, concerning my unjustified fear about memory problems.

    George_Sherston gave me two pieces of advice. The first one, about using two subs for the two different actions in my task, I found very much sensible and in line with good principles of software engineering. Sometimes I get carried away by the power Perl is offering that I forget the simple things I am able to do in other languages and could be workable in Perl as well. More food for thought and an invitation to balancing the ingredients in my scripts. Thanks.

    I can't fully appreciate the second piece in the same manner, since it doesn't seem to solve my problem. I might misunderstand your hint, but it seems that you are inviting me to create one query for each line of text in my input file.
    I am not sure that I can simplify my task with DBI (which is happily cruncing my data every day), since I would like to avoid creating 10_000 queries, and instead I need to create a query of 10_000 lines, which I can feed to the DB either through DBI or the standard MySQL client. It's true that DBI can quote my data much better, but it doesn't have any built-in facility for multi-line queries. Thanks anyway for taking the pains of giving me a solution, and sorry for not saying anything about DBI from the beginning. I did not include DBI in my request because I wanted to concentrate on the main issue. I was feeling that my question was already too long and I didn't want to overdo.

    Finally, danger gave me three lovely technical tricks that is what I was hoping for when I posted my question. I completely overlooked the “..” operator, and now I should start reviewing the perlop manpage. Many thanks.

    I should say that I could not have hoped for anything better from this question. I got at least two immediately workable hints, and plenty of insight on how to re-think my task.

    I wish a pleasant day to all the kind contributors to this node.
    Gmax
Re: A TRUE-once variable
by Lucky (Scribe) on Dec 05, 2001 at 21:32 UTC
    'with while' solution will be more elegant with this modification:
    while (...){ print",\n" unless $should_not_print++; # munging ... }
Re: A TRUE-once variable
by clintp (Curate) on Dec 05, 2001 at 21:39 UTC
    Probably overkill. Try:
    sub Trueonce::TIESCALAR { my $s=1; bless \$s, $_[0]; } sub Trueonce::FETCH { my($self)=@_; local $_=$$self; $$self=0; return $_; } sub Trueonce::STORE {}
    Include the code above in your program somewhere early, and use it like this:
    tie $f, "Trueonce"; print $f, "\n"; # 1 print $f, "\n"; # 0 print $f, "\n"; # 0 print $f, "\n"; # 0
    For some reason I'm in a tie() mood today...
Re: A TRUE-once variable
by Rhandom (Curate) on Dec 05, 2001 at 22:00 UTC
    I know this doesn't fit the answer of finding a true once (the examples above me do this well), but maybe you should rethink the way you do your flow. Instead of adding a comma and a value every time the trueonce variable is false, why not push everything onto an array and join it for the final query.

    my @values = (); while($whatever){ my $value = $got_from_somewhere; push @values, $value; } my $query = "INSERT INTO somewhere VALUES ("; $query .= join(" ,", @values); $query .= ")";

    my @a=qw(random brilliant braindead); print $a[rand(@a)];

(jeffa) Re: A TRUE-once variable
by jeffa (Bishop) on Dec 05, 2001 at 21:36 UTC
    OO to the rescue:
    use strict; my $bool = TrueOnce->new(); print $bool->value(), "\n" for (0..4); package TrueOnce; sub new { bless { true=>1 }, shift } sub value { my $self = shift; my $val = $self->{'true'}; $self->{'true'} = 0; return $val; }
    UPDATE: /me smacks forehead
    Do what Rhandom said. Sometimes finding the solution to a probably is not as good as solving the RIGHT PROBLEM. Rhandom++

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    F--F--F--F--F--F--F--F--
    (the triplet paradiddle)
    
Re: A TRUE-once variable
by japhy (Canon) on Dec 05, 2001 at 22:15 UTC
    The typical idiom is some variable named $seen:
    while (<FOO>) { print if !$seen++; # ... }

    _____________________________________________________
    Jeff[japhy]Pinyan: Perl, regex, and perl hacker.
    s++=END;++y(;-P)}y js++=;shajsj<++y(p-q)}?print:??;

Re: A TRUE-once variable
by Rhandom (Curate) on Dec 05, 2001 at 22:05 UTC
    Yet another way... use a closure...

    use strict; my $n = do{ my $x=0; sub{$x++} }; print "[".join("][", &$n, &$n, &$n)."]\n

    my @a=qw(random brilliant braindead); print $a[rand(@a)];

Re: A TRUE-once variable
by danger (Priest) on Dec 06, 2001 at 08:42 UTC

    Perl doesn't have a "true-once" variable, however there are many ways to get the behaviour you want --- and several have been mentioned, including the most pertinent one: namely that an algorithm with join is probably your best option.

    That said, there are a few ways to get the 'once-only' behaviour you are looking in your case without creating new synthetic variables. First you can use the flip-flop operator:

    #!/usr/bin/perl -w use strict; while(<DATA>){ chomp; print ",\n" unless 1..1; print; } __DATA__ foo bar blah

    In scalar context the range .. op is the flip-flop, and when using constant expressions on either side, they are automatically compared to the builtin $. variable. So the above flip-flop is only true for the first record read. This of course leads to the more explicit and only slightly longer direct comparison with $. itself:

    #!/usr/bin/perl -w use strict; while(<DATA>){ chomp; print ",\n" unless $. == 1; print; } __DATA__ foo bar blah

    But the idea of "once-only" can also be expressed by using the special ?? delimiters for a regex: with such delimiters, the regex will match only once --- so, using an expression that will succeed will also get you what you want:

    #!/usr/bin/perl -w use strict; while(<DATA>){ chomp; print ",\n" unless ?^?; print; } __DATA__ foo bar blah
      In my opinion the ?^? trick is a bit too much of... a trick.

      First of all, one should always do reset() after a use of ??. But the status of ?? is package scoped, so you might get scary side-effects if you use a function that uses ?? inside a loop where you used the first ??.

      For instance, say that you modify your last snippet:
      sub prettify { # Just code that uses ??. } while(<DATA>){ chomp; print ",\n" unless ?^?; print prettify($_); } __DATA__ foo bar blah
      Evil indeed.

      This is the same problem as with not localized usage of $_, e.g. when people do while (<FOO>) {} without an explicit local() on $_. (I know no way to localize the status of ??.)

      -Anomo
Re: A TRUE-once variable
by perrin (Chancellor) on Dec 05, 2001 at 22:54 UTC
    A join is the right thing to use. Your grep/map thing looks bad because you're trying to do too much at once.
    sub with_no_map { my @lines; while <DATA> { chomp; # removes trailing newline next unless /\S/; # skips blank lines @values = split /;/; # gets individual records @values = map {quotrim($_)} @values; # quote vals # normalize number of values by padding with NULL if (scalar(@values) == 3) { splice(@values, 2, 0, 'NULL'); } my $values_group = '(NULL, ' . join(',', @values) . ')'; push @lines, $values_group; } return join(",\n", @lines); }
    Untested, but hopefully you get the gist.
Re: A TRUE-once variable
by baku (Scribe) on Dec 06, 2001 at 03:10 UTC

    Well, *I* never use it, because I have to run  man perlop every time I see one, but...

    In scalar context, ``..'' returns a boolean value. The operator is bistable, like a flip-flop ... It can test the right operand and become false on the same evaluation it became true (as in awk), but it still returns true once.

    Anybody who can get it to work, though? I can't get what I expect from, e.g.

     $\ = ', '; for (1..10) { print if (1..0) }

    Rhandom is right, revising the algorithm is likely the way to go, but I'd like to learn how to use this as a cool way to write "the first time..."

      This is derived from an example in the Perl Developer's Dictionary and it's a cool trick if you can stomach it.

      $a=0; $_=0; while($_++<10) { if (! $a..(!$a)) { print "True!\n"; } else { print "False!\n"; } $a++; }
      The if with the .. operator there returns true exactly once (so long as $a isn't externally modified) for the entire life of the program.

      You can make the example a bit more concise using this:

      $a=0; $_=0; while($_++<10) { if (! $a..(!$a++)) { print "True!\n"; } else { print "False!\n"; } }
      But using an auto-increment in an expression where the target variable appears twice makes me nauseous. Probably indigestion from my C days.
Re: A TRUE-once variable
by George_Sherston (Vicar) on Dec 05, 2001 at 22:49 UTC
    To answer the question you asked, I like the ugly but effective:
    my $do = 1; for (0...9) { stuff_to_do_always(); if ($do) { stuff_to_do_first_time(); } else { stuff_to_do_other_times(); } $do = 0; }
    I know this isn't too subtle, but I find I can look at it and know what it does.

    To answer the question you didn't ask, I think the problem wd go away if you used DBI.pm. Then you could use placeholders, which put the commas in for you (and also handle the quoting issues which you do with quotrim).

    This does what (I think) you want:
    use DBI; my $dbh = DBI->connect("DBI:mysql:database=chess", "usr", "pwd"); # o +r whatever my @line; my $sth; while (<DATA>) { # your parsing: chomp; next if /^\s*$/; @line = split /;/; # these lines deal with the only variation in # your data I can see - there may be others: if (@line == 3) { push @line, $line[2]; $line[2] = 'NULL'; } # and finally, DBI takes the strain: $sth = $dbh->prepare("INSERT INTO eco_class (eco_id,eco,opening,va +riation,moves) VALUES (NULL,?,?,?,?)") or die $dbh->errstr; $sth->execute(@line) or die $dbh->errstr; }


    § George Sherston
Re: A TRUE-once variable
by Lucky (Scribe) on Dec 05, 2001 at 22:14 UTC
    Another solution of your problem:
    while (<DATA>){ chomp; # removes trailing newline next if /^\s*$/; # skips blank lines @x=map {quotrim($_)} split /;/; push @line, \@x; # gets individual records } print join ",\n", map { "( ${\(join ',', @$_)} )" } @line; print ";\n";
Re: A TRUE-once variable
by diotalevi (Canon) on Sep 21, 2005 at 21:50 UTC

    Heh

    $| = 0; sub true_once { not $|++; }