http://qs1969.pair.com?node_id=79320

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

The apprentice, after taking aspirin for the pain in his forehead, finally caves after several hours...

Some time ago, a certain monk (privately) advised the following after seeing my poor attempts to verify that a certain CGI parameter was actually a number, e.g. a non-blank, Base-10 numeric value that may (or may not) contain decimal points and/or hyphens masquerading as negative number indicators:

Best perlop check? Try this: eval {local $^W = 1; $num + 0}; # Now check $@

Okay. So I'm trying to suit the action to these words and am not having much success:

#!/usr/bin/perl -wT use strict; $|++; use CGI qw( :standard ); my $result = ''; my %params = param(); if ( my $error = cgi_error() ) { print header( -status => $error ); exit 0; } my $invalue = param( 'INVALUE' ); if ( defined $invalue ) { eval{ local $^W = 1; $invalue + 0 }; ### Is Number? (Line 19) if ( $@ ) { $result = "$invalue is *not* a Number; Details: $@" } else { $result = "$invalue appears to be a number."; } } else { $result = "Enter a value to test." } print header( "text/html" ), start_html('Test'), h1('Validation'), start_form(), p( $result ), p( "Please enter a number to test: ", textfield( 'INVALUE', '' ) + ), p, submit, end_form, hr,"\n"; print end_html; exit 1;

As the more experienced of you will recognize, this is resulting in values such as "20" as being recognized as a number. Sadly, so are "Shoe", "Fred" and other patently non-numeric values, by which (just to be perfectly clear to the most rententive) are *not* numeric in the locale, idiom, and operational parameters that I'm trying to communicate in.

My petitions are:

Yes, I am aware of the Cookbook and its examples; you'll note this monk had a different approach in mind. I'm trying to get it to work. A gentle nudge would be greatly appreciated.

Many thanks in advance...

--f

