Avoid "printf" from printing zeros on empty values - perl

I wonder how to avoid getting zeros from printf when printing an undefined value or empty string with Perl:
$ perl -le 'printf "%.4f", undef'
0.0000
This little C program tells me that that's the way printf works.
#include <stdio.h>
main()
{
printf ("%.4f\n", "");
}
Is there anyway within printf to avoid printing zeros?

You need to convert the value to a string according to its contents
This code shows the idea, but it's probably over-elaborate, and can be reduced depending on the expected contents of your variable
use strict;
use warnings 'all';
for my $val ( undef, "", 'XX', 0, 0.3 ) {
my $sval = defined $val && length $val ? sprintf '%.4f', $val : '';
printf "Value is %s\n", $sval;
}
output
Value is
Value is
Argument "XX" isn't numeric in sprintf at E:\Perl\source\012.pl line 7.
Value is 0.0000
Value is 0.0000
Value is 0.3000
Another way would be to use looks_like_number from the core Scalar::Util module
This code also handles undefined values by converting them to the string undef. Again, the best way to code this will depend on your requirements
use strict;
use warnings 'all';
use Scalar::Util 'looks_like_number';
for my $val ( undef, "", 'XX', 0, 0.3 ) {
my $sval = $val // 'undef';
$sval = sprintf '%.4f', $sval if looks_like_number($sval);
printf "Value is %s\n", $sval;
}
output
Value is undef
Value is
Value is XX
Value is 0.0000
Value is 0.3000

The C program tells you nothing. It has undefined behavior, because "" is not of the correct type for the format string.
You're getting floating-point output because that's what you asked for by using "%.4f".
In Perl, if you print something using printf "%.4f", ..., it will treat the argument as a real number and format it accordingly. The special value undef is apparently treated as 0.0 in this context. If you want to print the empty string (or, equivalently, print nothing) for an undef argument, then you need to use a different format string -- or just not call printf at all.
if (defined $foo) {
printf "%.4f", $foo;
}
Note that this only checks whether $foo is defined, not whether it's numeric. If the value of $foo is a reference, for example, you'll get a meaningless numeric representation of some memory address.

Related

Perl: undef element in array

I defined some elements in an array:
my #arrTest;
$arrTest[0]=1;
$arrTest[2]=2;
#Result for #arrTest is: (1, undef, 2)
if($arrTest[1]==0)
{
print "one\n";
}
elsif($arrTest[1] == undef)
{
print "undef\n";
}
I think it should print "undef". But it prints "one" here...
Does it mean $arrTest[1]=undef=0?
How can I modify the "if condition" to distinguish the "undef" in array element?
The operator == in the code $arrTest[1] == 0 puts $arrTest[1] in numeric context and so its value gets converted to a number if needed, as best as the interpreter can do, and that is used in the comparison. And when a variable in a numeric test hasn't been defined a 0 is used so the test evaluates to true (the variable stays undef).
Most of the time when this need be done we get to hear about it (there are some exceptions) -- if we have use warnings; that is (best at the beginning of the program)† Please always have warnings on, and use strict. They directly help.
To test for defined-ness there is defined
if (not defined $arrTest[1]) { ... }
† A demo
perl -wE'say "zero" if $v == 0; say $v; ++$v; say $v'
The -w enables warnings. This command-line program prints
Use of uninitialized value $v in numeric eq (==) at -e line 1.
zero
Use of uninitialized value $v in say at -e line 1.
1
Note how ++ doesn't warn, one of the mentioned exceptions.

Substituting text string in uninitialized table fields having numeric sprintf format

I need to format a table row having fields of various numeric data. Some fields have integers and others have real numbers that have to be formatted with a fixed precision. Right justified numbers might be left padded with spaces, others with zeros.
Obviously sprintf is the perfect tool for the job.
However, occasionally a table field is undefined, being NULL, n/a and likewise. In such cases I usually have to print a text string like 'NULL', 'undef' or other. (Even leaving it uninitialized makes Perl issue a warning.)
Just plugging in the text string won't do. Beside issuing a warning, a sprintf of say '% 11.2' not only issues a warning "Argument "NULL" isn't numeric in sprintf at..." but also prints zero rather than the text string.
Any idea as to how to do what I need?
You could try using the "%s" format instead and a helper sub routine: For example:
use strict;
use warnings;
my $num = "N/A";
printf "%s\n", format_float( $num );
sub format_float {
return ($_[0] eq "N/A") ? $_[0] : sprintf "%f", $_[0];
}
I suggest that you use printf with a major format that uses just string fields %s together with a set of conditional expressions that return either a number formatted with sprintf if the value is defined, or the string NULL if it is not
An example should make that clearer
my ($f1, $f2, $f3) = ( 700/3, undef, 1/7 );
printf "%11s %11s %11s\n",
defined $f1 ? sprintf('%.2f', $f1) : 'NULL',
defined $f2 ? sprintf('%.2f', $f2) : 'NULL',
defined $f3 ? sprintf('%.2f', $f3) : 'NULL';
output
233.33 NULL 0.14
If you can be 99% certain that all non-number "values" you need to deal with will not have a digit anywhere, this is probably what you want:
sprintf (($num =~ /\d/) ? "%11.2f" : "%s", $num)
If you need to additionally distinguish integral from float numbers, you'll have to do more...

