99

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.

brian d foy
  • 129,424
  • 31
  • 207
  • 592
Derek Park
  • 45,824
  • 15
  • 58
  • 76

14 Answers14

150

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:

cxw
  • 16,685
  • 2
  • 45
  • 81
nohat
  • 7,113
  • 10
  • 40
  • 43
  • 1
    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... – Day Oct 04 '10 at 14:07
  • 3
    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. – MkV Oct 18 '12 at 08:15
  • 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. – Znik May 14 '15 at 13:49
  • 1
    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. – Dave Jacoby Jul 07 '15 at 16:19
  • 1
    Note: hexadecimal strings like `0x12` are _not_ considered numbers by this test. – Adam Katz Oct 10 '16 at 19:41
  • 2
    `looks_like_number` exposes the [internal function](http://perldoc.perl.org/perlapi.html#looks_like_number) 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](http://perldoc.perl.org/functions/fcntl.html)) – haukex Feb 12 '18 at 22:55
28

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.)

ysth
  • 96,171
  • 6
  • 121
  • 214
  • 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 & "")'); }` – yogibimbi Apr 24 '13 at 15:29
  • @yogibimbi: you are reusing the same $obj variable each time; try `my $obj = shift;`. Why the eval? – ysth Apr 24 '13 at 16:54
  • 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. – yogibimbi Apr 25 '13 at 18:47
  • 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. – yogibimbi Apr 25 '13 at 18:59
  • @yogibimbi seems most likely to me you weren't running the code you thought you were – ysth Sep 04 '19 at 17:17
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}; }
brian d foy
  • 129,424
  • 31
  • 207
  • 592
naumcho
  • 18,671
  • 14
  • 48
  • 59
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.

andrewrk
  • 30,272
  • 27
  • 92
  • 113
  • 2
    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! – osirisgothra Nov 02 '14 at 08:30
  • '.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+)?$/ – Aconcagua Mar 27 '15 at 09:02
  • 1
    The `\d*\.?\d+` part introduces a [ReDoS](https://en.wikipedia.org/wiki/ReDoS) risk. I recommend `/^[+-]?(?!\.(?!\d)|$)\d*(?:\.\d*)?$/` or `/^[+-]?(?!\.(?!\d)|$)\d*(?:\.\d*)?(?:(?<=[\d.])e[+-]?\d+)?$/i` to include scientific notation ([explanation and examples](https://regex101.com/r/fX6cW5/3)) 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. – Adam Katz Sep 08 '16 at 06:43
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.

Smamatti
  • 3,901
  • 3
  • 32
  • 43
Peter Vanroose
  • 109
  • 1
  • 2
  • 1
    That will throw warnings if you use "-w" or "use warnings;". – Derek Park Oct 26 '12 at 15:02
  • 1
    The warnings can be removed `$x eq (($x+0)."")` however a worse problem is that under this function, "1.0" is not numeric – Eponymous Jan 07 '15 at 16:10
  • 1
    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. – Znik May 14 '15 at 13:52
  • $x eq $x+0 fails for "0.0", ".5", "0x1"… – Skeeve Feb 03 '23 at 09:55
4

Not perfect, but you can use a regex:

sub isnumber 
{
    shift =~ /^-?\d+\.?\d*$/;
}
farmerchris
  • 777
  • 1
  • 6
  • 6
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.

brian d foy
  • 129,424
  • 31
  • 207
  • 592
Jon 'links in bio' Ericson
  • 20,880
  • 12
  • 98
  • 148
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;
}
fringd
  • 2,380
  • 1
  • 18
  • 13
1

Try this:

If (($x !~ /\D/) && ($x ne "")) { ... }
Vijaya Pandey
  • 4,252
  • 5
  • 32
  • 57
CDC
  • 19
  • 1
1

I found this interesting though

if ( $value + 0 eq $value) {
    # A number
    push @args, $value;
} else {
    # A string
    push @args, "'$value'";
}
Swadhikar
  • 2,152
  • 1
  • 19
  • 32
  • 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 asked – Kumar Saurabh Oct 01 '15 at 14:25
  • 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. – Swadhikar Oct 01 '15 at 15:02
  • Doesn't work for ex. if $value is '10.0'. – soger May 22 '23 at 14:25
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"; }
}
zagrimsan
  • 195
  • 2
  • 14
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

bLIGU
  • 45
  • 6
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]+$/;
}
Kevin
  • 175
  • 8
-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.