Replies are listed 'Best First'.
Re: Validating Numbers in CGI Script?
by chipmunk (Parson) on May 10, 2001 at 07:46 UTC
    The mysterious monk's advice is incomplete. Trying this approach from the command line shows the problem:
    #!perl for $invalue (qw/ 1 -10 abc /) { eval{ local $^W = 1; $invalue + 0 }; ### Is Number? if ( $@ ) { print "$invalue is *not* a Number; Details: $@\n"; } else { print = "$invalue appears to be a number.\n"; } }
    produces:
    1 appears to be a number. -10 appears to be a number. Argument "abc" isn't numeric in add at num.pl line 3. abc appears to be a number.
    The reason for this is that eval only traps errors, not warnings. The warning is printed directly to STDERR and $@ remains empty.

    To fix this, you need to force the warning into an error. (And add an empty list assignment to silence the void context warning.):

    #!perl for $invalue (qw/ 1 -10 abc /) { eval{ local $SIG{__WARN__} = sub { die @_ }; local $^W = 1; () = $invalue + 0 }; ### Is Number? if ( $@ ) { print "$invalue is *not* a Number; Details: $@\n"; } else { print "$invalue appears to be a number.\n"; } }
    produces:
    1 appears to be a number. -10 appears to be a number. abc is *not* a Number; Details: Argument "abc" isn't numeric in add at + num.pl line 4.

      Note, the new 'use warnings' pragma allows for various fine tuning of default and optional warnings -- including promoting any warning category into FATALs for a given lexical scope. So chipmunk's example can also be written as (assuming 5.6+):

      for $invalue (qw/ 1 -10 abc /) { eval{ use warnings FATAL=>qw/numeric/; $invalue + 0 }; ### Is Number? if ( $@ ) { print "$invalue is *not* a Number; Details: $@\n"; } else { print "$invalue appears to be a number.\n"; } }

      For details see the perllexwarn manpage, and the docs for the warning.pm module.

Re: Validating Numbers in CGI Script?
by MeowChow (Vicar) on May 10, 2001 at 08:24 UTC
    You may be interested in an article on this topic, written by Tom Christiansen a while back.
       MeowChow                                   
                   s aamecha.s a..a\u$&owag.print
Re: Validating Numbers in CGI Script?
by perlmonkey (Hermit) on May 10, 2001 at 08:11 UTC
    As chipmunk lays out, the eval manpage says:
    Beware that using eval() neither silences perl from printing warnings to STDERR, nor does it stuff the text of warning messages into $@. To do either of those, you have to use the $SIG{__WARN__} facility.
Re: Validating Numbers in CGI Script?
by larryk (Friar) on May 10, 2001 at 14:23 UTC
    the following regex looks for a non-numeric character (if it fails then they are all digits - and therefore acceptable) in the $invalue string:
    my $invalue = param( 'INVALUE' ); if ( defined $invalue ) { if ( $invalue =~ /\D/ ) { $result = "$invalue is *not* a Number@" } else { $result = "$invalue is a number."; } } else { $result = "Enter a value to test." }
    The same could be achieved with $invalue =~ /^\d+$/ but as we all know...
    $less->{'characters'}->{'typed'} == `"time in the pub" | more`; # :-D
    hope this helps

    Update: _doh_!! just re-read your post and realised I would have to quickly whip up a regex to allow "-"s and a possible "." so here it is...

    /^(?:-)?\d+(?:\.\d+)?$/
    oh, and for people who like to leave off a leading 0 on decimals > -1 but < 1...
    /^(?:-)?(?:\d+(?:\.\d+)?|\.\d+)$/
    I would be interested in an optimisation of this if anyone can do better.
      I know it's in the FAQ, but I will construct here on my own, for the good of all.
      /^(?:-)?(?:\d+(?:\.\d+)?|\.\d+)$/ # becomes /^-?(?:\d+(?:\.\d+)?|\.\d+)$/ # UPDATE! /^-?(?=\d|\.\d)\d*\.?\d+$/
      The look-ahead is used to remove the redundancy in your regex. It basically ensures there's either a digit, or a dot and THEN a digit. Then, it matches digits and/or a dot and digits, which we know will be valid.

      japhy -- Perl and Regex Hacker
        Of course, if you're going to use a regex(p), you might as well make it easy on yourself:

        use Regexp::Common;

        if ($thing =~ /^$RE{num}{dec}$/) {
            ...
        }

        (this particular example, of course, requires DWIM.pm... :-)

        dha

        I can only refer you to the article that MeowChow mentioned. Taking a look at Tom Christiansen's list of benefits that someone who rolls their own RE usually misses, you get zero out of 3 of them.

        RE's are not always the right answer...

        UPDATE
        Changed the link because the page moved. (Thanks footpad.)

        cheers for that - don't know what I was thinking with the (?:-)? bit - maybe the smiley just cheered us all up!

        correct me if i'm wrong but won't your regex pick up a number which finishes a sentence _and_ the full stop (period to americans)? this would lead to perl treating it as a string in some cases (eg. ++ or --).

Re: (Buzzcutbuddha - Pack is an alternative) Validating Numbers in CGI Script?
by buzzcutbuddha (Chaplain) on May 11, 2001 at 00:34 UTC
    This solution might not be optimal (as it is not very strict and not exactly what you want) but you could also use pack and unpack.

    As you can see from the code below, I tested the eval, the regex posted by japhy, and two versions of pack,
    one that stores the number as a double precision float (packstuffD), and one that stores the number as a single precision float (packstuffF).

    The regex outputs:
    4.34 -2.33333 5 20
    Whereas both of the packs output:
    4.34 -2.33333 0 5 0 5 12 0 0 0 20 4.3e+015

    As you can see, anything that is not a number gets turned into a zero by the pack/unpack process, which is good
    and harmless, and only returns valid Perl numbers which is not quite what you wanted
    so this became was a fun little exercise for myself!
    use Benchmark; sub regexstuff() { map {$foo = $_ if /^-?(?=\d|\.\d)\d*\.?\d+$/} (qw/4.34 -2.33333 abc 5 maurice 5_5_4_1 12peter peter12 shoe fred 20 4.3E15/); } sub packstuffD() { map { $foo = unpack ("d", pack ("d", $_)), "\n" } (qw/4.34 -2.33333 abc 5 maurice 5_5_4_1 12peter peter12 shoe fred 20 4.3E15/); } sub packstuffF() { map { $foo = unpack ("f", pack ("f", $_)), "\n" } (qw/4.34 -2.33333 abc 5 maurice 5_5_4_1 12peter peter12 shoe fred 20 4.3E15/); } sub evalstuff() { map { eval{ local $^W = 1; $_ + 0 } } (qw/4.34 -2.33333 abc 5 maurice 5_5_4_1 12peter peter12 shoe fred 20 4.3E15/); if ( $@ ) { $result = "$invalue is *not* a Number; Details: $@" } else { $result = "$invalue appears to be a number."; } } timethese ( 100000, { packstuffD => "packstuffD", regex => "regexstuff", eval => "evalstuff", packstuffF => "packstuffF" } ); eval: 13 wallclock secs (11.97 usr + 0.02 sys = 11.99 CPU) @ 83 +42.37/s (n=100000) packstuffD: 11 wallclock secs (10.06 usr + 0.00 sys = 10.06 CPU) @ 99 +45.30/s (n=100000) packstuffF: 10 wallclock secs (10.63 usr + 0.02 sys = 10.65 CPU) @ 93 +93.20/s (n=100000) regex: 11 wallclock secs (11.01 usr + 0.01 sys = 11.02 CPU) @ 90 +77.71/s (n=100000)
Re: Validating Numbers in CGI Script?
by markjugg (Curate) on May 11, 2001 at 02:20 UTC
    HTML::FormValidator can extract a number from a CGI input field for you. I have some more info and examples of using it here.

    -mark