September 14, 2016

Today I saw a piece of Perl code to format a date and time that uses what I consider a clusmy way to do zero padding. I have seen this method or a variation thereof before; it checks if a number is smaller than 10 and if this is the case it converts the number to a string, adding a zero in front of it. Often when there are several variables this code is just repeated, a violation of Don't Repeat Yourself (DRY).

Below follows an example to show this technique. Please don't use it in your own code.


use strict;
use warnings;

my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst )
    = localtime( time );

$year += 1900;  # $year contains years since 1900; make it actual years
$mon++;         # $mon is zero-based; make it one-based

# Zero padding; the clumsy way

$mday = '0' . $mday if $mday < 10;
$mon  = '0' . $mon  if $mon  < 10;

# Same method, but now using string interpolation
$hour = "0$hour" if $hour < 10;
$min  = "0$min"  if $min  < 10;

my $stamp = "$year-$mon-$mday-$hour$min";
print "$stamp\n";

Perl, being very often used as a data wrangling language, comes with two functions for formatting strings which emulate (and extend) two functions from the C programming language with the same names: printf and sprintf. The former writes its output to STDOUT and the latter returns a formatted string.

To match the above code we're going to use sprintf and assign the result to $stamp. Just assume the print statement was added to the above code for debugging purposes; in actual code the value of $stamp could be used to, for example, create a directory or as part of a filename.

The first argument to sprintf is a format which controls how the following arguments are converted. To convert a decimal number the format specification %d is used. The (minimum) formatting width can be specified by using a non-zero number inserted between the % and d. Default spaces are used for padding, for example:

$ perl -e 'printf "%5d\n", 13'

This displays three spaces followed by the numer 13 for a total of 5 characters.

Note that the width is a mimimum. If the value to be converted requires more space the result is not truncated:

perl -e 'printf "%5d\n", 12335919'

When a zero follows the % character zeroes are used for padding instead of spaces, for example:

$ perl -e 'printf "%08d\n", 237'

However, if you want to print floating point numbers use %d instead of %f, for example:

$ perl -e 'printf "%012f\n", 3.1415927'

Probably more useful than padding with zeroes to the start is the control over the number of digits after the decimal point and rounding, for example:

$ perl -e 'printf "%8.4f\n", 3.1415927'

The number 8 in the above example is again the (minimum) width of the converted value, not the number of digits before the decimal point. Hence why just two spaces are used for padding.

Note that there are various methods for rounding, so if you're going to use printf or sprintf make sure the method it uses is what is required.

Other format specifications that I consider useful are %x and %X to print hexadecimal numbers, and %b to print binary. Each can also be padded with zeroes:

$ perl -e 'printf "%08x\n", 123689'
$ perl -e 'printf "%#08x\n", 123689'
$ perl -e 'printf "%08X\n", 123689'
perl -e 'printf "%08b\n", 19'

See perldoc -f sprintf for a complete list of possible conversions.

Rewriting the Perl program given earlier using sprintf results in:


use strict;
use warnings;

my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst )
    = localtime( time );

my $stamp = sprintf '%04d-%02d-%02d-%02d%02d',
    $year + 1900, $mon + 1, $mday, $hour, $min;
print "$stamp\n";

But if all we want is formatting dates we can do even better than this, using the Time::Piece module, for example, which comes shipped with perl since version 5.9.5:

$ perl -MModule::CoreList \
    -e 'print Module::CoreList->first_release( "Time::Piece" ), "\n"'

Using this module the Perl program given earlier becomes:


use strict;
use warnings;

use Time::Piece;

my $tp = Time::Piece->new();
my $stamp = $tp->strftime( '%Y-%m-%d-%H%M' );
print "$stamp\n";

See the FreeBSD man page of strftime for a complete list of conversion specifications.

I recommend to read David Farrell's Solve almost any datetime need with Time::Piece to learn more about the Time::Piece module. And, of course, read the official documentation on the command line using perldoc Time::Piece.

Blog - Email - Twitter

I am a freelance Perl programmer for hire, download my up-to-date resume (PDF).