Warnings on equality operators

Has something changed in Perl or has it always been this way, that examples like the second ($number eq 'a') don't throw a warning?
#!/usr/bin/env perl
use warnings;
use 5.12.0;
my $string = 'l';
if ($string == 0) {};
my $number = 1;
if ($number eq 'a') {};
# Argument "l" isn't numeric in numeric eq (==) at ./perl.pl line 6.
Perl will be try to convert a scalar to the type required by the context where it is used.
There is a valid conversion from any scalar type to a string, so this is always done silently.
Conversion to a number is also done silently if the string passes a looks_like_number test (accessible through Scalar::Util). Otherwise a warning is raised and a 'best guess' approximation is done anyway.
my $string = '9';
if ( $string == 9 ) { print "YES" };
Converts the string silently to integer 9, the test succeeds and YES is printed.
my $string = '9,8';
if ( $string == 9 ) { print "YES" };
Raises the warning Argument "9,8" isn't numeric in numeric eq (==), converts the string to integer 9, the test succeeds and YES is printed.
To my knowledge it has always been this way, at least since v5.0.
It has been that way.
In the first if, l is considered to be in numeric context. However, l cannot be converted to a number. Therefore, a warning is emitted.
In the second if, the number 1 is considered to be in string context. Therefore the number 1 is converted to the string '1' before comparison and hence no warnings are emitted.
Did you use a lowercase "L" on purpose? It's often hard to tell the difference between a lowercase "L" and one. You would have answered your own question if you had used a one instead.
>perl -wE"say '1' == 0;"
>perl -wE"say 1 eq 'a';"
>
As you can see,
If one needs a number, Perl will convert a string to a number without warning.
If one needs a string, Perl will convert a number to a string without warning.
Very consistent.
You get a warning when you try to convert a lowercase L to a number, but how is that surprising?

Where is the error "Use of uninitialized value in string ne" coming from?

