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...
Related
I'm trying to truncate a string in a select input option using perl if it is longer than a set value, though i can't get it to work correctly.
my $value = defined $option->{value} ? $option->{value} : '';
my $maxValueLength = 50;
if ($value.length > $maxValueLength) {
$value = substr $value, 0, $maxValueLength + '...';
}
Another option is regex
$string =~ s/.{$maxLength}\K.*/.../;
It matches any character (.) given number of times ({N}, here $maxLength), what is the first $maxLength characters in $string; then \K makes it "forget" all previous matches so those won't get replaced later. The rest of the string that is matched is then replaced by ...
See Lookaround assertions in perlre for \K.
This does start the regex engine for a simple task but it doesn't need any conditionals -- if the string is shorter than the maximum length the regex won't match and nothing happens.
Your code has several syntax errors. Turn on use strict and use warnings if you don't have it, and then read the error messages it tells you about. This is a bit tricky because of Perl's very complex syntax (see also Damian Conway's keynote from the 2020 Perl and Raku Conference), but it boils down to these:
Use of uninitialized value in concatenation (.) or string at line 7
Argument "..." isn't numeric in addition (+) at line 8
I've used the following adaption of your code to produce these
use strict;
use warnings;
my $value = '1234567890' x 10;
my $maxValueLength = 50;
if ( $value.length > $maxValueLength ) {
$value = substr $value, 0, $maxValueLength + '...';
}
print $value;
Now let's see what they mean.
The . operator in Perl is a concatenation. You cannot use it to call methods, and length is not a method on a string. Perl thinks you are using the built-in length (a function, not a method) without an argument, which makes it default to $_. Most built-ins do this, to make one-liners shorter. But $_ is not defined. Now the . tries to concatenate the length of undef to $value. And using undef in a string operation leads to this warning.
The correct way of doing this is length $value (or with parentheses if you prefer them, length($value)).
The + operator is not concatenation (we just learned that the . is). It's a numerical addition. Perl is pretty good at converting between strings and numbers as there aren't really any types, so saying 1 + "5" would give you 6 without problems, but it cannot do that for a couple of dots in a string. Hence it complains about a non-number value in an addition.
You want the substring with a given length, and then you want to attach the three dots. Because of associativity (or stickyness) of operators you will need to use parentheses () for your substr call.
$value = substr($value, 0, $maxValueLength) . '...';
To find a length of the string use length(STRING)
Here is the code snippet how you can modify the script.
#!/usr/bin/perl
use strict;
use warnings;
use feature qw(say);
my $string = "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz";
say "length of original string is:".length($string);
my $value = defined $string ? $string : '';
my $maxValueLength = 50;
if (length($value) > $maxValueLength) {
$value = substr $value, 0, $maxValueLength;
say "value:$value";
say "value's length:".length($value);
}
Output:
length of original string is:80
value:abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvw
value's length:50
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.
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?
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 = <> );
How can I include a variable in a printf expression?
Here's my example:
printf "%${cols}s", $_;
Where $cols is the number of columns
and $_ is a string.
The statement results in an "Invalid conversion" warning.
The problem ended up being that I forgot to chomp the variable. Gah. Thanks everyone.
Your interpolated variable $cols looks like its supposed to be a number, say 10, so
"%${cols}s"
should interpolate and be equivalent to
"%10s"
which is a valid format string.
If however $cols was something other than a number or valid format string, you'd get the warning.
For example, if:
$cols = "w";
that would result in "%ws" as a format string - giving the error you quote:
Invalid conversion in printf: "%w"
Valid format information can be found here.
I figured out your specific problem. Your code is correct. However, I suppose $cols might be a number read from user input, say like this:
my $cols = <STDIN>;
This works, and in numeric context $cols will appear to be a number, but the problem is that $cols isn't appearing in numeric context here. It's in string context, which means that instead of expanding to "%5s", your format string expands to "%5\ns". The newline there is mucking up the format string.
Change the code where you read $cols to this:
chomp(my $cols = <STDIN>);
See the documentation on chomp, as you may want to use it for other input reading as well.
Always use * in your format specifier to unambiguously indicate variable width! This is similar to the advice to use printf "%s", $str rather than printf $str.
From the perlfunc documentation on sprintf:
(minimum) width
Arguments are usually formatted to be only as wide as required to display the given value. You can override the width by putting a number here, or get the width from the next argument (with *) or from a specified argument (with e.g. *2$):
printf '<%s>', "a"; # prints "<a>"
printf '<%6s>', "a"; # prints "< a>"
printf '<%*s>', 6, "a"; # prints "< a>"
printf '<%*2$s>', "a", 6; # prints "< a>"
printf '<%2s>', "long"; # prints "<long>" (does not truncate)
If a field width obtained through * is negative, it has the same effect as the - flag: left-justification.
For example:
#! /usr/bin/perl
use warnings;
use strict;
my $cols = 10;
$_ = "foo!";
printf "%*s\n", $cols, $_;
print "0123456789\n";
Output:
foo!
0123456789
With the warnings pragma enabled, you'll see warnings for non-numeric width arguments.
Your current method should work
perl -e'my $cols=500; $_="foo"; printf "%${cols}s\n\n", $_;'
The following seems to work for me:
#!/bin/perl5.8 -w
use strict;
my $cols = 5;
my $a = "3";
printf "%${cols}d\n", $a;
yields
28$ ./test.pl
3
29$
I cannot reproduce your problem. The following code works fine:
use strict;
use warnings;
my $cols=40;
while (<>) {
printf "%${cols}s\n", $_;
}
It prints any input line using at least 40 columns of width.