How do I tell if a variable has a numeric value in Perl?
Asked Answered
D

15

102

Is there a simple way in Perl that will allow me to determine if a given variable is numeric? Something along the lines of:

if (is_number($x))
{ ... }

would be ideal. A technique that won't throw warnings when the -w switch is being used is certainly preferred.

Dniester answered 15/8, 2008 at 19:43 Comment(0)
V
151

Use Scalar::Util::looks_like_number() which uses the internal Perl C API's looks_like_number() function, which is probably the most efficient way to do this. Note that the strings "inf" and "infinity" are treated as numbers.

Example:

#!/usr/bin/perl

use warnings;
use strict;

use Scalar::Util qw(looks_like_number);

my @exprs = qw(1 5.25 0.001 1.3e8 foo bar 1dd inf infinity);

foreach my $expr (@exprs) {
    print "$expr is", looks_like_number($expr) ? '' : ' not', " a number\n";
}

Gives this output:

1 is a number
5.25 is a number
0.001 is a number
1.3e8 is a number
foo is not a number
bar is not a number
1dd is not a number
inf is a number
infinity is a number

See also:

Valuable answered 26/8, 2008 at 16:53 Comment(6)
And as usual with perl docs, finding the actual definition of what the function does is rather difficult. Following the trail perldoc perlapi tells us: Test if the content of an SV looks like a number (or is a number). "Inf" and "Infinity" are treated as numbers (so will not issue a non-numeric warning), even if your atof() doesn't grok them. Hardly a testable spec...Chelyuskin
The description in Scalar::Util is fine, looks_like_number tells you if your input is something that Perl would treat as a number, which is not necessarily the best answer for this question. The mention of atof is irrelevant, atof isn't part of CORE:: or POSIX (you should be looking at strtod which has subsumed atof and /is/ part of POSIX) and assuming that what Perl thinks is a number is valid numeric input to C functions is obviously very wrong.Meaganmeager
very nice function :) for undef and non number strings returns 0, for number strings returns 1, for integers returns 4352 and for floats returns 8704 :) generally >0 number is detected. I have tested it under linux.Lerner
I like this function in general, but consider large ints. 1000000 is a lot of zeros to keep track of, begging for error, but 1,000,000 is seen as a three-element array, so Perl accepts 1_000_000, but looks_like_number() says no. Makes me sad.Ostosis
Note: hexadecimal strings like 0x12 are not considered numbers by this test.Behave
looks_like_number exposes the internal function that is the same as what Perl uses to magically convert strings to numbers. Underscores, hex, octal, and binary numbers are only allowed as literals in the source, not strings, so e.g. 0+"0x15" does not work right. (Note that looks_like_number("0 but true") is true! :-) reference)Silent
K
29

The original question was how to tell if a variable was numeric, not if it "has a numeric value".

There are a few operators that have separate modes of operation for numeric and string operands, where "numeric" means anything that was originally a number or was ever used in a numeric context (e.g. in $x = "123"; 0+$x, before the addition, $x is a string, afterwards it is considered numeric).

One way to tell is this:

if ( length( do { no warnings "numeric"; $x & "" } ) ) {
    print "$x is numeric\n";
}

If the bitwise feature is enabled, that makes & only a numeric operator and adds a separate string &. operator, you must disable it:

if ( length( do { no if $] >= 5.022, "feature", "bitwise"; no warnings "numeric"; $x & "" } ) ) {
    print "$x is numeric\n";
}

(bitwise is available in perl 5.022 and above, and enabled by default if you use 5.028; or above.)

Korea answered 27/9, 2010 at 17:16 Comment(5)
If I package your routine into a sub, I get a strange behaviour in that it detects non-numeric values correctly, until I try out the first numeric value, which is also detected correctly as true, but then, everything else from there on out is also true. When I put an eval around the length(...) part, however, it works fine all of the time. Any idea what I was missing? sub numeric { $obj = shift; no warnings "numeric"; return eval('length($obj & "")'); }Enthusiast
@yogibimbi: you are reusing the same $obj variable each time; try my $obj = shift;. Why the eval?Korea
oops, my bad, I used my $obj = shift, of course, just did not transfer it correctly from my code to the comment, I edited it a bit. However, sub numeric { my $obj = shift; no warnings "numeric"; return length($obj & ""); } produces the same error. Of course, having a clandestine global variable would explain the behaviour, it is exactly what I would expect in that case, but unfortunately, it's not that simple. Also, that would be caught by strict & warnings. I tried the eval in a rather desperate attempt to get rid of the error, and it worked. No deeper reasoning, just trial & error.Enthusiast
Check it out: sub numeric { my $obj = shift; no warnings "numeric"; return length($obj & ""); } print numeric("w") . "\n"; #=>0, print numeric("x") . "\n"; #=>0, print numeric("1") . "\n"; #=>0, print numeric(3) . "\n"; #=>1, print numeric("w") . "\n"; #=>1. If you put an eval('') around the length, the last print would give a 0, like it should. Go figure.Enthusiast
@Enthusiast seems most likely to me you weren't running the code you thought you wereKorea
V
23