Where is the uninitialised value in the below code?
#!/usr/bin/perl
use warnings;
my #sites = (undef, "a", "b");
my $sitecount = 1;
my $url;
while (($url = $sites[$sitecount]) ne undef) {
$sitecount++;
}
Output:
Use of uninitialized value in string ne at t.pl line 6.
Use of uninitialized value in string ne at t.pl line 6.
Use of uninitialized value in string ne at t.pl line 6.
Use of uninitialized value in string ne at t.pl line 6.
You can't use undef in a string comparison without a warning.
if ("a" ne undef) { ... }
will raise a warning. If you want to test if a variable is defined or not, use:
if (defined $var) { ... }
Comments about the original question:
That's a strange way to iterate over an array. The more usual way of doing this would be:
foreach my $url (#sites) { ... }
and drop the $sitecount variable completely, and don't overwrite $url in the loop body. Also drop the undef value in that array. If you don't want to remove that undef for some reason (or expect undefined values to be inserted in there), you could do:
foreach my $url (#sites) {
next unless defined $url;
...
}
If you do want to test for undefined with your form of loop construct, you'd need:
while (defined $sites[$sitecount]) {
my $url = $sites[$sitecount];
...
$sitecount++;
}
to avoid the warnings, but beware of autovivification, and that loop would stop short if you have undefs mixed in between other live values.
The correct answers have already been given (defined is how you check a value for definedness), but I wanted to add something.
In perlop you will read this description of ne:
Binary "ne" returns true if the left argument is stringwise not equal
to the right argument.
Note the use of "stringwise". It basically means that just like with other operators, such as ==, where the argument type is pre-defined, any arguments to ne will effectively be converted to strings before the operation is performed. This is to accommodate operations such as:
if ($foo == "1002") # string "1002" is converted to a number
if ($foo eq 1002) # number 1002 is converted to a string
Perl has no fixed data types, and relies on conversion of data. In this case, undef (which coincidentally is not a value, it is a function: undef(), which returns the undefined value), is converted to a string. This conversion will cause false positives, that may be hard to detect if warnings is not in effect.
Consider:
perl -e 'print "" eq undef() ? "yes" : "no"'
This will print "yes", even though clearly the empty string "" is not equal to not defined. By using warnings, we can catch this error.
What you want is probably something like:
for my $url (#sites) {
last unless defined $url;
...
}
Or, if you want to skip to a certain array element:
my $start = 1;
for my $index ($start .. $#sites) {
last unless defined $sites[$index];
...
}
Same basic principle, but using an array slice, and avoiding indexes:
my $start = 1;
for my $url (#sites[$start .. $#sites]) {
last unless defined $url;
...
}
Note that the use of last instead of next is the logical equivalent of your while loop condition: When an undefined value is encountered, the loop is exited.
More debugging: http://codepad.org/Nb5IwX0Q
If you, like in this paste above, print out the iteration counter and the value, you will quite clearly see when the different warnings appear. You get one warning for the first comparison "a" ne undef, one for the second, and two for the last. The last warnings come when $sitecount exceeds the max index of #sites, and you are comparing two undefined values with ne.
Perhaps the message would be better to understand if it was:
You are trying to compare an uninitialized value with a string.
The uninitialized value is, of course, undef.
To explicitely check if $something is defined, you need to write
defined $something
ne is for string comparison, and undef is not a string:
#!/usr/bin/perl
use warnings;
('l' ne undef) ? 0 : 0;
Use of uninitialized value in string ne at t.pl line 3.
It does work, but you get a [slightly confusing] warning (at least with use warnings) because undef is not an "initialized value" for ne to use.
Instead, use the operator defined to find whether a value is defined:
#!/usr/bin/perl
use warnings;
my #sites = (undef, "a", "b");
my $sitecount = 1;
my $url;
while (defined $sites[$sitecount]) { # <----------
$url = $sites[$sitecount];
# ...
$sitecount++;
}
... or loop over the #sites array more conventionally, as Mat explores in his answer.

Unexpected result for IF statement: "string" <= 72 is true?

In my code below, when I enter in some non-numeric letters at the input (ie. $temp), it responds with "Too cold!" instead of "invalid". What am I missing?
#!/usr/bin/perl
print "What is the temperature outside? ";
$temp=<>;
if ($temp > 72) {
print "Too hot!\n"; }
elsif ($temp <= 72) {
print "Too cold!\n"; }
else {
print "Temperature $temp is invalid.\n"; }
This is because it will be treated as 0 if it cannot be converted into a number. You should check before if the response has only numbers, or restrict the input in any other way so that only a valid number can be entered. Something along the lines:
print "invalid" if ($temp =~ /\D/);
(prints invalid if $temp contains any non-digit character. Note that this may invalidate "+" and "-", but you get the idea).
The numerical comparison operators expect their arguments to be numbers. If you try to compare a string like 'foo' using a numerical comparison, it will be converted silently to the number 0, which is less than 72.
If you had warnings turned on, you would have been told what was going on.
friedo$ perl -Mwarnings -E 'say "foo" < 72'
Argument "foo" isn't numeric in numeric lt (<) at -e line 1.
1
This is why you should always begin your programs with
use strict;
use warnings;
Casting an invalid numerical string to a number results in 0, therefor you could use something as the below to see if the input was indeed valid or not.
print "What is the temperature outside? ";
$temp=<>;
if ($temp == 0 && $temp ne '0') {
print "Temperature $temp is invalid.\n"; }
elsif ($temp > 72) {
print "Too hot!\n"; }
elsif ($temp <= 72) {
print "Too cold!\n"; }
Explanation: If the input string was casted into 0 (zero) though the string itself isn't equal to '0' (zero) the input is not numeric, hence; invalid.
You could also check to see if the input only consists of [0-9.] by using a regular expression, that would ensure that it's a valid number (also remember that numbers do not start with 0 (zero) and then have digits that follow, unless you are writing in octal.
Note: Remember to trim the input string from white spaces before the above check.
For precisely this reason (and many others), you're MUCH better off if you enable "use warnings":
#!/usr/bin/perl
use strict;
use warnings;
...
Try it after removing the trailing newline, which is probably what's causing Perl to treat it as a string rather than a number:
chomp( my $test = <> );