24 days of Perl code from RJBS! Feed

%{adj}s %s{holiday}s!

String::Formatter - 2009-12-08

sprintf is My Favorite Chainsaw

There are a lot of horribly overcomplicated builtins, not just in Perl, but in many languages. Some people like Lisp's format best -- and why not? Like...


1: 
2: 
3: 
4: 
5: 
6: 
7: 

 

(format nil "~4,'0d-~2,'0d-~2,'0d" 2005 6 10) ; ==> 2005-06-10

(format nil "~:(~a~)" "tHe Quick BROWN foX") ; ==> "The Quick Brown Fox"

(format nil "~:r" 1234) ; ==> "one thousand two hundred thirty-fourth"

(format nil "~:@r" 1234) ; ==> "MCCXXXIIII"

 

Right? That's pretty awesome. Then there's Perl's pack. It's sort of like a sprintf designed by an assembly programmer who hates you:


1: 
 

pack 'n/a* w/a', 'hello,', 'world'; # gives "\000\006hello,\005world"
 

There are actually people who like that, and I have seen, with my own eyes, a guy who defended format:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 

 

format STDOUT_TOP =
                        Passwd File
Name Login Office Uid Gid Home
------------------------------------------------------------------
.

format STDOUT =
@<<<<<<<<<<<<<<<<<< @||||||| @<<<<<<@>>>> @>>>> @<<<<<<<<<<<<<<<<<
$name, $login, $office,$uid,$gid, $home
.

 

Well, sprintf isn't as awful as any of these, and that's why I love it... at least if you ignore things like:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 

 

# Vectors!
printf "%vd", "AB\x{100}"; # prints "65.66.256"
printf "version is v%vd\n", $^V; # Perl's version

# Put an asterisk "*" before the "v" to override the string to use to
# separate the numbers:
printf "address is %*vX\n", ":", $addr; # IPv6 address

printf "%2\$*3\$d %d\n", 12, 34, 3; # will print " 34 12\n"

 

Enough! Settle down!

Sorry. I got distracted.

sprintf is the sweet middle ground between a double-quoted string and Template Toolkit. If you stick to the basics that most people use, it's a nice simple way to put placeholders in a string, then stick values into it, doing just a little formatting as needed. Here's how people actually use sprintf:


1: 
2: 

 

printf "Product %s (%s) added for \$%0.2u.\n",
  $product->id, $product->name, $product->price;

 

Nice and simple, right? What I really wanted was a way to use these kinds of templates as templates, outside of code. So, for example, I could store the above string in a configuration file without worrying about the order of arguments:


1: 
2: 

 

[Purchasing]
format = Product %{id}s (%{name}s) added for $%0.2{price}f

 

Then my program could call:


1: 
 

printf $config->{purchasing}{format}, $product;
 

String::Formatter is all about building routines that make this possible. It's very flexible, and can probably be tortured into doing all kinds of horrible things (although I hope nobody ever reimplements all of Perl's sprintf). Here are just a few simple examples of how you can put String::Formatter to use.

object-specific conversions

You can set up format codes that correspond to methods, making a formatting routine that helps convert given objects to strings.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 

 

use String::Formatter method_stringf => {
  -as => 'datetimef',

  codes => {
    f => 'strftime',
    c => 'format_cldr',
    s => sub { "$_[0]" },
  },
};

print datetimef("%{%Y-%m-%d}f is the same as %{yyyy-MM-dd}c.", DateTime->now);

 

generic method-calling conversions:

You could write a formatter that just calls any method you want:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 

 

use String::Formatter stringf => {
  -as => 'objectf',

  string_replacer => 'method_call_replace', # imaginary but easy
  codes => { s => sub { "$_" } }
};

print objectf("Product %{id}s (%{name}s) added for $%0.2{price}s", $product);

 

custom conversions

You could write something meant to be used just like sprintf, but with different conversion routines:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 

 

use String::Formatter stringf => {
  codes => {
    s => sub { $_ }, # string itself
    l => sub { length }, # length of input string
    e => sub { /[^\x00-\x7F]/ ? '8bit' : '7bit' }, # ascii-safeness
  },
};

print stringf(
  "My name is %s. I am about %l feet tall. I use an %e alphabet.\n",
  'Ricardo',
  'ffffff',
  'abcchdefghijklmnñopqrrrstuvwxyz',
);

# Output:
# My name is Ricardo. I am about 6 feet tall. I use an 8bit alphabet.

 

...and you can make it do lots more.

String::Formatter does its job in a few phases, and you can replace each or all of those phases per formatter, so it's easy to write very powerful custom formatting routines. Any time you need to a teeny tiny templating language, consider whether a slightly-improved sprintf would be enough. You might be surprised how often it will be just right.

See Also