Check out the CPAN module Regexp::Common. I think it does exactly what you need and handles all the edge cases (e.g. real numbers, scientific notation, etc). e.g.

use Regexp::Common;
if ($var =~ /$RE{num}{real}/) { print q{a number}; }
Valida answered 15/8, 2008 at 21:1 Comment(0)
S
14

Usually number validation is done with regular expressions. This code will determine if something is numeric as well as check for undefined variables as to not throw warnings:

sub is_integer {
   defined $_[0] && $_[0] =~ /^[+-]?\d+$/;
}

sub is_float {
   defined $_[0] && $_[0] =~ /^[+-]?\d+(\.\d+)?$/;
}

Here's some reading material you should look at.

Sosa answered 15/8, 2008 at 19:58 Comment(3)
I do think this digresses a bit, especially when the asker said /simple/. Many cases, including scientific notation, are hardly simple. Unless using this for a module, i would not worry about such details. Sometimes simplicity is best. Don't put the chocolate syrup in the cow to make chocolate milk!Unquestionable
'.7' is probably one of the most simple cases that still is missed... better try /^[+-]?\d*\.?\d+$/ for float. My variant, considering scientific notation, too: /^[+-]?\d*\.?\d+(?:(?:e|E)\d+)?$/Forsook
The \d*\.?\d+ part introduces a ReDoS risk. I recommend /^[+-]?(?!\.(?!\d)|$)\d*(?:\.\d*)?$/ or /^[+-]?(?!\.(?!\d)|$)\d*(?:\.\d*)?(?:(?<=[\d.])e[+-]?\d+)?$/i to include scientific notation (explanation and examples) instead. This uses a doubled negative lookahead to also prevent strings like . and .e0 from passing as numbers. It also uses a positive lookbehind to ensure the e follows a number.Behave
T
10

A simple (and maybe simplistic) answer to the question is the content of $x numeric is the following:

if ($x  eq  $x+0) { .... }

It does a textual comparison of the original $x with the $x converted to a numeric value.

Tude answered 17/10, 2012 at 15:10 Comment(4)
That will throw warnings if you use "-w" or "use warnings;".Dniester
The warnings can be removed $x eq (($x+0)."") however a worse problem is that under this function, "1.0" is not numericIngram
it is enough testing $x+0 ne '' . when you will text 0001, then correct number will be checked as non number. the same is when you will test '.05' text value.Lerner
$x eq $x+0 fails for "0.0", ".5", "0x1"…Sweep
C
4

Not perfect, but you can use a regex:

sub isnumber 
{
    shift =~ /^-?\d+\.?\d*$/;
}
Charter answered 15/8, 2008 at 19:49 Comment(1)
Same problem as andrewrk's answer: misses many even simple cases, e. g. '.7'Forsook
D
2

A slightly more robust regex can be found in Regexp::Common.

It sounds like you want to know if Perl thinks a variable is numeric. Here's a function that traps that warning:

sub is_number{
  my $n = shift;
  my $ret = 1;
  $SIG{"__WARN__"} = sub {$ret = 0};
  eval { my $x = $n + 1 };
  return $ret
}

Another option is to turn off the warning locally:

{
  no warnings "numeric"; # Ignore "isn't numeric" warning
  ...                    # Use a variable that might not be numeric
}

Note that non-numeric variables will be silently converted to 0, which is probably what you wanted anyway.

Dixiedixieland answered 15/8, 2008 at 20:56 Comment(0)
D
2

rexep not perfect... this is:

use Try::Tiny;

sub is_numeric {
  my ($x) = @_;
  my $numeric = 1;
  try {
    use warnings FATAL => qw/numeric/;
    0 + $x;
  }
  catch {
    $numeric = 0;
  };
  return $numeric;
}
Domingadomingo answered 10/12, 2010 at 18:58 Comment(0)
D
1

Try this:

If (($x !~ /\D/) && ($x ne "")) { ... }
Darees answered 31/5, 2013 at 16:1 Comment(0)
S
1

I found this interesting though

if ( $value + 0 eq $value) {
    # A number
    push @args, $value;
} else {
    # A string
    push @args, "'$value'";
}
Shortcircuit answered 1/10, 2015 at 14:15 Comment(3)
you need to explain a better , you are saying u find it interesting but does it answer the op? Try to explain why your answer is the solution for the question askedVereen
For example my $value is 1, $value + 0 remains same 1. By comparing against $value 1 equals 1. If the $value is a string say "swadhi" then $value + 0 becomes ascii value of the string "swadhi" + 0 = some other number.Shortcircuit
Doesn't work for ex. if $value is '10.0'.Smearcase
D
0

Personally I think that the way to go is to rely on Perl's internal context to make the solution bullet-proof. A good regexp could match all the valid numeric values and none of the non-numeric ones (or vice versa), but as there is a way of employing the same logic the interpreter is using it should be safer to rely on that directly.

As I tend to run my scripts with -w, I had to combine the idea of comparing the result of "value plus zero" to the original value with the no warnings based approach of @ysth:

do { 
    no warnings "numeric";
    if ($x + 0 ne $x) { return "not numeric"; } else { return "numeric"; }
}
Dailey answered 31/12, 2015 at 13:47 Comment(0)
K
0

You can use Regular Expressions to determine if $foo is a number (or not).

Take a look here: How do I determine whether a scalar is a number

Kagu answered 26/2, 2016 at 16:55 Comment(0)
C
0

There is a highly upvoted accepted answer around using a library function, but it includes the caveat that "inf" and "infinity" are accepted as numbers. I see some regex stuff for answers too, but they seem to have issues. I tried my hand at writing some regex that would work better (I'm sorry it's long)...

/^0$|^[+-]?[1-9][0-9]*$|^[+-]?[1-9][0-9]*(\.[0-9]+)?([eE]-?[1-9][0-9]*)?$|^[+-]?[0-9]?\.[0-9]+$|^[+-]?[1-9][0-9]*\.[0-9]+$/

That's really 5 patterns separated by "or"...

Zero: ^0$
It's a kind of special case. It's the only integer that can start with 0.

Integers: ^[+-]?[1-9][0-9]*$
That makes sure the first digit is 1 to 9 and allows 0 to 9 for any of the following digits.

Scientific Numbers: ^[+-]?[1-9][0-9]*(\.[0-9]+)?([eE]-?[1-9][0-9]*)?$
Uses the same idea that the base number can't start with zero since in proper scientific notation you start with the highest significant bit (meaning the first number won't be zero). However, my pattern allows for multiple digits left of the decimal point. That's incorrect, but I've already spent too much time on this... you could replace the [1-9][0-9]* with just [0-9] to force a single digit before the decimal point and allow for zeroes.

Short Float Numbers: ^[+-]?[0-9]?\.[0-9]+$
This is like a zero integer. It's special in that it can start with 0 if there is only one digit left of the decimal point. It does overlap the next pattern though...

Long Float Numbers: ^[+-]?[1-9][0-9]*\.[0-9]+$
This handles most float numbers and allows more than one digit left of the decimal point while still enforcing that the higher number of digits can't start with 0.

The simple function...

sub is_number {
    my $testVal = shift;
    return $testVal =~ /^0$|^[+-]?[1-9][0-9]*$|^[+-]?[1-9][0-9]*(\.[0-9]+)?([eE]-?[1-9][0-9]*)?$|^[+-]?[0-9]?\.[0-9]+$|^[+-]?[1-9][0-9]*\.[0-9]+$/;
}
Chrisman answered 2/6, 2022 at 17:58 Comment(0)
C
0

I use this to check for positive integers (though it is destructive)...

$value =~ s/[0-9]+//;
$is_numeric = ($value eq '');

(This question highlights perfectly my love-hate relationship with Perl - "makes difficult things easy, impossible things possible ...and what should be trivial things stupidly difficult and expensive")

Connivance answered 9/10, 2023 at 16:24 Comment(0)
C
-1

if ( defined $x && $x !~ m/\D/ ) {} or $x = 0 if ! $x; if ( $x !~ m/\D/) {}

This is a slight variation on Veekay's answer but let me explain my reasoning for the change.

Performing a regex on an undefined value will cause error spew and will cause the code to exit in many if not most environments. Testing if the value is defined or setting a default case like i did in the alternative example before running the expression will, at a minimum, save your error log.

Ceramal answered 21/9, 2013 at 16:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.