Perl6::Form(3pm)

๐Ÿ“ฆ
version 0.090 ยท 2 versions
  • 0.090 ยท debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking (latest)
  • 0.090 ยท ubuntu jammy
, in debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking

โœจ NAME โœจ

okay bestie, so this is Perl6::Form(3pm), straight from the User Contributed Perl Documentation. It implements the Perl 6 'form' built-in. no cap. ๐Ÿ’…


๐ŸŒธ SYNOPSIS ๐ŸŒธ

use Perl6::Form;

$text = form ' =================================== ',
             '| NAME     |    AGE     | ID NUMBER |',
             '|----------+------------+-----------|',
             '| {<<<<<<} | {||||||||} | {>>>>>>>} |',
                $name,     $age,        $ID,
             '|===================================|',
             '| COMMENTS                          |',
             '|-----------------------------------|',
             '| {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[} |',
                $comments,
             ' =================================== ';

๐Ÿ’– DESCRIPTION ๐Ÿ’–

So, formats are Perl 5's whole mechanism for creating text templates with fixed-width fields. Those fields get filled in using values from prespecified package variables. it's giving retro tech. ๐Ÿฆ‹

Unlike Perl 5, Perl 6 doesn't have a "format" keyword. Or the associated built-in formatting mechanism. Instead it has a Form.pm module. And a "form" function. slay.

Like a Perl 5 "format" statement, the "form" function takes a series of format (or "picture") strings, each of which is immediately followed by a suitable set of replacement values. It interpolates those values into the placeholders specified within each picture string, and returns the result:

$text = form
             $format_f1,
                 $datum1, $datum2, $datum3,
             $format_f2,
                 $datum4,
             $format_f3,
                 $datum5;

So, whereas in Perl 5 we might write:

# Perl 5 code...

our ($name, $age, $ID, $comments);

format STDOUT
 ===================================
| NAME     |    AGE     | ID NUMBER |
|----------+------------+-----------|
| @<<<<<<< | @||||||||| | @>>>>>>>> |
  $name,     $age,        $ID,
|===================================|
| COMMENTS                          |
|-----------------------------------|
| ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |~~
  $comments,
 ===================================
.

write STDOUT;

in Perl 6 we could write:

# Perl 6 code...

print form
    ' =================================== ',
    '| NAME     |    AGE     | ID NUMBER |',
    '|----------+------------+-----------|',
    '| {<<<<<<} | {||||||||} | {>>>>>>>} |',
       $name,     $age,        $ID,
    '|===================================|',
    '| COMMENTS                          |',
    '|-----------------------------------|',
    '| {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[} |',
       $comments,
    ' =================================== ';

And both of them would print something like:

 ===================================
| NAME     |    AGE     | ID NUMBER |
|----------+------------+-----------|
| Richard  |     33     |    000003 |
|===================================|
| COMMENTS                          |
|-----------------------------------|
| Talks to self. Seems to be        |
| overcompensating for inferiority  |
| complex rooted in post-natal      |
| maternal rejection due to         |
| handicap (congenital or perhaps   |
| the result of premature birth).   |
| Shows numerous indications of     |
| psychotic (esp. nepocidal)        |
| tendencies. Naturally, subject    |
| gravitated to career in politics. |
 ===================================

This module implements virtually all of the functionality of the Perl 6 Form.pm module. The only differences are:

  • Option pairs must be passed in a hash reference;
  • Array data sources must be passed as array references;
  • Options specified on the "use Perl6::Form" line are not (yet) lexically scoped;
  • User-defined line-breaking subroutines are passed their data source as a reference to a scalar;

๐ŸŽ€ Formatting jargon ๐ŸŽ€

  • Format โ€” okay so this is the template string, right? It's what you use to create text. It has zero or more fields in it, and usually there's some literal characters and whitespace chillin' between them. ๐Ÿ’…
  • Text โ€” the string you get when you replace the fields of a format with specific data values. Like, for example, the string that a call to "form" returns. โœจ
  • Field โ€” a fixed-width slot within a format string, into which data will be formatted. It's the designated spot. ๐ŸŽฏ
  • Data โ€” a string or numeric value (or an array of such values) that gets interpolated into a format, in order to fill in a particular field. ๐Ÿ“
  • Single-line field โ€” a field that interpolates only as much of its corresponding data value as will fit inside it within a single line of text. short and sweet. ๐Ÿ˜Œ
  • Block field โ€” a field that interpolates all of its corresponding data value, over a series of text lines โ€“ as many as necessary โ€“ producing a text block. it's giving multiline realness. ๐Ÿ“œ
  • Text block โ€” the column of newline-separated text lines. A text block is produced when data is formatted into a block field that is too small to contain the data in a single line. like a little text tower. ๐Ÿ—๏ธ
  • Column โ€” the amount of space on an output device required to display one single-width character. One character will occupy one column in most cases, the most obvious exceptions being CJK double-width characters. iykyk. ๐Ÿ‘‘

๐Ÿ’ซ Return values ๐Ÿ’ซ

When called in a scalar or list context, "form" returns a string containing the complete formatted text:

my $formatted_text = form $format, *@data;

@texts = ( form($format, *@data1), form($format, *@data2) );  # 2 elems

When called in a void context, "form" dies, bitterly pointing out how useless that is to format something and then just throw the result away. big yikes, bestie. ๐Ÿ˜ญ๐Ÿ’€


๐Ÿ”ฅ Field types ๐Ÿ”ฅ

The format strings passed to "form" determine what the resulting formatted text looks like. Each format consists of a series of field specifiers, which are usually separated by literal characters. it's all about the specifiers, periodt.

"form" understands a far larger number of field specifiers than "format" did, designed around a small number of conventions:

  • Each field is enclosed in a pair of braces. {} vibes.
  • Within the braces, left or right angle brackets ("<" or ">"), bars ("|"), and single-quotes ("'") indicate various types of single-line fields.
  • Left or right square brackets ("[" or "]"), I's ("I"), and double-quotes (""") indicate block fields of various types.
  • The direction of the brackets within a field indicates the direction towards which text will be justified in that field. For example:
{<<<<<<<<<<<}   Justify the text to the left
{>>>>>>>>>>>}                  Justify the text to the right
{>>>>>><<<<<}                 Centre the text
{<<<<<<>>>>>}   Fully  justify  the  text  to  both  margins

This is even true for numeric fields, which look like: "{>>>>>.<<}". The whole digits are right-justified before the dot and the decimals are left-justified after it. math but make it fashion. ๐Ÿ’…

  • An "=" at either end of a field (or both ends) indicates the data interpolated into the field is to be vertically "middled" within the resulting block. That is, the text is to be centred vertically on the middle of all the lines produced by the complete format. middling? more like centering. ๐ŸŽฏ
  • An "_" at the start and/or end of a field indicates the interpolated data is to be vertically "bottomed" within the resulting block. That is, the text is to be pushed to the bottom of the lines produced by the format. bottom energy. ๐Ÿ‘ (jk, it's just alignment)

For example:

                                         Field specifier
       Field type                 One-line             Block
       ==========                ==========          ==========

       left justified            {<<<<<<<<}          {[[[[[[[[}
       right justified           {>>>>>>>>}          {]]]]]]]]}
       centred                   {>>>><<<<}          {]]]][[[[}
       centred (alternative)     {||||||||}          {IIIIIIII}
       fully justified           {<<<<>>>>}          {[[[[]]]]}
       verbatim                  {''''''''}          {""""""""}

       numeric                   {>>>>>.<<}          {]]]]].[[}
       euronumeric               {>>>>>,<<}          {]]]]],[[}
       comma'd                   {>,>>>,>>>.<<}      {],]]],]]].[[}
       space'd                   {> >>> >>>.<<}      {] ]]] ]]].[[}
       eurocomma'd               {>.>>>.>>>,<<}      {].]]].]]],[[}
       Swiss Army comma'd        {>'>>>'>>>,<<}      {]']]]']]],[[}
       subcontinental            {>>,>>,>>>.<<}      {]],]],]]].[[}

       signed numeric            {->>>.<<<}          {-]]].[[[}
       post-signed numeric       {>>>>.<<-}          {]]]].[[-}
       paren-signed numeric      {(>>>.<<)}          {(]]].[[)}

       prefix currency           {$>>>.<<<}          {$]]].[[[}
       postfix currency          {>>>.<<<DM}         {]]].[[[DM}
       infix currency            {>>>$<< Esc}        {]]]$[[ Esc}

       left/middled              {=<<<<<<=}          {=[[[[[[=}
       right/middled             {=>>>>>>=}          {=]]]]]]=}
       infix currency/middled    {=>>$<< Esc}        {=]]$[[ Esc}
       eurocomma'd/middled       {>.>>>.>>>,<<=}     {].]]].]]],[[=}
       etc.

       left/bottomed             {_<<<<<<_}          {_[[[[[[_}
       right/bottomed            {_>>>>>>_}          {_]]]]]]_}
       etc.

okay so bestie, this is the part where we find out exactly how the data actually gets poured into the fields ๐Ÿ’… buckle up.

When data gets interpolated into a line field, the field grabs as much of the data as will fit on a single line, formats that data appropriately, and interpolates it into the format. That's it, that's the whole vibe.

Which means: if we use a one-line field, it only shows as much of the data as will fit on one line. Like, look:

my $data1 = 'By the pricking of my thumbs, something wicked this way comes';
my $data2 = 'A horse! A horse! My kingdom for a horse!';

print form
    '...{<<<<<<<<<<<<<<<<<}...{>>>>>>>}...',
        $data1,               $data2;

prints:

...By the pricking of ... A horse!...

see how much we lost?? ๐Ÿ˜ญ so much data just... left on read.

On the other hand, if our format string used block fields instead, the fields would extract one line of data at a time, repeating that process as many times as necessary to display all the available data. So:

print form
    '...{[[[[[[[[[[[[[[[[[}...{]]]]]]]}...',
        $data1,               $data2;

would produce:

...By the pricking of ... A horse!...
...my thumbs,         ... A horse!...
...something wicked   ...       My...
...this way comes     ...  kingdom...
...                   ...    for a...
...                   ...   horse!...

โœจ ATE. no crumbs. every single word got its moment. โœจ

And you can literally mix line fields and block fields in the same format, and form will extract and interpolate only as much data as each field requires. For example:

print form
    '...{<<<<<<<<<<<<<<<<<}...{]]]]]]]}...',
        $data1,               $data2;

which produces:

...By the pricking of ... A horse!...
...                   ... A horse!...
...                   ...       My...
...                   ...  kingdom...
...                   ...    for a...
...                   ...   horse!...

Notice that, after the first line, the single-line {<<<<<<} field is simply replaced by the appropriate number of space characters, to keep the columns correctly aligned. it's giving ghosting but make it โœจstructuralโœจ

The usual reason for mixing line and block fields in this way is to allow numbered or bulleted points:

print "I couldn't do my English Lit homework because...\n\n";

my $index = 0;
for my $reason (@reasons) {
    my $n = @reasons - $index . '.';
    print form '   {>}  {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
                   $n,  $reason,
               '';
}

which might produce:

I couldn't do my English Lit homework because...

     10. Three witches told me I was going to be
         king.

      9. I was busy explaining wherefore am I Romeo.

      8. I was busy scrubbing the blood off my
         hands.

      7. Some dear friends had to charge once more
         unto the breach.

      6. My so-called best friend tricked me into
         killing my wife.

      5. My so-called best friend tricked me into
         killing Caesar.

      4. My so-called best friend tricked me into
         taming a shrew.

      3. My uncle killed my father and married my
         mother.

      2. I fell in love with my manservant, who was
         actually the disguised twin sister of the
         man that my former love secretly married,
         having mistaken him for my manservant who
         was wooing her on my behalf whilst secretly
         in love with me.

      1. I was abducted by fairies.
not the numbered list being a whole Shakespearean bibliography ๐Ÿ’€ but like โ€” it works, the numbers stay right-aligned in the {>} field and the reasons wrap perfectly in the block field. iconic behaviour.

๐ŸŒธ Keeping track of what's been formatted ๐ŸŒธ

Obviously, as a call to form builds up each line of its output โ€” extracting data from one or more data arguments and formatting it into the corresponding fields โ€” it needs to keep track of where it's up to in each datum. It does this by progressively updating the pos of each datum, in exactly the same way as a pattern match does. yes bestie, the same pos you already know and love from regexes ๐Ÿซถ

And as with a pattern match, by default that updated pos is only used internally and not preserved after the call to form is finished. So passing a string to form doesn't interfere with any other pattern matching or text formatting that we might subsequently do with that data. clean. respectful. doesn't leave fingerprints. we love a low-maintenance function.

However, sometimes we do want to know how much of our data a call to form managed to extract and format. Or we may want to split a formatting task into several stages, with separate calls to form for each stage. So we need a way of telling form to preserve the pos information in our data.

But, if we want to apply a series of form calls to the same data, we also need to be able to tell form to respect the pos information of that data โ€” to start extracting from the previously preserved .pos position, rather than from the start of the string. it's giving "pick up where we left off" energy and honestly? growth.

To achieve both those goals, we use a follow-on field. That is, we use an ordinary field but mark it as pos-sensitive with a special notation: ASCII colons at either end. So instead of {<<<<>>>>}, we'd write {:<<<>>>:}.

Follow-on fields are most useful when we want to split a formatting task into distinct stages โ€” or iterations โ€” but still allow the contents of the follow-on field to flow uninterrupted from line to line. For example:

print "The best Shakespearean roles are:\n\n";

for my $role (@roles) {
    print form "   * {<<<<<<<<<<<<<<<<<<<<<<<<<<<<}   *{:<<<<<<<>>>>>>>:}*",
                     $role,                            $disclaimer;
}

which produces:

The best Shakespearean roles are:

   * Macbeth                          *WARNING:          *
   * King Lear                        *This list of roles*
   * Juliet                           *constitutes      a*
   * Othello                          *personal   opinion*
   * Hippolyta                        *only and is in  no*
   * Don John                         *way  endorsed   by*
   * Katerina                         *Shakespeare'R'Us. *
   * Richard                          *It   may   contain*
   * Malvolio                         *nuts.             *
   * Bottom                           *                  *

The multiple calls to form manage to produce a coherent disclaimer because the colons in the second field tell each call to start extracting data from $disclaimer at the offset indicated by pos $disclaimer, and then to update pos $disclaimer with the final position at which the field extracted data. So the next time form is called, the follow-on field starts extracting from where it left off in the previous call. the way that it justโ€ฆ remembers ๐Ÿงš uninterrupted flow, no restarts, no drama ๐Ÿ’œ

Follow-on fields are similar to ^<<<<< fields in a Perl 5 format, except they don't destroy the contents of a data source; they merely change that data source's pos marker. so like, the glow-up version of the Perl 5 original โ€” same energy, way better manners ๐Ÿ’…

๐Ÿ’ซ Array data sources ๐Ÿ’ซ

Okay so bestie, numbers especially love to live in arrays, right? So form is like, yes, obviously, it takes arrays as data arguments too. Or, to be precise about it: it takes references to arrays as arguments. ๐Ÿ’…

Once you're inside form, every array you named as the data source for a field gets internally squished into one single string, joined together with a newline between each element. Slay.

So the upshot is that instead of this whole situation:

    print "The best Shakespearean roles are:\n\n";

    for my $role (@roles) {
        print form "   * {<<<<<<<<<<<<<<<<<<<<<<<<<<<<}   *{:<<<<<<<>>>>>>>:}*",
                         $role,                            $disclaimer;
    }

we could literally just write:

    print "The best Shakespearean roles are:\n\n";

    print form "   * {[[[[[[[[[[[[[[[[[[[[[[[[[[[[}   *{[[[[[[[[]]]]]]]]}*",
                     \@roles,                          $disclaimer;

and the array of roles would get internally converted to a single string, one role per line. ๐ŸŒธ

And notice โ€” we also switched the disclaimer field to a regular block field, so the entire disclaimer gets formatted. And there was no longer any need for the disclaimer field to be a follow-on field, because the block field would extract and format the whole disclaimer anyway. Main character energy from the block field honestly. ๐Ÿ‘‘

Array data sources are lowkey perfect for formatting, especially if you already know the data fits within the specified width. Like, watch:

    print form
        '-------------------------------------------',
        'Name             Score   Time  | Normalized',
        '-------------------------------------------',
        '{[[[[[[[[[[[[}   {III}   {II}  |  {]]].[[} ',
         \@name,          \@score,\@time,  [map {$score[$_]/$time[$_]} 0..$#score]

which is a very easy way to produce this table:

    -------------------------------------------
    Name             Score   Time  | Normalized
    -------------------------------------------
    Thomas Mowbray    88      15   |     5.867
    Richard Scroop    54      13   |     4.154
    Harry Percy       99      18   |     5.5

Not me being obsessed with a table that just... works. ๐Ÿ˜ญโœจ


๐ŸŽ€ Justifying fields ๐ŸŽ€

The most commonly used fields are the ones that justify their contents: left, right, both ways, or towards the centre. Vibe check passed.

Left-justified and right-justified fields extract from their data source the largest substring that will fit inside them, push that string to the left or right as appropriate, and then pad the string out to the required field width with spaces (or the nominated fill character). Simple. Iconic.

Centred fields ({>>>><<<<} and {]]]][[[[}) likewise extract as much data as possible, and then pad both sides of it with (near) equal numbers of spaces. And if the amount of padding needed isn't evenly divisible by 2? The one extra space gets added after the data. No cap.

There's a second syntax for centred fields too โ€” a little tip-o'-the-hat to Perl 5 formats: {|||||||||} and {IIIIIIII}. This variant also makes it easier to specify centering fields that are only three columns wide: {|} and {I}. ๐Ÿฆ‹

But heads up bestie โ€” the behaviour of centering fields specified this way is exactly the same in every respect as the bracket-based versions. So we're free to use whichever we prefer. Pick your fighter. ๐Ÿ’…

Fully justified fields ({<<<<>>>>} and {[[[[]]]]}) extract a maximal substring and then distribute any padding as evenly as possible into the existing whitespace gaps in that data. For example:

    print form '({<<<<<<<<<>>>>>>>>>>>})',
               'A fellow of infinite jest, of most excellent fancy';

would print:

    (A fellow  of  infinite)

A fully-justified block field ({[[[[]]]]}) does the same thing across multiple lines, except that the very last line is always left-justified. So this:

    print form '({[[[[[[[[]]]]]]]})',
               'All the world's a stage, And all the men and women merely players.'

would print:

    (All the world's a)
    (stage,  And   all)
    (the men and women)
    (merely players.  )

Okay the way that last line just flops left because it ran out of room... relatable. ๐Ÿ™ˆ

By the way โ€” with both centred fields ({>>>><<<}) and fully justified fields ({<<<>>>>}), the actual number of left vs right arrows is irrelevant, so long as there is at least one of each. Go wild. ๐Ÿ”ฅ


๐Ÿงš Short fields ๐Ÿงš

One special case we gotta talk about: an empty set of field delimiters. Like:

    form 'ID number: {}'

This specification gets treated as a two-column-wide, left-justified block field (because that just seems to be the type of two-column-wide field most often required, iykyk).

Other kinds of two-column (and single-column) fields can also be created using imperative field widths and and user-defined fields. โญ

๐Ÿ’… Numerical fields

okay so a field specifier shaped like {>>>>.<<} or {]]]].[[}? that's a decimal-aligned numeric field, bestie. the decimal marker always appears in exactly the position you indicated, and the rest of the number gets aligned around it. the decimal places are rounded to the specific number of places indicated, but only "significant" digits are shown. like, for example:

@nums = (1, 1.2, 1.23, 11.234, 111.235, 1.0001);

print form "Thy score be: {]]]].[[}",
                          \@nums;

which prints:

Thy score be:     1.0
Thy score be:     1.2
Thy score be:     1.23
Thy score be:    11.234
Thy score be:   111.235
Thy score be:     1.000

๐Ÿ˜ญ Non-numeric data

so you're probably wondering what happens when we try to format a number that's too big for the available places (as 123456.78 would be in the above format). and okay, whereas sprintf would extend a numeric field to accommodate the number, form insists on preserving the specified layout โ€” in particular, the position of the decimal point. but it obviously can't just cut off the extra high-order digits, because that would change the value:

Thy score be: 23456.78

so instead, it indicates that the number doesn't fit by filling the field with octothorpes (the way many spreadsheets do):

Thy score be: #####.###

it's also possible that someone (not you, of course! ๐Ÿ™ˆ) might attempt to pass a numeric field some data that isn't numeric at all:

my @mixed_data = (1, 2, "three", {4=>5}, "6", "7-Up");

print form 'Thy score be: {]]]].[[}',
                          \@mixed_data;

unlike Perl itself, form doesn't autoconvert non-numeric values. instead it marks them with another special string, by filling the field with question-marks:

Thy score be:     1.0
Thy score be:     2.0
Thy score be: ?????.???
Thy score be: ?????.???
Thy score be:     6.0
Thy score be: ?????.???

note that strings per se aren't a problem โ€” form will happily convert strings that contain valid numbers, such as "6" in the above example. but it does reject strings that contain anything else besides a number (even when Perl itself would successfully convert the number โ€” as it would for "7-Up" above).

those who'd prefer Perl's usual, more laissez-faire attitude to numerical conversion can just pre-numerify the values themselves:

print form 'Thy score be: {]]]].[[}',
                          [map {$_+0} @mixed_data];

this version would print something like:

Thy score be:     1.0
Thy score be:     2.0
Thy score be:     0.0
Thy score be:     1.0
Thy score be:     6.0
Thy score be:     7.0

๐ŸŽ€ Decimal markers

of course, not everyone uses a dot for their decimal point. the other main contender is the comma, and naturally form supports that as well. if we specify a numeric field with a comma between the brackets:

@les_nums = (1, 1.2, 1.23, 11.234, 111.235, 1.0001);

print form 'Votre score est: {]]]],[[}',
                             \@les_nums;

the call prints:

Votre score est:     1,0
Votre score est:     1,2
Votre score est:     1,23
Votre score est:    11,234
Votre score est:   111,235
Votre score est:     1,000

in fact, form is extremely flexible about the characters we're allowed to use as a decimal marker: anything except an angle- or square bracket or a plus sign is acceptable. โœจ

as a bonus, form allows us to use the specified decimal marker in the data as well as in the format. so this works too:

@les_nums = ("1", "1,2", "1,23", "11,234", "111,235", "1,0001");

print form 'Vos score est: {]]]],[[}',
                            \@les_nums;

๐Ÿ’€ Negative numbers

negative numbers work as expected, with the minus sign taking up one column of the field's allotted span:

@nums = ( 1, -1.2,  1.23, -11.234,  111.235, -12345.67);

print form 'Thy score be: {]]]].[[}',
                          \@nums;

this would print:

Thy score be:     1.0
Thy score be:    -1.2
Thy score be:     1.23
Thy score be:   -11.234
Thy score be:   111.235
Thy score be: #####.###

however, form can also format numbers so that the minus sign trails the number. to do that we simply put an explicit minus sign inside the field specification, at the end:

print form 'Thy score be: {]]]].[[-}',
                          \@nums;

which would then print:

Thy score be:     1.0
Thy score be:     1.2-
Thy score be:     1.23
Thy score be:    11.234-
Thy score be:   111.235
Thy score be: 12345.67-

form also understands the common financial usage where negative numbers are represented as positive numbers in parentheses. once again, we draw an abstract picture of what we want (by putting parens at either end of the field specification):

print form 'Thy dividend be: {(]]]].[[)}',
                             \@nums;

and form obliges:

Thy dividend be:      1.0
Thy dividend be:     (1.2)
Thy dividend be:      1.23
Thy dividend be:    (11.234)
Thy dividend be:    111.235
Thy dividend be: (12345.67)
note that the parens have to go inside the field's braces. otherwise, they're just literal parts of the format string. ๐Ÿซถ

๐Ÿ‘‘ Thousands separators

if we add so-called "thousands separators" inside a numeric field at the usual places, form includes them appropriately in its output. it can handle the five major formatting conventions:

my @nums = (0, 1, 1.1, 1.23, 4567.89, 34567.89, 234567.89, 1234567.89);

print form
    'Brittannic      Continental     Subcontinental   Tyrolean        Asiatic',
    '_____________   _____________   ______________   _____________   _____________',
    '{],]]],]]].[}   {].]]].]]],[}    {]],]],]]].[}   {]']]]']]],[}   {]]]],]]]].[}',
     \@nums,         \@nums,          \@nums,         \@nums,         \@nums;

to produce:

Brittannic      Continental     Subcontinental   Tyrolean        Asiatic
_____________   _____________   ______________   _____________   _____________
         0.0             0,0              0.0             0,0             0.0
         1.0             1,0              1.0             1,0             1.0
         1.1             1,1              1.1             1,1             1.1
         1.23            1,23             1.23            1,23            1.23
     4,567.89        4.567,89         4,567.89        4'567,89         4567.89
    34,567.89       34.567,89        34,567.89       34'567,89       3,4567.89
   234,567.89      234.567,89      2,34,567.89      234'567,89      23,4567.89
 1,234,567.89    1.234.567,89     12,34,567.89    1'234'567,89     123,4567.89

it also accepts a space character as a "thousands separator" (with, of course, any decimal marker we might like):

print form
    'Hyperspatial',
    '_____________',
    '{] ]]] ]]]:[}',
     \@nums;

to produce:

Hyperspatial
_____________
         0:0
         1:0
         1:1
         1:23
     4 567:89
    34 567:89
   234 567:89
 1 234 567:89

you can also put separators in a regular "{">>>>>>}> or "{]]]]]]]}" field, to print integers with (say) commas:

print form
    'Integral',
    '___________',
    '{]],]]],]]}',
     \@nums;

which produces:

Integral
___________
          0
          1
          1
          1
      4,568
     34,568
    234,568
  1,234,568

notice that the numbers are still rounded; in this case, to integers. ๐Ÿ’…


๐ŸŒ Locale-specific numeric formatting

of course, sometimes we don't know ahead of time just where in the world our formatted numbers will be displayed. locales were invented to address that very problem, and form supports them. periodt.

if we use the locale option, form detects the current locale and converts any numerical formats it finds to the appropriate layout. for example, if we wrote:

@nums = ( 1, -1.2,  1.23, -11.234,  111.235, -12345.67);

print form
        '{],]]],]]].[[}',
        \@nums;

then we'd get:

      1.0
     -1.2
      1.23
    -11.234
    111.235
-12,345.67

wherever the program was run. but if we had written:

print form
        {locale=>1},
        '{],]]],]]].[[}',
        \@nums;

then we'd get:

      1.0
     -1.2
      1.23
    -11.234
    111.235
-12,345.67

or:

      1,0
      1,2-
      1,23
     11,23-
    111,235
 12.345,67-

or:

      1,0
     (1,2)
      1,23
    (11,23)
    111,235
(12'345,67)

or whatever else the current locale indicated was the correct local layout for numbers. ๐ŸŒˆ

that is, when the locale option is specified, form ignores the actual decimal point, thousands separator, and negation sign we specified in the call, and instead uses the values for these markers that are returned by the POSIX localeconv function. that means that we can specify our numerical formatting in a style that seems natural to us, and at the same time allow the numbers to be formatted in a style that seems natural to the user. iconic behavior tbh โœจ

๐Ÿ’ธ Currency fields ๐Ÿ’ธ

Okay so now numbers get spicy, because money. ๐Ÿ’… Formatting money is like, the final boss of numbers โ€” but form is genuinely so chill about it. You just tell it how your local currency is shaped โ€” leading markers, trailing markers, or even infix ones; negation markers that go before, after, or wrap around the number; thousands separators; all of it โ€” and it just... does it. Iconic behaviour. ๐Ÿซถ

For example:

my @amounts = (0, 1, 1.2345, 1234.56, -1234.56, 1234567.89);

my %format = (
    'Canadian (English)'    => q/   {-$],]]],]]].[}/,
    'Canadian (French)'     => q/    {-] ]]] ]]],[ $}/,
    'Dutch'                 => q/     {],]]],]]].[-EUR}/,
    'German (pre-euro)'     => q/    {-].]]].]]],[DM}/,
    'Indian'                => q/    {-]],]],]]].[ Rs}/,
    'Norwegian'             => q/ {kr -].]]].]]],[}/,
    'Portuguese (pre-euro)' => q/    {-].]]].]]]$[ Esc}/,
    'Swiss'                 => q/{Sfr -]']]]']]].[}/,
);

for my $nationality (keys %format) {
    my $layout = $format{$nationality};
    print form "$nationality:",
               "    $layout",
                    \@amounts,
               "\n";
}

and bestie, it serves:

Swiss:
              Sfr 0.0
              Sfr 1.0
              Sfr 1.23
          Sfr 1'234.56
         Sfr -1'234.56
      Sfr 1'234'567.89

Canadian (French):
                  0,0 $
                  1,0 $
                  1,23 $
              1 234,56 $
             -1 234,56 $
          1 234 567,89 $

Dutch:
                  0.0EUR
                  1.0EUR
                  1.23EUR
              1,234.56EUR
              1,234.56-EUR
          1,234,567.89EUR

Norwegian:
               kr 0,0
               kr 1,0
               kr 1,23
           kr 1.234,56
          kr -1.234,56
       kr 1.234.567,89

German (pre-euro):
                  0,0DM
                  1,0DM
                  1,23DM
              1.234,56DM
             -1.234,56DM
          1.234.567,89DM

Indian:
                  0.0 Rs
                  1.0 Rs
                  1.23 Rs
              1,234.56 Rs
             -1,234.56 Rs
          12,34,567.89 Rs

Portuguese (pre-euro):
                  0$0 Esc
                  1$0 Esc
                  1$23 Esc
              1.234$56 Esc
             -1.234$56 Esc
          1.234.567$89 Esc

Canadian (English):
                 $0.0
                 $1.0
                 $1.23
             $1,234.56
            -$1,234.56
         $1,234,567.89

Like. Look at that. Every single one, correct, and nobody had to hand-place a single dollar sign. ๐Ÿ˜ญโœจ


๐ŸŒˆ Verbatim fields ๐ŸŒˆ

Sometimes though? You do NOT want form to touch your data at all. You've got a block of data that's already laid out exactly how you want it, and you just want it beside some other text โ€” no reformatting, no rejustification, no opinions. Enter: verbatim fields.

So say we have this little string, an absolute banger of a string:

$diagram = <<EODNA;
   G==C
     A==T
       T=A
       A=T
     T==A
   G===C
  T==A
 C=G
TA
AT
 A=T
  T==A
    G===C
      T==A
EODNA

(okay yes in the actual page it's indented like this:)

    $diagram = <<EODNA;
       G==C
         A==T
           T=A
           A=T
         T==A
       G===C
      T==A
     C=G
    TA
    AT
     A=T
      T==A
        G===C
          T==A
    EODNA

and we wanna put it next to some other text. It's already carefully formatted, so the last thing we want is to interpolate it into a left-justified field:

print form
    '{[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]}       {[[[[[[[[[[[[[[[}',
     $diatribe,                                        $diagram;

because that would absolutely SQUASH our lovely helix ๐Ÿงฌ๐Ÿ˜ญ:

Men at  some  time  are  masters  of  their       G==C
fates: / the fault, dear Brutus, is not  in       A==T
our genes, / but in ourselves, that we  are       T=A
underlings.  /  Brutus  and  Caesar:   what       A=T
should be in that 'Caesar'?  /  Why  should       T==A
that DNA be sequenced more  than  yours?  /       G===C
Extract them together, yours is as  fair  a       T==A
genome; / transcribe them, it  doth  become       C=G
mRNA as well; / recombine them,  it  is  as       TA
long; clone with 'em, / Brutus will start a       AT
twin as soon as Caesar. / Now, in the names       A=T
of all  the  gods  at  once,  /  upon  what       T==A
proteins doth our Caesar feed, / that he is       G===C
grown so great?                                       T==A

And ngl, right-, full-, centre- or numeric-justification would not rescue you here either. None of them. ๐Ÿ™ˆ What you actually need is "leave-it-the-hell-alone" justification โ€” a field specifier that lays the data out exactly as it is, leading whitespace included, zero notes.

And that's literally the whole purpose of a verbatim field:

  • A verbatim single-line field ({'''''''''}) grabs the next line of data it's offered and inserts as much of it as will fit in the field's width, preserving whitespace "as is". ๐ŸŽ€
  • A verbatim block field ({"""""""""} ) grabs every line of the data it's offered and interpolates it into the text without any reformatting or justification. ๐Ÿ’–

And that's precisely what our diagram needed:

print form
    '{[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]}       {"""""""""""""""}',
     $diatribe,                                        $diagram;

to produce:

Men at  some  time  are  masters  of  their          G==C
fates: / the fault, dear Brutus, is not  in            A==T
our genes, / but in ourselves, that we  are              T=A
underlings.  /  Brutus  and  Caesar:   what              A=T
should be in that 'Caesar'?  /  Why  should            T==A
that DNA be sequenced more  than  yours?  /          G===C
Extract them together, yours is as  fair  a         T==A
genome; / transcribe them, it  doth  become        C=G
mRNA as well; / recombine them,  it  is  as       TA
long; clone with 'em, / Brutus will start a       AT
twin as soon as Caesar. / Now, in the names        A=T
of all  the  gods  at  once,  /  upon  what         T==A
proteins doth our Caesar feed, / that he is           G===C
grown so great?                                         T==A

LOOK AT HER. Untouched. Thriving. ๐Ÿ‘‘๐Ÿงฌ

Now here's the important part, bestie, so lock in ๐Ÿ”’: unlike other types of fields, verbatim fields don't break and wrap their data if that data doesn't fit on a single line. Nope. Instead, they truncate each line to the appropriate field width. So a too-short verbatim field:

print form
    '{[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]}       {""""""}',
     $diatribe,                                        $diagram;

results in gene slicing โœ‚๏ธ๐Ÿงฌ:

Men at  some  time  are  masters  of  their          G==C
fates: / the fault, dear Brutus, is not  in            A==
our genes, / but in ourselves, that we  are              T
underlings.  /  Brutus  and  Caesar:   what              A
should be in that 'Caesar'?  /  Why  should            T==
that DNA be sequenced more  than  yours?  /          G===C
Extract them together, yours is as  fair  a         T==A
genome; / transcribe them, it  doth  become        C=G
mRNA as well; / recombine them,  it  is  as       TA
long; clone with 'em, / Brutus will start a       AT
twin as soon as Caesar. / Now, in the names        A=T
of all  the  gods  at  once,  /  upon  what         T==A
proteins doth our Caesar feed, / that he is           G===
grown so great?                                         T=

rather than teratogenesis ๐Ÿ˜ญ (do NOT do this):

Men at  some  time  are  masters  of  their          G==C
fates: / the fault, dear Brutus, is not  in            A=-
our genes, / but in ourselves, that we  are       =T
underlings.  /  Brutus  and  Caesar:   what              -
should be in that 'Caesar'?  /  Why  should       T=A
that DNA be sequenced more  than  yours?  /              -
Extract them together, yours is as  fair  a       A=T
genome; / transcribe them, it  doth  become            T=-
mRNA as well; / recombine them,  it  is  as       =A
long; clone with 'em, / Brutus will start a          G===C
twin as soon as Caesar. / Now, in the names         T==A
of all  the  gods  at  once,  /  upon  what        C=G
proteins doth our Caesar feed, / that he is       TA
grown so great?                                  AT
                                               A=T
                                                T==A
                                                  G==-
                                              =C
                                                    T-
                                              ==A
That second one is a genuine crime scene. Truncate, don't overflow. ๐Ÿšจ

๐Ÿ”ฅ Overflow fields ๐Ÿ”ฅ

It's not uncommon for a report to need a series of data fields in one column and then a second column with only single field, perhaps containing a summary or discussion of the other data. For example, we might want to produce recipes of the form:

=================[  Hecate's Broth of Ambition  ]=================
             Preparation time:             Method:
                66.6 minutes                  Remove the legs from the
                                              lizard, the wings from the
             Serves:                          owlet, and the tongue of the
                2 doomed souls                adder. Set them aside.
                                              Refrigerate the remains (they
             Ingredients:                     can be used to make a lovely
                2 snakes (1 fenny, 1          white-meat stock). Drain the
                adder)                        newts' eyes if using pickled.
                2 lizards (1 legless,         Wrap the toad toes in the
                1 regular)                    bat's wool and immerse in half
                3 eyes of newt (fresh         a pint of vegan stock in
                or pickled)                   bottom of a preheated
                2 toad toes (canned           cauldron. (If you can't get a
                are fine)                     fresh vegan for the stock, a
                2 cups of bat's wool          cup of boiling water poured
                1 dog tongue                  over a vegetarian holding a
                1 common or spotted           sprouted onion will do). Toss
                owlet                         in the fenny snake, then the
                                              legless lizard. Puree the
                                              tongues together and fold
                                              gradually into the mixture,
                                              stirring widdershins at all
                                              times.  Allow to bubble for 45
                                              minutes then decant into two
                                              tarnished copper chalices.
                                              Garnish each with an owlet
                                              wing, and serve immediately.

okay so bestie, there are literally so many ways to pull off that effect. The most obvious? Format each column separately and then lay them out side-by-side with a pair of verbatim fields:

           my $prep = form 'Preparation time:        ',
                           '   {<<<<<<<<<<<<<<<<<<<<}', $prep_time,
                           '                         ',
                           'Serves:                  ',
                           '   {<<<<<<<<<<<<<<<<<<<<}', $serves,
                           '                         ',
                           'Ingredients:             ',
                           '   {[[[[[[[[[[[[[[[[[[[[}', $ingredients;

           my $make = form 'Method:                          ',
                           '   {[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
                               $method;

           print form
               '=================[ {||||||||||||||||||||||||||} ]=================',
                                             $recipe,
               '                                                                  ',
               '  {"""""""""""""""""""""""}     {"""""""""""""""""""""""""""""""} ',
                  $prep,                        $make;

We could even chain the calls to form to eliminate the interim variables:

           print form
               '=================[ {||||||||||||||||||||||||||} ]=================',
                                             $recipe,
               '                                                                  ',
               '  {"""""""""""""""""""""""}     {"""""""""""""""""""""""""""""""} ',
                  form('Preparation time:        ',
                       '   {<<<<<<<<<<<<<<<<<<<<}', $prep_time,
                       '                         ',
                       'Serves:                  ',
                       '   {<<<<<<<<<<<<<<<<<<<<}', $serves
                       '                         ',
                       'Ingredients:             ',
                       '   {[[[[[[[[[[[[[[[[[[[[}', $ingredients,
                      ),
                  form('Method:                          ',
                       '   {[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
                           $method,
                      );

While it's impressive to be able to do that kind of nested formatting (and highly useful in extreme formatting scenarios), it's also far too ungainly for regular use. A cleaner, more maintainable solution is use a single format and just build the method column up piecemeal, like so:

           print form
               '=================[ {||||||||||||||||||||||||||} ]=================',
                                             $recipe,
               '                                                                  ',
               'Preparation time:               Method:                           ',
               '   {<<<<<<<<<<<<<<<<<<<<}          {<<<<<<<<<<<<<<<<<<<<<<<<<<<:} ',
                   $prep_time,                     $method,
               '                                   {:<<<<<<<<<<<<<<<<<<<<<<<<<<:} ',
                                                   $method,
               'Serves:                            {:<<<<<<<<<<<<<<<<<<<<<<<<<<:} ',
                                                   $method,
               '   {<<<<<<<<<<<<<<<<<<<<}          {:<<<<<<<<<<<<<<<<<<<<<<<<<<:} ',
                   $serves,                        $method,
               '                                   {:<<<<<<<<<<<<<<<<<<<<<<<<<<:} ',
                                                   $method,
               'Ingredients:                       {:<<<<<<<<<<<<<<<<<<<<<<<<<<:} ',
                                                   $method,
               '   {[[[[[[[[[[[[[[[[[[[[}          {:[[[[[[[[[[[[[[[[[[[[[[[[[[[} ',
                   $ingredients,                   $method;

That produces exactly the same result as the previous versions, because each follow-on {:<<<<<<<:} field in the Method column grabs one extra line from $method, and then the final follow-on {:[[[[[[} field grabs as many more as are required to lay out the rest of the contents of the variable. The only down-side is that the resulting code is still downright ugly. With all those tedious repetitions of the same variable, there's far too much $method in our madness. ๐Ÿ˜ญ

Having a series of follow-on fields like this โ€“ vertically continuing a single column across subsequent format lines โ€“ is so common that form provides a special shortcut: the {VVVVVVVVV} overflow field. โœจ

An overflow field automagically duplicates the field specification immediately above it. The important point being that, because that duplication includes copying the preceding field's data source, overflow fields don't require a separate data source of their own. ๐Ÿ’…

Using overflow fields, we could rewrite our quotation generator like this:

           print form
               '=================[ {||||||||||||||||||||||||||} ]=================',
                                             $recipe,
               '                                                                  ',
               'Preparation time:               Method:                           ',
               '   {<<<<<<<<<<<<<<<<<<<<}          {<<<<<<<<<<<<<<<<<<<<<<<<<<<<} ',
                   $prep_time,                     $method,
               '                                   {VVVVVVVVVVVVVVVVVVVVVVVVVVVV} ',
               'Serves:                            {VVVVVVVVVVVVVVVVVVVVVVVVVVVV} ',
               '   {<<<<<<<<<<<<<<<<<<<<}          {VVVVVVVVVVVVVVVVVVVVVVVVVVVV} ',
                   $serves,
               '                                   {VVVVVVVVVVVVVVVVVVVVVVVVVVVV} ',
               'Ingredients:                       {VVVVVVVVVVVVVVVVVVVVVVVVVVVV} ',
               '   {[[[[[[[[[[[[[[[[[[[[}          {VVVVVVVVVVVVVVVVVVVVVVVVVVVV} ',
                   $ingredients,
               '                                   {VVVVVVVVVVVVVVVVVVVVVVVVVVVV} ';

Which would once again produce the recipe shown earlier. ๐Ÿ‘‘

Note that the overflow fields interact equally well in formats with single-line and block fields. That's because block overflow fields have one other special feature: they're non-greedy. Unless we specify otherwise, all types of block fields will consume their entire data source. For example, if we wrote:

           print form {layout=>"across"},
                '{<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>:}',
                                         $speech,
                '{:<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>:}',
                                         $speech,
                '{:[[[[[]]]]]:}   {="""""""""""""""""""=}   {:[[[[[]]]]]]:}',
                    $speech,             $advert,              $speech,
                '{:[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}',
                                         $speech;

we'd get:

           Now is the winter of our discontent / Made glorious summer
           by this sun of York; / And all the clouds that lour'd upon
           our house / In                             the deep  bosom
           of  the  ocean                             buried.  /  Now
           are our  brows                             bound      with
           victorious                                 wreaths; /  Our
           bruised   arms                             hung   up   for
           monuments;   /                             Our       stern
           alarums          +---------------------+   changed      to
           merry            |                     |   meetings, / Our
           dreadful         | Eat at Mrs Miggins! |   marches      to
           delightful       |                     |   measures. Grim-
           visaged    war   +---------------------+   hath   smooth'd
           his   wrinkled                             front;  /   And
           now,   instead                             of     mounting
           barded  steeds                             / To fright the
           souls       of                             fearful
           adversaries, /                             He       capers
           nimbly  in   a                             lady's chamber.

That's because the two {:[[[[[]]]]]:} block fields on either side of the verbatim advertisement field will eat all the data in $speech, leaving nothing for the final format. Then the advertisement will be centred on the two resulting columns of text. ๐Ÿ“

But, block overflow fields are different. They only take as many lines as are required to fill the lines generated by the non-overflow fields in their format. So, if we changed our code to use overflows:

           print form {layout=>"across"},
                '{<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>}', $speech,
                '{VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
                '{VVVVVVVVVVVV}   {="""""""""""""""""""=}   {VVVVVVVVVVVVV}', $advert,
                '{VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}';

we get both a cleaner specification and a more elegant result: โœจ

Now is the winter of our discontent / Made glorious summer
by this sun of York; / And all the clouds that lour'd upon
our house / In                             the deep  bosom
of  the  ocean   +---------------------+   buried.  /  Now
are our  brows   |                     |   bound      with
victorious       | Eat at Mrs Miggins! |   wreaths; /  Our
bruised   arms   |                     |   hung   up   for
monuments;   /   +---------------------+   Our       stern
alarums                                    changed      to
merry meetings,  /  Our  dreadful  marches  to  delightful
measures. Grim-visaged  war  hath  smooth'd  his  wrinkled
front; / And now, instead of mounting barded steeds  /  To
fright the souls  of  fearful  adversaries,  /  He  capers
nimbly in a lady's chamber.

Okay bestie, notice how in that third format line of the example above, the two overflow fields sitting on either side of the advertisement are each overflowing from the single field that's above both of them? Yeah. That's a multiple overflow and it's totally valid ๐Ÿ’… but it does mean you gotta spell out how those fields overflow โ€” like, as two separate columns of text, or (like in this case) as one single broken column running across the page. That's literally the whole job of the "layout" option on the first line. It gets explained in detail below. โœจ

The "{VVVVVVVV}" fields only consumed as much data from $speech as was required to sandwich the output lines created by the verbatim advertisement. And this is important, not just cute: it means we can lay out a series of block fields in one column and a single overflowed field in another column without introducing ugly gaps. For example โ€” because the "{VVVVVVVVV}" fields in:

print form
    'Name:                                                  ',
    '  {[[[[[[[[[[[[}                                       ', $name,
    '                  Biography:                           ',
    'Status:             {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}', $bio,
    '  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}', $status,
    '                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    'Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    '  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}', $comments;

only consume as much of the overflowing $bio field as necessary, the result is something like:

Name:
  William
  Shakespeare
                  Biography:
Status:             William Shakespeare was born on
  Deceased (1564    April 23, 1564 in Strathford-upon-
  -1616)            Avon, England; he was third of
                    eight children from Father John
Comments:           Shakespeare and Mother Mary Arden.
  Theories          Shakespeare began his education at
  abound as to      the age of seven when he probably
  the true          attended the Strathford grammar
  author of his     school. The school provided
  plays. The        Shakespeare with his formal
  prime             education. The students chiefly
  alternative       studied Latin rhetoric, logic, and
  candidates        literature. His knowledge and
  being Sir         imagination may have come from his
  Francis           reading of ancient authors and
  Bacon,            poetry. In November 1582,
  Christopher       Shakespeare received a license to
  Marlowe, or       marry Anne Hathaway. At the time of
  Edward de         their marriage, Shakespeare was 18
  Vere              years old and Anne was 26. They had
                    three children, the oldest Susanna,
                    and twins- a boy, Hamneth, and a
                    girl, Judith. Before his death on
                    April 23 1616, William Shakespeare
                    had written thirty-seven plays. He
                    is generally considered the
                    greatest playwright the world has
                    ever known and has always been the
                    world's most popular author.

Now imagine "{VVVVVVVVVVV}" fields ate their entire data โ€” the way "{[[[[[[[[[}" or "{IIIIIIIIII}" fields do. Then the output would be so much less satisfactory ๐Ÿ˜ญ. That first block overflow field for $bio would have to consume the whole entire biography before the comments field was even reached. So you'd get something like:

Name:
  William
  Shakespeare
                  Biography:
Status:             William Shakespeare was born on
  Deceased (1564    April 23, 1564 in Strathford-upon-
  -1616)            Avon, England; he was third of
                    eight children from Father John
                    Shakespeare and Mother Mary Arden.
                    Shakespeare began his education at
                    the age of seven when he probably
                    attended the Strathford grammar
                    school. The school provided
                    Shakespeare with his formal
                    education. The students chiefly
                    studied Latin rhetoric, logic, and
                    literature. His knowledge and
                    imagination may have come from his
                    reading of ancient authors and
                    poetry. In November 1582,
                    Shakespeare received a license to
                    marry Anne Hathaway. At the time of
                    their marriage, Shakespeare was 18
                    years old and Anne was 26. They had
                    three children, the oldest Susanna,
                    and twins- a boy, Hamneth, and a
                    girl, Judith. Before his death on
                    April 23 1616, William Shakespeare
                    had written thirty-seven plays. He
                    is generally considered the
                    greatest playwright the world has
                    ever known and has always been the
                    world's most popular author.

Comments:
  Theories
  abound as to
  the true
  author of his
  plays. The
  prime
  alternative
  candidates
  being Sir
  Francis
  Bacon,
  Christopher
  Marlowe, or
  Edward de
  Vere

Which is precisely why "{VVVVVVVVVVV}" fields don't work that way. Not me defending the design, it's just correct ๐Ÿซถ


โœ‚๏ธ Line-breaking โœ‚๏ธ

Whenever a field gets passed more data than it can fit in a single line, form is forced to break that data somewhere. No cap.

So if the field in question is W columns wide, form first squeezes any whitespace (as specified by the user's "ws" option) and then looks at the next W columns of the string. ๐Ÿง

Then form's breaking algorithm searches for a newline, a carriage return, any other whitespace character, or a hyphen. If it finds a newline or carriage return within the first W columns, it immediately breaks the data string right there. Otherwise it locates the last whitespace or hyphen in the first W columns and breaks the string immediately after that space or hyphen. And if it can't find anywhere suitable to break the string at all? It breaks it at the (W-1)th column and appends a hyphen. Iconic, honestly.

So, for example:

$data = "You can play no part but Pyramus;\nfor Pyramus is a sweet-faced man";

print form "|{[[[[[}|",
             $data;

prints:

|You can|
|play no|
|part   |
|but    |
|Pyramu-|
|s;     |
|for    |
|Pyramus|
|is a   |
|sweet- |
|faced  |
|man    |

Notice the line-breaks after can (at a whitespace), part (after a whitespace), sweet- (after a hyphen), and s; (at a newline). Also notice that Pyramus; doesn't fit in the field, so it has to get chopped in two with a hyphen shoved in. ๐Ÿ’€

And yeah, this particular style of line-breaking may not suit every application, and maybe you'd rather form used some other algorithm. For example, if form used the TeX breaking algorithm it would have broken Pyramus; less clumsily, giving you:

|You can|
|play no|
|part   |
|but    |
|Pyra-  |
|mus;   |
|for    |
|Pyramus|
|is a   |
|sweet- |
|faced  |
|man    |

To support all these different line-breaking strategies, form provides the "break" option. The "break" option's value must be a closure/subroutine, which then gets called whenever a data string needs to be broken to fit a particular field width. โœจ

That subroutine is passed three arguments: a reference to the data string itself, an integer specifying how wide the field is, and a regex indicating which (if any) characters are to be squeezed. It's expected to return a list of two values: a string which is taken as the "broken" text for the field, and a boolean value indicating whether or not any data remains after the break (so form knows when to stop breaking the data string). The subroutine is also expected to update the .pos of the data string to point immediately after the break it has imposed. ๐Ÿ‘‘

For example, if we always wanted to break at the exact width of the field (with no hyphens), we could do that with:

sub break_width {
    my ($data_ref, $width, $ws) = @_;
    for ($$data_ref) {
        # Treat any squeezed or vertical whitespace as a single character
        # (since they'll subsequently be squeezed to a single space)
        my $single_char = qr{ $ws | [\n\r]+ | . }

        # Give up if there are no more characters to grab...
        return ("", 0) unless m/\G (single_char{1,$width}) /gcx;

        # Squeeze the resultant substring...
        (my $result = $1) =~ s/ $ws | [\n\r] / /gx;

        # Check for any more data still to come...
        my $more = m/\G (?= .* \S) /gcx;

        # Return the squeezed substring and the "more" indicator...
        return ($result, $more);
    }
}

print form
    {break=>\&break_width},
    '|{[[[[[}|',
      $data;

producing:

|You can|
|play no|
|part bu|
|t Pyram|
|us; for|
|Pyramus|
|is a sw|
|eet-fac|
|ed man |

Or we might prefer to break on every single whitespace-separated word:

sub break_word {
    my ($data_ref, $width, $ws) = @_;
    for ($$data_ref) {
        # Locate the next word (no longer than $width cols)
        my $found = m/\G \s* (\S{1,$width}) /gcx;

        # Fail if no more words...
        return ("", 0) unless $found;
        my $word = $1;

        # Check for any more data still to come...
        my bool $more = m/\G (?= .* \S) /gcx;

        # Otherwise, return broken text and "more" flag...
        return ($word, $more);
    }
}

print form
    {break=>\&break_word},
    '|{[[[[[}|',
      $data;

and the glow-up it produces is:

|You    |
|can    |
|play   |
|no     |
|part   |
|but    |
|Pyramus|
|;      |
|for    |
|Pyramus|
|is     |
|a      |
|sweet-f|
|aced   |
|man    |

we'll run into user-defined breaking one more time when we get to user-defined fields, so like, keep it in your notes bestie ๐Ÿ’œ


Interleaving data ๐Ÿซถ

okay so when you're calling form with more than one format, there are (at least) three schools of thought about how to lay it out. the "traditional" way โ€” i.e. the way Perl 5 formats do it โ€” is to interleave each format string with a line holding the data it's supposed to interpolate, with every datum lined up directly under the field it's gonna get fitted into. like this:

print form
    'Name:                                                  ',
    '  {[[[[[[[[[[[[}                                       ',
       $name,
    '                  Biography:                           ',
    'Status:             {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}',
                         $bio,
    '  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
       $status,
    '                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    'Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    '  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
       $comments;

the serve here is that it self-documents: if you wanna know what a particular field is meant to hold, you just look straight down one line. iconic behaviour honestly.

<b>but</b> โ€” and this is the tradeoff โ€” it does chop up the "abstract picture" that the formats are painting, which can make it way harder to picture what the final formatted text is actually gonna look like. so some people prefer to dump all the data off to the right of the formats instead:

print form
    'Name:                                                  ',
    '  {[[[[[[[[[[[[}                                       ', $name,
    '                  Biography:                           ',
    'Status:             {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}', $bio,
    '  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}', $status,
    '                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    'Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    '  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}', $comments;

and that's perfectly acceptable too, bestie. no gatekeeping here ๐Ÿซถ

sometimes tho, the data you wanna interpolate just isn't sitting there all neatly pre-packaged in separate variables that are easy to sprinkle between the formats. like, maybe it's a list that some subroutine handed back โ€” get_info($next_person) โ€” or maybe it's chillin' in a hash: @person{qw( name biog stat comm )}. and in those cases it is such a nuisance to have to tease all that data out into separate variables (or hash accesses) and then yeet them through the formats one by one:

print form
    'Name:                                                  ',
    '  {[[[[[[[[[[[[}                                       ',$person{name},
    '                  Biography:                           ',
    'Status:             {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}',$person{biog},
    '  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',$person{stat},
    '                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    'Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',
    '  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}',$person{comm};

so form has an option for exactly this ๐Ÿ’… it lets you drop a single, multi-line format at the start of the argument list, put all the data together after it, and have that data automatically interleaved as needed. and not surprisingly, that option is: interleave. it's normally used together with a heredoc, since that's the easiest way to specify a multi-line string in Perl:

print form {interleave=>1}, <<'EOFORMAT',
Name:
  {[[[[[[[[[[[[}
                  Biography:
Status:             {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}
  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
EOFORMAT
     @person{qw( name biog stat comm )};

so when interleave is in effect, form grabs the first string argument it's handed and breaks that argument up into individual lines. then it treats those individual lines as a series of distinct formats and grabs as many of the remaining arguments as are required to supply data for each one. it's giving auto-pilot fr fr ๐Ÿฆ‹


Multi-line formats โœจ

important thing to flag, bestie: even when we're rocking form's default non-interleaving behaviour, it's still totally fine to use a format that spans multiple lines. <b>but</b> there is a significant (and genuinely useful) difference in behaviour between those two alternatives ๐Ÿ‘€

the normal behaviour of form is to take each format string, fill in every field in that format with a substring from the corresponding data source, and then repeat that whole process until all the data sources have been exhausted. which means a multi-line format like this:

print form
     <<'EOFORMAT',
Name:    {[[[[[[[[[[[[[[[}   Role: {[[[[[[[[[[}
Address: {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}
_______________________________________________
EOFORMAT
     \@names, \@roles, \@addresses;

would normally produce this:

Name:    King Lear           Role: Protagonist
Address: The Cliffs, Dover
_______________________________________________
Name:    The Three Witches   Role: Plot devices
Address: Dismal Forest, Scotland
_______________________________________________
Name:    Iago                Role: Villain
Address: Casa d'Otello, Venezia
_______________________________________________

because that entire three-line format gets repeatedly filled in as a single unit, line-by-line and datum-by-datum ๐ŸŽ€

on the other hand, if we tell form it's supposed to automatically interleave the data coming after the format, like so:

print form {interleave=>1},
     <<'EOFORMAT',
Name:    {[[[[[[[[[[[[[[[}   Role: {[[[[[[[[[[}
Address: {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}
_______________________________________________
EOFORMAT
     \@names, \@roles, \@addresses;

then the call produces:

Name:    King Lear           Role: Protagonist
Name:    The Three Witches   Role: Plot devices
Name:    Iago                Role: Villain
Address: The Cliffs, Dover
Address: Dismal Forest, Scotland
Address: Casa d'Otello, Venezia
_______________________________________________

because that second version is really just equivalent to:

print form
     'Name:    {[[[[[[[[[[[[[[[}   Role: {[[[[[[[[[[}',
                   \@names,                  \@roles,
     'Address: {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
                   \@addresses,
     '_______________________________________________';

and ngl that's not much use in <i>this</i> particular example, but it was exactly what was needed for the biography example earlier. it's just a matter of choosing the right type of data placement to get the particular effect you want, periodt ๐Ÿ’…


Layout control ๐Ÿ’ซ

as we saw earlier, with follow-on fields and overflow fields, form is perfectly happy to have several fields in a single format that are all fed by the same data source. for example:

print form
    '{[[[[[[[[]]]]]]]]]]:}   {:[[[[[[[]]]]]]]]]]:}   {:[[[[[[[[]]]]]]]]]]}',
         $soliloquy,             $soliloquy,              $soliloquy;

and honestly? that kind of format is especially useful for making multi-column outputs โ€” newspaper columns, that whole vibe ๐Ÿ“ฐโœจ

but a small quandry arises ๐Ÿ˜ญ in what order should form fill in these fields? should the data be formatted down the page, filling each column completely before starting the next one (and therefore potentially leaving the last column "short"):

Now is the winter  of   torious  wreaths;   /   front; / And now, in-
our discontent / Made   Our bruised arms hung   stead of mounting ba-
glorious  summer   by   up for  monuments;  /   rded steeds / To fri-
this sun of  York;  /   Our stern alarums ch-   ght the souls of fea-
And  all  the  clouds   anged to merry meeti-   rful  adversaries,  /
that lour'd upon  our   ngs, /  Our  dreadful   He capers nimbly in a
house / In  the  deep   marches to delightful   lady's chamber.
bosom  of  the  ocean   measures.   /   Grim-
buried. / Now are our   visaged war hath smo-
brows bound with vic-   oth'd  his   wrinkled

or should the data be run line-by-line across all three columns (the way a Perl 5 format does it), filling one line completely before starting the next:

Now is the winter  of   our discontent / Made   glorious  summer   by
this sun of  York;  /   And  all  the  clouds   that lour'd upon  our
house / In  the  deep   bosom  of  the  ocean   buried. / Now are our
brows bound with vic-   torious  wreaths;   /   Our bruised arms hung
up for  monuments;  /   Our stern alarums ch-   anged to merry meeti-
ngs, /  Our  dreadful   marches to delightful   measures.   /   Grim-
visaged war hath smo-   oth'd  his   wrinkled   front; / And now, in-
stead of mounting ba-   rded steeds / To fri-   ght the souls of fea-
rful  adversaries,  /   He capers nimbly in a   lady's chamber.

or should the text run down the columns, but in such a way as to leave those columns as evenly balanced in length as possible:

Now is the winter  of   brows bound with vic-   visaged war hath smo-
our discontent / Made   torious  wreaths;   /   oth'd  his   wrinkled
glorious  summer   by   Our bruised arms hung   front; / And now, in-
this sun of  York;  /   up for  monuments;  /   stead of mounting ba-
And  all  the  clouds   Our stern alarums ch-   rded steeds / To fri-
that lour'd upon  our   anged to merry meeti-   ght the souls of fea-
house / In  the  deep   ngs, /  Our  dreadful   rful  adversaries,  /
bosom  of  the  ocean   marches to delightful   He capers nimbly in a
buried. / Now are our   measures.   /   Grim-   lady's chamber.

okay so plot twist bestie ๐Ÿซถ there is literally no "right" answer to any of that. it depends entirely on what kind of effect we're tryna achieve. like, that's the whole vibe.

  • The first approach (i.e. lay out the text down each column first) works well if we're formatting a news-column, or a report, or a description of some kind. ๐Ÿ’–
  • The second (i.e. lay out the text across each line first), is excellent for putting diagrams or call-outs in the middle of a piece of text (as we did for Mrs Miggins ๐Ÿ‘‘).
  • The third approach (i.e. lay out the data downwards but balance the columns) is best for presenting a single list of data in multiple columns โ€” like ls does. no cap.

so obviously we need an option to tell form which of these useful alternatives we want for a particular format โœจ That option is named layout and can take one of three string values: "down", "across", or "balanced". slay.

so, for example, to produce three versions of Richard III's famous monologue in the order shown above, we'd use:

print form {layout=>"down"},
    '{[[[[[[[[]]]]]]]]]]:}   {:[[[[[[[]]]]]]]]]]:}   {:[[[[[[[[]]]]]]]]]]}',
         $soliloquy,             $soliloquy,              $soliloquy;

then:

print form {layout=>"across"},
    '{[[[[[[[[]]]]]]]]]]:}   {:[[[[[[[]]]]]]]]]]:}   {:[[[[[[[[]]]]]]]]]]}',
         $soliloquy,             $soliloquy,              $soliloquy;

then:

print form {layout=>"balanced"},
    '{[[[[[[[[]]]]]]]]]]:}   {:[[[[[[[]]]]]]]]]]:}   {:[[[[[[[[]]]]]]]]]]}',
         $soliloquy,             $soliloquy,              $soliloquy;

by the way ๐ŸŽ€ the default value for the layout option is "balanced" since formatting regular columns of data is more common than formatting news or advertising inserts. iykyk.

๐ŸŒˆ Tabular layout ๐ŸŒˆ

The layout option controls one other form of inter-column formatting: tabular layout. ๐Ÿ‘‘

so far, all the examples of tables we've created (for example, our normalized scores) lined up nicely. But that was only because each item in each row happened to take the same number of lines (typically just one). So, a table generator like this:

my @play = map {"$_\r"}  ( "Othello", "Richard III", "Hamlet"   );
my @name = map {"$_\r"}  ( "Iago",    "Henry",       "Claudius" );

print form
     'Character       Appears in  ',
     '____________    ____________',
     '{[[[[[[[[[[}    {[[[[[[[[[[}',
      \@name,         \@play;

correctly produces:

Character       Appears in
____________    ____________
Iago            Othello

Henry           Richard III

Claudius        Hamlet

Note that we appended "\r" to each element to add an extra newline after each entry in the table ๐Ÿ’… We can't use "\n" to specify a line-break within an array element, because form uses "\n" as an "end-of-element" marker. So, to allow line breaks within a single element of an array datum, form treats "\r" as "end-of-line-but-not-end-of-element" (somewhat like Perl 5's format does).

However ๐Ÿ˜ญ if we were to use the full titles for each character and each play:

my @play = map {"$_\r"}  ( "Othello, The Moor of Venice",
                           "The Life and Death of King Richard III",
                           "Hamlet, Prince of Denmark",
                         );

my @name = map {"$_\r"}  ( "Iago",
                           "Henry,\rEarl of Richmond",
                           "Claudius,\rKing of Denmark",
                         );

the same formatter would produce:

Character       Appears in
____________    ____________
Iago            Othello, The
                Moor of
Henry,          Venice
Earl of
Richmond        The Life and
                Death of
Claudius,       King Richard
King of         III
Denmark
                Hamlet,
                Prince of
                Denmark

the problem is that the two block fields we're using just grab all the data from each array and format it independently into each column ๐Ÿ™ˆ Usually that's fine because the columns are independent (as we've previously seen).

But in a table, the data in each column specifically relates to data in other columns, so corresponding elements from the column's data arrays ought to remain vertically aligned โœจ To achieve this, we simply tell form that the data in the various columns should be laid out like a table:

print form {layout=>"tabular"},
     'Character       Appears in  ',
     '____________    ____________',
     '{[[[[[[[[[[}    {[[[[[[[[[[}',
      \@name,         \@play;

which then produces the desired result:

Character       Appears in
____________    ____________
Iago            Othello, The
                Moor of
                Venice

Henry,          The Life and
Earl of         Death of
Richmond        King Richard
                III

Claudius,       Hamlet,
King of         Prince of
Denmark         Denmark

๐Ÿฆ‹ Give him line and scope... ๐Ÿฆ‹

sometimes we want to use a particular option or combination of options in every single call we make to form ๐Ÿ’– Or, more likely, in every call we make within a specific scope. For example, we might wish to default to a different line-breaking algorithm everywhere, or we might want to make repeated use of a new type of field specifier, or we might want to reset the standard page length from a printable 60 to a screenable 24.

so the Perl6::Form module provides a mechanism by which options can be prebound โœจ To use it, we (re-)load the module with an explicit argument list:

use Form { layout=>"down", locale=>1, interleave=>1 };

This causes the module to export a modified version of form in which the specified options are prebound. That modified version of form takes effect from the line following the use statement, until the end of the current package (or another use Perl6::Form statement). The effect is not truly lexical (as it would be in Perl 6). ๐Ÿ’€

These default options are handy if we have a series of calls to form that all need some consistent non-standard behaviour. For example:

use Form { layout=>"across",
           interleave=>1,
           page => { header => "Draft $(localtime)\n\n" },
         };

print form $introduction_format, \@introduction_data;

while ($format, @data = get_next) {
    print form $format, @data;
}

print form $conclusion_format, \@conclusion_data;

๐ŸŽ€ Declarative field widths ๐ŸŽ€

when specific field widths are required (perhaps by some design document or data formatting protocol ๐Ÿ“„) laying out wide fields can be error-prone. For example, most people can't visually distinguish between a 52-column field and a 53-column field and are therefore forced to manually verify the width of the corresponding field specifier in some way ๐Ÿ˜ญ

to catch mistakes of this kind, fields can be specified with an embedded integer in parentheses (with optional whitespace inside the parens). For example:

print form '{[[[( 15 )[[[[} {<<<<<(17)<<<<<<}  {]]](14)]]].[[}',
           $problem,        $ID,               $description;

The integer in the parentheses acts like a checksum โœจ Its value must be identical to the actual width of the field (including the delimiting braces and the embedded integer itself). Otherwise an exception is thrown. For instance, running the above example produces the error message:

Inconsistent width for field 3.
Specified as '{]]](14)]]].[[}' but actual width is 15
in call to &form at demo.pl line 1

Numeric fields can be given a decimal checksum, which then also specifies their number of decimal places ๐Ÿ’…

print form
    '{[[[( 15 )[[[[} {<<<<<(17)<<<<<<}  {]](14.2)]].[}',
    $problem,        $ID,               $description;

Note that the digits before the decimal still indicate the total width of the field. So the {]](14.2)]].[} field in the above example means must be 14 columns wide, including 2 decimal places, in exactly the same way as a %14.2f specifier would in a sprintf. periodt.


๐Ÿ’œ Imperative field widths ๐Ÿ’œ

of course, in some instances it would be much more convenient if we could simply tell form that we want a particular field to be a particular width, instead of having to explicitly show it ๐Ÿ™ˆ

so there's another type of integer field annotation that, instead of acting like a checksum, acts like an... err... "tellsum" ๐Ÿ˜ญ That is, we can tell form to ignore a field's physical width and instead insist that it be magically expanded (or shrunk) to a nominated width. Such a field is said to have an imperative width. The integer specifying the imperative width is placed in curly braces instead of parens.

For example, the format in the previous example could be specified imperatively as:

print form
    '{[{15}[} {<{17}<<}  {]]]]{14.2}]]]].[[}',
    $problem, $ID,       $description;

Note that the actual width of any field becomes irrelevant if it contains an imperative width ๐Ÿ”ฅ The field will be condensed or expanded to the specified width, with subsequent fields pushed left or right accordingly.


โญ Distributive field widths โญ

a special form of imperative width field is the starred field โœจ A starred field is one that contains an imperative width specification in which the number is replaced by a single asterisk.

The width of a starred field is not fixed, but rather is computed during formatting. That width is whatever is required to cause the entire format to fill the current page width of the format (by default, 78 columns). Consider, for example:

print form
    '{]]]]]]]]]]]]]]} {]]].[[}  {[[{*}[[}  ',
     \@names,         \@scores, \@comments;

The width of the starred comment field in this case is 49 columns โ€“ the default page width of 78 columns minus the 29 columns consumed by the fixed-width portions of the format (including the other two fields). iconic. ๐Ÿ’–

If a format has two or more starred fields, the available space gets split equally between them. No favorites, no drama ๐Ÿ’…. So like, say you wanna build two equal columns (perfect for comparing two files side by side), you'd do:

use Perl6::Slurp;

print form
     '{[[[[{*}[[[[}   {[[[[{*}[[[[}',
      slurp($file1),  slurp($file2);

(And yes bestie, Perl 6 does have a built-in "slurp" function that takes a filename, opens the file, reads the whole entire contents, and hands it back as one single string. For the full tea, see the Perl6::Slurp module โ€” it's on the CPAN now ๐Ÿซถ.)

There's one special case for starred fields, and it's the starred verbatim field:

{""""{*}""""}

It acts like every other starred field โ€” it grows according to the available space โ€” except it will never grow any wider than the widest line of the data it's formatting. So, like, a regular starred field:

print form
     '| {[[{*}[[} |',
        $monologue;

expands to the full page width:

| Now is the winter of our discontent                           |
| Made glorious summer by this sun of York;                     |
| And all the clouds that lour'd upon our house                 |
| In the deep bosom of the ocean buried.                        |
| Now are our brows bound with victorious wreaths               |
| Our bruised arms hung up for monuments;                       |
| Our stern alarums changed to merry meetings,                  |
| Our dreadful marches to delightful measures.                  |
| Grim-visaged war hath smooth'd his wrinkled front;            |
| And now, instead of mounting barded steeds                    |
| To fright the souls of fearful adversaries,                   |
| He capers nimbly in a lady's chamber.                         |

...but a starred verbatim field:

print form
     '| {""{*}""} |',
        $monologue;

only expands as much as is strictly necessary to fit the data. Iconic behaviour honestly:

| Now is the winter of our discontent                |
| Made glorious summer by this sun of York;          |
| And all the clouds that lour'd upon our house      |
| In the deep bosom of the ocean buried.             |
| Now are our brows bound with victorious wreaths;   |
| Our bruised arms hung up for monuments;            |
| Our stern alarums changed to merry meetings,       |
| Our dreadful marches to delightful measures.       |
| Grim-visaged war hath smooth'd his wrinkled front; |
| And now, instead of mounting barded steeds         |
| To fright the souls of fearful adversaries,        |
| He capers nimbly in a lady's chamber.              |

โœจ Extensible fields โœจ

Okay so by now you've probably clocked that "form" and "(s)printf" have a lot of overlap in what they can do. For example, this call:

for (@procs) {
    print form
        '{>>>}  {<<<<<<<(20)<<<<<<<}  {>>>>>>}  {>>.}%',
        $_->{pid}, $_->{cmd},         $_->{time}, $_->{cpu};
}

does roughly the same thing as this call:

for (@procs) {
    printf "%5d  %-20s  %8s  %5.1f%%\n",
           $_->{pid}, $_->{cmd}, $_->{time}, $_->{cpu};
}

One is more WYSIWYG, the other is more concise, but (drop them in a suitable loop) they'd both spit out lines like:

     2461  vi -ii henry           0:55.83   11.6%
     2395  ex cathedra            0:06.59    3.5%
     2439  head anne.boleyn       0:00.18    0.1%
     2581  dig -short grave       0:01.04    0.0%

But โ€” and this is the crucial part โ€” there's a big difference between these two formatting facilities, and it only shows up when one of your processes runs over 99 hours. So say your browser has been running continuously for a few months (or, to be precise, for 1214:23.75 hours). Then the "printf" calls would print:

     2461  vi -ii henry           0:55.83   11.6%
     2395  ex cathedra            0:06.59    3.5%
    27384  lynx www.divorce.com  1214:23.75    0.8%
     2439  head anne.boleyn       0:00.18    0.1%
     2581  dig -short grave       0:01.04    0.0%

while the "form" calls would print:

     2461  vi -ii henry           0:55.83   11.6%
     2395  ex cathedra            0:06.59    3.5%
    27384  lynx www.divorce.com  1214:23-    0.8%
     2439  head anne.boleyn       0:00.18    0.1%
     2581  dig -short grave       0:01.04    0.0%

In other words: field widths in "printf" are minimal spacing (even if that throws off the whole layout), whereas field widths in "form" are guaranteed spacing (even if that truncates some of your data). Not the vibe, but it's the truth ๐Ÿ˜ญ.

Now, in a situation like this โ€” where you know the data might not fit and you do not want it truncated โ€” you could reach for a block field instead:

for (@procs) {
    print form
        '{>>>}  {<<<<<<<(19)<<<<<<}  {]]]]]]}  {>>.%}',
        $_->{pid}, $_->{cmd},        $_->{time},  $_->{cpu};
}

and you'd get:

     2461  vi -ii henry           0:55.83   11.6%
     2395  ex cathedra            0:06.59    3.5%
    27384  lynx www.divorce.com  1214:23-    0.8%
                                      .75
     2439  head anne.boleyn       0:00.18    0.1%
     2581  dig -short grave       0:01.04    0.0%

That keeps the data, sure, but the results are still ugly, and it also requires some fancy footwork โ€” making the percentage sign part of the field specification, like it's a currency marker โ€” just to get that last field working right. Bestie, it's a kludge. Periodt. The sad truth is that sometimes variable-width fields are just the better solution.

So "form" provides those too ๐ŸŽ€. Any field specification may include a plus sign ("+") anywhere between its braces, and when it does, it specifies an extensible field: a field whose width is minimal, rather than absolute. So in the above example, our "form" call should actually look like this:

for (@procs) {
    print form
        '{>>>}  {<<<<<<<(20)<<<<<<<}  {>>>>>+}  {>>.}%',
        $_->{pid}, $_->{cmd},        $_->{time},  $_->{cpu};
}

and it would produce this:

     2461  vi -ii henry           0:55.83   11.6%
     2395  ex cathedra            0:06.59    3.5%
    27384  lynx www.divorce.com  1214:23.75    0.8%
     2439  head anne.boleyn       0:00.18    0.1%
     2581  dig -short grave       0:01.04    0.0%

just like "printf" does ๐Ÿ’….

Likewise, if we thought the command names might run past 20 columns, we could let that field stretch too:

for (@procs) {
    print form
        '{>>>}  {<<<<<<<(20+)<<<<<<}  {>>>>>+}  {>>.}%',
        $_->{pid}, $_->{cmd},        $_->{time},  $_->{cpu};
}

Note that the field width specifier would still warn us if the field's "picture" wasn't exactly 20 columns wide, but the resulting field would stretch as necessary to fit longer data anyway. ๐Ÿ‘‘

Whitespace squeezing

When a field is getting filled in, whitespace is normally left as-is (except for justification, and wrapping of lines in block fields). But you can change that behaviour by specifying a whitespace squeezing strategy. Squeezing replaces those substrings of the data that match a specified pattern (like "/\s+/" for example), substituting a single space character.

If we don't want the default (non-)squeezing strategy, we can use the "ws" option to specify the particular pattern used for squeezing:

print form
    {ws=>qr/\h+/},           # squeeze any horizontal whitespace
    $format1, \@data1,
    {ws=>qr/$comment|\s+/},  # now squeeze comments or whitespace
    $format2, \@data2;

For example, say we've got a eulogy generator:

sub eulogize ($who, $to, $blaming) {...}

that (rather poorly ๐Ÿ’€) drops the appropriate names into a pre-formatted template, producing strings like:

Friends,   Romans  , countrymen, lend me your ears;
I come to bury    Caesar   , not to praise him.
The evil that men do lives after them;
The good is oft interred with their bones;
So let it be with    Caesar    . The noble    Brutus
Hath told you     Caesar     was ambitious:
If it were so, it was a grievous fault,
And grievously hath    Caesar    answer'd it.

If we interpolate that string โ€” with all its extra spaces and embedded newlines โ€” into a "form" field:

print form
     '| {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[} |',
        eulogize('Caesar', 'Romans', 'Brutus');

we'd get:

| Friends,   Romans  , countrymen, lend me   |
| your ears;                                 |
| I come to bury    Caesar   , not to praise |
| him.                                       |
| The evil that men do lives after them;     |
| The good is oft interred with their bones; |
| So let it be with    Caesar    . The noble |
| Brutus                                     |
| Hath told you     Caesar     was           |
| ambitious:                                 |
| If it were so, it was a grievous fault,    |
| And grievously hath    Caesar    answer'd  |
| it.                                        |

Note that those extra spaces and embedded newlines are preserved in the resulting text.

But if we tell "form" to squeeze all whitespaces:

print form {ws => qr/\s+/},
     '| {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[} |',
        eulogize('Caesar', 'Romans', 'Brutus');

we'd get:

| Friends, Romans , countrymen, lend me your |
| ears; I come to bury Caesar , not to       |
| praise him. The evil that men do lives     |
| after them; The good is oft interred with  |
| their bones; So let it be with Caesar .    |
| The noble Brutus Hath told you Caesar was  |
| ambitious: If it were so, it was a         |
| grievous fault, And grievously hath Caesar |
| answer'd it.                               |

with each sequence of characters matching "/\s+/" reduced down to a single space.

On the other hand, if we wanted to keep the newlines and squeeze only horizontal whitespace, that would be:

print form {ws => qr/[ \t]+/},
     '| {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[} |',
        eulogize('Caesar', 'Romans', 'Brutus');

which produces:

| Friends, Romans , countrymen, lend me your |
| ears;                                      |
| I come to bury Caesar , not to praise him. |
| The evil that men do lives after them;     |
| The good is oft interred with their bones; |
| So let it be with Caesar . The noble       |
| Brutus                                     |
| Hath told you Caesar was ambitious:        |
| If it were so, it was a grievous fault,    |
| And grievously hath Caesar answer'd it.    |

okay but bestie look at that ๐Ÿ’€ โ€” for this particular text, none of those solutions is fully cute. Squeezing the whitespaces down to a single space still leaves a stray single space hanging around in spots like "Caesar ." and "Romans ,". Not the vibe.

To yeet those blemishes we gotta flex a more sophisticated aspect of form's whitespace squeezing behaviour. Namely: when squeezing whitespace using a particular pattern, form detects if that pattern captures anything and then doesn't squeeze the captured items. ๐Ÿ‘‘

More precisely, fr fr:

  • if the squeeze pattern matches but doesn't capture, form simply replaces the entire match with a single space character.
  • but if the squeeze pattern does capture, form doesn't insert a space character โ€” instead it replaces the entire match with the concatenation of the captured substrings.

So that means we can completely eliminate any whitespace before a punctuation character with:

print form {ws => qr/[ \t]+ ([.!?,:;])?/},
     '| {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[} |',
        eulogize('Caesar', 'Romans', 'Brutus');

which produces the desired:

| Friends, Romans, countrymen, lend me your  |
| ears;                                      |
| I come to bury Caesar, not to praise him.  |
| The evil that men do lives after them;     |
| The good is oft interred with their bones; |
| So let it be with Caesar. The noble Brutus |
| Hath told you Caesar was ambitious:        |
| If it were so, it was a grievous fault,    |
| And grievously hath Caesar answer'd it.    |

โœจ iconic. โœจ

Why does it work? Because in those instances where the pattern matches some whitespace followed by one of the punctuation characters, the punctuation character gets captured, and that captured character is then used to replace the entire whitespace-plus-punctuator. On the flip side, if the pattern matches whitespace but no punctuator (and it's allowed to do that, because the punctuator is optional), then nothing is captured, so form falls back to replacing the whitespace with a single space. No cap. ๐Ÿซถ


โœจ Field filling โœจ

Fields are (almost) always of a fixed width. So if there isn't enough data to fill a particular field, the unused portions of that field get filled in with spaces โ€” this is to preserve the vertical alignment of other columns of formatted data. ๐Ÿ“ But spaces are only the default. The hfill (horizontal fill) option can be used to change fillers. For example:

print form
    {hfill=>"=-"},                  # Fill next fields with "=-"
    '{|{*}|}',                      # Full width field for title
    '[ Table of Contents ]',        # Title
    {hfill=>" ."},                  # Fill next fields with spaced dots
    '   {[[[[[{*}[[[[[}{]]]}   ',   # Two indented block fields
        \@contents,    \@page;      # Data for those blocks

This fills the empty space either side of the centred title with a repeated =-=-=- sequence. It then fills the gaps to the right of the left-justified contents field, and to the left of the right-justified pages field, with spaced dots. Which, rather prettily, produces something like:

=-=-=-=-=-=-=-[ Table of Contents ]-=-=-=-=-=-=-=

   Foreword. . . . . . . . . . . . . . . . . .i
   Preface . . . . . . . . . . . . . . . . .iii
   Glossary. . . . . . . . . . . . . . . . . vi
   Introduction. . . . . . . . . . . . . . . .1
   The Tempest . . . . . . . . . . . . . . . .7
   Two Gentlemen of Verona . . . . . . . . . 17
   The Merry Wives of Winsor . . . . . . . . 27
   Twelfh Night. . . . . . . . . . . . . . . 39
   Measure for Measure . . . . . . . . . . . 50
   Much Ado About Nothing. . . . . . . . . . 62
   A Midsummer Night's Dream . . . . . . . . 73
   Love's Labour's Lost. . . . . . . . . . . 82
   The Merchant of Venice. . . . . . . . . . 94
   As You Like It. . . . . . . . . . . . . .105

Note that the fill sequence doesn't have to be a single character ๐ŸŽ€ and that the fill pattern is consistent across multiple fields and between adjacent lines. That is, it's as if every field is first filled with the same fill pattern, then the actual data written over the top. That's particularly handy in the above example, because it ensures that the fill pattern seamlessly bridges the boundary between the adjacent contents and pages fields. Chef's kiss. ๐Ÿ’…

It's also possible to specify separate fill sequences for the left- and right-hand gaps in a particular field, using the lfill and rfill options. This is particularly common for numerical fields. For example, this call to form:

print form
  'Name              Bribe (per dastardry)',
  '=============     =====================',
  '{[[[[[[[[[[[}         {]],]]].[[[}     ',
  \@names,               \@bribes;

would print something like:

Name              Bribe (per dastardry)
=============     =====================
Crookback                  12.676
Iago                        1.62
Borachio               45,615.0
Shylock                    19.0003

with the numeric field padded with whitespace and only showing as many decimal places as there are in the data.

However ๐Ÿ˜ญ โ€” in order to prevent subsequent...err...creative calligraphy (they are, after all, villains and would presumably not hesitate to add a few digits to the front of each number), we might prefer to put stars before the numbers and show all decimal places. We could do that like so:

print form
  'Name              Bribe (per dastardry)',
  '=============     =====================',
  '{[[[[[[[[[[[}         {]],]]].[[[}     ',
  \@names,               {lfill=>'*', rfill=>'0'},
                         \@bribes;

which would then print:

Name              Bribe (per dastardry)
=============     =====================
Crookback             *****12.6760
Iago                  ******1.6200
Borachio              *45,615.0000
Shylock               *****19.0003

Note that the lfill and rfill options are specified after the format string and, more particularly, before the data for the second field. This means that those options only take effect for that particular field and the previous fill behaviour is then reasserted for subsequent fields. Many other form options โ€“ for example ws, height, or break โ€“ can be specified in this way, so as to apply them only to a particular field. ๐Ÿ‘‘

There is also a general fill option that sets the default sequence for any filling that isn't otherwise specified.


๐ŸŽ€ Zero-filled numeric fields ๐ŸŽ€

Filling numeric fields with zeros is so common that form offers a shorthand notation for it. If the first character inside a numeric field specification is a zero, then the left-fill string for that field is set to 0. Likewise if the last character in the field is a zero, it is right-filled with zeros. For example:

my @nums = (0, 1, -1.2345, 1234.56, -1234.56, 1234567.89);

print form
    '{]]]].[[}     {]]]].[0}     {0]]].[[}     {0]]].[0}',
     \@nums,       \@nums,       \@nums,       \@nums;

prints:

    0.0           0.000     00000.0       00000.000
    1.0           1.000     00001.0       00001.000
   -1.234        -1.234     -0001.234     -0001.234
 1234.56       1234.560     01234.56      01234.560
-1234.56      -1234.560     -1234.56      -1234.560
#####.###     #####.###     #####.###     #####.###

(Yes, those ##### are the overflow vibes. We been knew. ๐Ÿ’€)


๐Ÿฆ‹ Up and down, up and down, I will lead them up and down... ๐Ÿฆ‹

Formatted text blocks are also filled vertically. Empty lines at the end of the block are normally filled with spaces (so as to preserve the alignment of any other fields on the same line). However, this too can be controlled, with the vfill option. Alternatively โ€“ as with horizontal filling โ€“ separate fill sequences can be specified for above and below the text using the tfill and bfill ("top" and "bottom" fill) options. ๐ŸŒธ

For example, if we had six elements in @task, but only four processors:

print form
    {bfill=>'[unallocated]'},
    'Task                      Processor',
    '====                      =========',
    '{[[[[[[[[[[[[[[[[[[[[}  {]]]]]][[[[[}',
     \@task,                     [1..4];

we'd get:

Task                      Processor
====                      =========
Borrow story                  1
Rename characters             2
Subdivide into scenes         3
Write dialogue                4
Check rhythm and meter  [unallocated]
Insert puns and japes   [unallocated]

the way the [unallocated] just slots itself in there ๐Ÿ˜Œ perfection.


๐Ÿ“ Height control ๐Ÿ“

It is possible to constrain the minimum and maximum number of lines that a particular format or block field must cover, regardless of how much data it contains. We do that using the height option. For example:

print form
    {height=>3},
    '{[[[[}{IIII}{]]]]}',
     $l,   $c,   $r;

This will cause the call to form to generate exactly three output lines, even if the contents of the data variables would normally fit in fewer lines or would actually require more. ๐Ÿ’…

To specify a range of heights we can use the min and max suboptions:

print form
    {height=>{ min=>3, max=>20 }},
    '{[[[[}{IIII}{]]]]}',
     $l,   $c,   $r;

This specifies that, no matter how much data is available, the output will be no less than three lines and no more than 20. ๐Ÿ“

Note, however, that the height option refers to the height of individual fields, not of entire output pages. we'll see how to control the latter shortly.

๐ŸŒธ Minimal height fields ๐ŸŒธ

As we saw earlier, a block overflow field ({VVVVVVVVV}) has the special property that it only overflows as much as necessary to fill the output lines generated by other block fields. That enabled us to create an overflowing column of text like so:

print form
    {interleave=>1}, <<EOFORMAT,
Name:
  {[[[[[[[[[[[[}
                  Biography:
Status:             {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}
  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
EOFORMAT
    $name,
    $biography,
    $status,
    $comments;

...without that first {VVVVVVVVV} field just inhaling all the data out of $bio and leaving a whole awkward gap between the Status and the Comments. bestie, rude. ๐Ÿ’€

That's a super handy feature fr, but restricting the "minimal height" feature to only overflow fields? Turns out that's lowkey not enough in the general case. Like, say we wanted the biography field to start all the way up at the first line of the output text:

Name:             Biography:
  William           William Shakespeare was born on
  Shakespeare       April 23, 1564 in Strathford-upon-
                    Avon, England; he was third of
Status:             eight children from Father John
  Deceased (1564    Shakespeare and Mother Mary Arden.
  -1616)            Shakespeare began his education at
                    the age of seven when he probably
Comments:           attended the Strathford grammar
  Theories          school. The school provided
  abound as to      Shakespeare with his formal
  the true          education. The students chiefly
  author of his     studied Latin rhetoric, logic, and
  plays. The        literature. His knowledge and
  prime             imagination may have come from his
  alternative       reading of ancient authors and
  candidates        poetry. In November 1582,
  being Sir         Shakespeare received a license to
  Francis           marry Anne Hathaway. At the time of
  Bacon,            their marriage, Shakespeare was 18
  Christopher       years old and Anne was 26. They had
  Marlowe, or       three children, the oldest Susanna,
  Edward de         and twins- a boy, Hamneth, and a
  Vere              girl, Judith. Before his death on
                    April 23 1616, William Shakespeare
                    had written thirty-seven plays. He
                    is generally considered the greatest
                    playwright the world has ever known
                    and has always been the world's most
                    popular author.

To pull that off, we would have needed a call to form like this:

print form
    {interleave=>1}, <<EOFORMAT,
Name:             Biography:
  {[[[[[[[[[[[[}    {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}
                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
Status:             {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
EOFORMAT
    $name,
    $biography,
    $status,
    $comments;

Ok so notice how the first line of the Biography field has to be a block field now, not a single-line field like in previous versions. It literally cannot be a single-line one, because the Name field is a block field and that would leave a gap in the Biography column, like:

Name:             Biography:
  William           William Shakespeare was born on
  Shakespeare
                    April 23, 1564 in Strathford-upon-
Status:             Avon, England; he was third of
                    etc.

So yeah โ€” it's gotta be a block field, to "keep up" with however much output that multi-line Name field decides to produce. ๐Ÿ˜ค But unfortunately, starting the Biography column with a normal block field doesn't fix it either. Nope. In fact we get this:

Name:             Biography:
  William           William Shakespeare was born on
  Shakespeare       April 23, 1564 in Strathford-upon-
                    Avon, England; he was third of
                    eight children from Father John
                    Shakespeare and Mother Mary Arden.
                    Shakespeare began his education at
                    the age of seven when he probably
                    attended the Strathford grammar
                    school. The school provided
                    Shakespeare with his formal
                    education. The students chiefly
                    studied Latin rhetoric, logic, and
                    literature. His knowledge and
                    imagination may have come from his
                    reading of ancient authors and
                    poetry. In November 1582,
                    Shakespeare received a license to
                    marry Anne Hathaway. At the time of
                    their marriage, Shakespeare was 18
                    years old and Anne was 26. They had
                    three children, the oldest Susanna,
                    and twins- a boy, Hamneth, and a
                    girl, Judith. Before his death on
                    April 23 1616, William Shakespeare
                    had written thirty-seven plays. He
                    is generally considered the
                    greatest playwright the world has
                    ever known and has always been the
                    world's most popular author.

Status:
  Deceased (1564
  -1616)

Comments:
  Theories
  abound as to
  the true
  author of his
  plays. The
  prime
  alternative
  candidates
  being Sir
  Francis
  Bacon,
  Christopher
  Marlowe, or
  Edward de
  Vere

Normal block fields are absolutely remorseless when it comes to consuming all their data. The first Biography field genuinely will not stop formatting. Ever. Until your entire $biography string is gone. no thoughts, just vibes, and the vibe is every single character. ๐Ÿ˜ญ

What we actually need here is a kinder, gentler block field โ€” a block field that formats minimally, just like an overflow field does. And guess what, we get exactly that with yet another "height" option: height=>"minimal". Like so:

print form
    {interleave=>1}, <<EOFORMAT,
Name:             Biography:
  {[[[[[[[[[[[[}    {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}
                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
Status:             {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
  {[[[[[[[[[[[[}    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
                    {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
Comments:           {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
  {[[[[[[[[[[[}     {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}
EOFORMAT
     $name,
     {height=>"minimal"}, $biography,
     $status,
     $comments;

When you slap this option onto a particular field (by putting it immediately before that field's data), that field only consumes as much of its data is is required to fill the output lines created by the other (non-minimal) fields in the same format. In this case that means the first Biography field only extracts as much data from $biography as is needed to fill the text lines created by the Name field. ๐ŸŽ€ so polite, so restrained, we love her.

And bestie, note that any kind of block field can be modified this way: justified, numeric, currency, or verbatim. all of them. periodt. ๐Ÿ’…


โœจ Underlining โœจ

As some of the examples we've already seen illustrate, formats are frequently built out of a set of column titles, followed by the matching columns of data. And typically, those column titles get underlined to make them stand out:

print form
  'Name              Bribe (per dastardry)',
  '=============     =====================',
  '{[[[[[[[[[[[}         {]],]]].[[[}     ',
  \@names,               \@bribes;

So form has an option that automates that whole process for you. ๐Ÿฆ‹ For example, the payments example above could also have been written:

print form
  'Name              Bribe (per dastardry)',
  {under=>"="},
  '{[[[[[[[[[[[}         {]],]]].[[[}     ',
  \@names,               \@bribes;

The under option takes a string and uses it to underline the most recently formatted line. It does this by examining the formats immediately before and after the under. Then it generates a series of underlines by repeating the specified underlining string as many times as required. The underlines are generated such that every field and every other non-whitespace literal in the preceding format has an underline under it, and every field/non-whitespace in the next format has an "overline" above it. ๐Ÿ’–

For example, this call to form:

print form
    '      Rank Name         Serial Number',
    {under=>"_"},
    '{]]]]]]]]} {[[[[[[[[[}     {IIIII}',
        \@ranks,\@names,        \@nums;

prints:

          Rank Name         Serial Number
__________ ___________  _____________
  Corporal Nym              CMXVII
Lieutenant Bardolph          CCIV
   Captain Spurio           MMMCDX
   General Pompey             XI

The usual effect is that the auto-generated underlines always extend to the edges of both the preceding title and the following field, whichever is wider. ๐Ÿ‘‘

Many people, of course, prefer to draw the underlines themselves, because the results are then way easier to visualize when you're staring at the code. The under option is most useful when we're constructing tables programmatically, with columns and column titles that are only known at run-time. iykyk. โœจ


โœจ Output trimming โœจ

The default fill-with-spaces behaviour of fields is useful to preserve the vertical alignment of columns within a formatted text, but it could also potentially increase the size of form's output unnecessarily. ๐Ÿ™ˆ For example, the following:

print form
    'To Do:',
    '   {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
        \@todo;

might produce something like:

To Do:
   Dissemble
   Deceive
   Dispute
   Defy
   Duel
   Defeat
   Dispatch

...which like, lowkey looks fine but bestie, look closer. Because every single line is being made by that big left-justified field that just auto-fills the rest with whitespace, the output is dragging around several hundred extra space characters it does not need. You probably didn't even notice them, but they're <em>all</em> there โ€” just chillin' off the right sides of the individual To-Do items. ๐Ÿ™ˆ

But fortunately, form is way smarter than that. Extraneous trailing whitespace on the right-hand side of any output line is automatically trimmed. So the above example actually produces:

To Do:
   Dissemble
   Deceive
   Dispute
   Defy
   Duel
   Defeat
   Dispatch

Of course, if you really do need those "invisible" trailing whitespaces for some reason, form has your back โ€” it provides a way to keep them, the untrimmed option:

print form {untrimmed=>1},
    'To Do:',
    '   {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
        \@todo;

๐ŸŒธ Page control ๐ŸŒธ

Normally, form assumes that whatever data it's formatting is supposed to produce a single, arbitrarily long, unbroken piece of text. But form can also format data into multiple pages of fixed length and width, inserting customized, page-specific headers, footers, and pagefeeds for each page. it's giving โœจdocument publishingโœจ

All these features are controlled by the the page option (or more precisely, by its various suboptions):

print form
    { page => { length => $page_len,        # Default: 60 lines
                width  => $page_width,      # Default: 78 columns
                number => $first_page_num,  # Default: 1
                header => \&make_header,    # Default: no header
                footer => \&make_footer,    # Default: no footer
                feed   => \&make_pagefeed,  # Default: no pagefeed
                body   => \&adjust_body,    # Default: no chiropracty
              }
    },
    $format,
    \@data;

๐Ÿฆ‹ Measure his woe the length and breadth of mine... ๐Ÿฆ‹

The page => { length => ... } suboption determines the number of output lines per page (including headers and footers). Normally, this suboption is set to infinity, which produces that single, arbitrarily long, unbroken page of text. But the suboption can be set to any positive integer value, to cause form to generate distinct pages of that many lines each. periodt.

Note however, that if the value specified for the page length results in no formatted text appearing on a page (because the available length is entirely consumed by headers or footers), then the length will be (silently) increased so that a single line of content appears on each page. not it just quietly fixing itself ๐Ÿ’…

The value of the page => { width => ... } suboption is used to determine the width of distributive fields and in some page body postprocessors. By default, this suboption is set to 78 (columns), but it may be set to any positive integer value.

The page => { number => ... } suboption specifies the current page number. By default it starts at 1, but may be set to any numeric value. This suboption is generally only of use in headers and footers (see below).

๐ŸŽ€ Headers and footers ๐ŸŽ€

The page => { header => ... } suboption specifies a hash containing a set of strings or subroutines that are to be used to create page headers. Each key of the hash indicates a particular kind of page that the corresponding value will provide the header for. For example:

header => { first => "           'The Tempest' by W. Shakespeare          ",
            last  => "                   -- The End --                    ",
            odd   => "Act $act, Scene $scene                              ",
            even  => "                                                    ",
            other => "          [Thys hedder intenshunally blanke]        ",
          }

Given the above specification, form will:

  • use the full title and author as the header of the first page,
  • write -- The End -- across the top of the last page,
  • prepend the act and scene information to the start of any odd page (except, of course, the first or the last), and
  • provide an empty line as the header of any even page (except the last, if it happens to be even).

Note that, in this case, since we've provided specific headers for <em>every</em> odd and even page, the other header will never be used. On the other hand, if we'd specified:

header => { first => "           'The Tempest' by W. Shakespeare          ",
            other => "                                       'The Tempest'",
          }

then every page except the first would have just a right-justified title at the top.

Of course, if we want <em>every</em> page to have the same header, we can just write:

header => { other => "                                       'The Tempest'"}

But that's a little klunky, so form also accepts a single string instead of a hash, to specify a header to be used for every page:

header  => "                                       'The Tempest'"

Headers don't all have to be the same size either. For example, we might prefer a more imposing first header:

header => { first => "                  'The Tempest'                   \n"
                   . "                        by                        \n"
                   . "                  W. Shakespeare                  \n"
                   . "____________________________________________________",

            other => "                                       'The Tempest'",
          }

form simply notes the number of lines each header requires and then reduces the available number of lines within the page accordingly, so as to preserve the exact overall page length. math girlie behavior fr fr ๐Ÿงฎ

Often we'll need headers that aren't fixed strings. For example, we might want each page to include the appropriate page number. So instead of a string, we're allowed to specify a particular header as a subroutine. That subroutine is then called each time that particular header is required, and its return value is used as the required header.

When the subroutine is called, the current set of active formatting options are passed to it as a list of pairs. Typically, then, the subroutine will specify one or more named-only parameters corresponding to the options it cares about, followed by a starred hash parameter to collect the rest. For example if every page should have its (left-justified) page number for a header:

header => sub { return $_[0]{page}{number}; }

Footers work in exactly the same way in almost all respects; the obvious exception being that they're placed at the end of a page, rather than the start.

Pagefeeds work the same way too. A pagefeed is a string that is placed between the footer of one page and the header of the next. They're like formfeeds, except they can be any string we choose. They're called "pagefeeds" instead of "formfeeds" because they're placed between <em>pages</em>, not between calls to form. iykyk ๐Ÿ’…

๐ŸŒˆ Page body postprocessing ๐ŸŒˆ

Sometimes it's useful to be able to grab the entire body of a page (i.e. the contents of the page between the header and footer) after it's been formatted together. For example, we might wish to centre those contents, or to crop them at a particular column.

To this end, the page => { body => ... } suboption allows us to specify a page body post-processor. That is, a subroutine or format that lays out the page's formatted text between the page's header and footer. Like the header, footer, and feed suboptions, the body suboption can take either a closure, a hash, or a string.

If the value of the body suboption is a string or a hash of pairs, the text of the body is (recursively) form'ed using that string (or those string values) as its format. A very common usage is to arrange for the formatted text to be horizonally and vertically centred on each page:

body => '{=I{*}I=}'

A more sophisticated variation on this is to use a hash to insert a left or right "gutter" for each page:

$gutter = " " x $gutter_width;

body => { odd   =>  $gutter ~ '{"""{*}"""}',
          even  =>  '{"""{*}"""} ' ~ $gutter,
        }

On the other hand, if the value of the body suboption is a subroutine, the body text is passed to that sub as a reference to an array of lines. A second array reference is also passed in, containing as many newlines as would be needed to pad out the body text to the correct number of lines for the page. Finally, the current formatting options are passed as a hash reference. As with the header etc. suboption, the closure is expected to return a single string (representing the final formatting of the page body).

For example, to add line numbers to the text each page (but not to the headers or footers or filler lines):

my $linenum = 1;

sub numerate {
    my @lines = @{$_[0]};
    my @fill  = @{$_[1]};
    my $page  = ${$_[2]}{page};

    # Compute range of line numbers
    my @linenums = ($linenum .. $linenum+@lines-1);

    # Reformat body lines verbatim,
    # with a left-justified line number before each...
    my $body = form '{[[[[} {"""{*}"""}',
                     \@linenums, \@lines,
                    @fill;

    # Update the final line number and return the new body text...
    $linenum += @lines;
    return $body;
}

print form
    page => { body   => \&numerate,
              header => "\n==========\n\n",
              length => 12,
            },
    # Left-justify the Briton...
    '{[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
    $soliloquy{RichardIII},
                     # Right-justify the Dane...
    '                 {]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}',
                      $soliloquy{Hamlet};

which produces:

==========

1      Now is the winter of our discontent /
2      Made glorious summer by this sun of
3      York; / And all the clouds that lour'd
4      upon our house / In the deep bosom of
5      the ocean buried. / Now are our brows
6      bound with victorious wreaths; / Our
7      bruised arms hung up for monuments; /
8      Our stern alarums changed to merry
9      meetings, / Our dreadful marches to

==========

10     delightful measures. Grim-visaged war
11     hath smooth'd his wrinkled front; / And
12     now, instead of mounting barded steeds
13     / To fright the souls of fearful
14     adversaries, / He capers nimbly in a
15     lady's chamber.

==========

(and bestie, we're not even done yet โ€” the drama continues โœจ)

           16                      To be, or not to be -- that is the question: /
           17                         Whether 'tis nobler in the mind to suffer /
           18                       The slings and arrows of outrageous fortune /
           19                         Or to take arms against a sea of troubles /
           20                       And by opposing end them. To die, to sleep --
           21                         / No more -- and by a sleep to say we end /
           22                      The heartache, and the thousand natural shocks
           23                      / That flesh is heir to. 'Tis a consummation /
           24                        Devoutly to be wished. To die, to sleep -- /

           25                         To sleep -- perchance to dream: ay, there's
           26                          the rub, / For in that sleep of death what
           27                         dreams may come / When we have shuffled off
           28                             this mortal coil, / Must give us pause.
           29                        There's the respect / That makes calamity of
           30                                                       so long life.

E<nbsp>

๐Ÿ‘‘ USER-DEFINED FIELDS ๐Ÿ‘‘

okay so Perl6::Form ships with a huge variety of field types, but bestie, not every possible type. Say you want a field that masks its data somehow โ€” like, a field that blanks out certain words by swapping them for the same number of X's. Very censor-era-of-Twitter energy. ๐Ÿ’…

We could always just do that by writing a subroutine that generates the right filter:

sub expurgate {
    my $hidewords = join "|", map quotemeta, @_;
    return sub {
        $_[0] =~ s/($hidewords)/ 'X' x length $1 /gixe;
        return $data;
    }
}

Then we could apply that subroutine to the data of any field that needed bowdlerization:

my $censor = expurgate qw(villain plot libel treacherous murderer false deadly 'G');

print form
    "[Ye following tranfcript hath been cenfored by Order of ye King]\n\n",
    '         {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
              $censor->($speech);

and it'd produce:

[Ye following tranfcript hath been cenfored by Order of ye King]

         And therefore, since I cannot prove a lover,
         To entertain these fair well-spoken days,
         I am determined to prove a XXXXXXX
         And hate the idle pleasures of these days.
         XXXXs have I laid, inductions dangerous,
         By drunken prophecies, XXXXXs and dreams,
         To set my brother Clarence and the king
         In XXXXXX hate the one against the other:
         And if King Edward be as true and just
         As I am subtle, XXXXX and XXXXXXXXXXX,
         This day should Clarence closely be mew'd up,
         About a prophecy, which says that XXX
         Of Edward's heirs the XXXXXXXX shall be.

And like, obviously, if this were Puritanism and not Perl, we'd be stuck with a long list of proscribed words we needed to excise from every single formatted text. ๐Ÿ™ˆ And in that case, instead of explicitly running every data source through the same censorious subroutine by hand, it would be so handy if form had a built-in field that did it for us automatically. Lowkey the dream.

Naturally, form doesn't have such a field built-in... but bestie, we can absolutely give it one. โœจ

User-defined field specifiers can be declared using the field option, which takes as its value an array of pairs. The key of each pair is a string or a rule (i.e. regex) that specifies the syntax of the user-defined field. The value of each pair is a closure/subroutine that constructs a standard field specifier to replace the user-defined specifier. Alternatively, the value of a pair may be a string, which is taken as the (static) field specifier to be used instead of the user-defined field.

In other words: each pair is a macro that maps a user-defined field (specified by the pair's key) onto a standard form field (specified by the pair's value). Like so:

field => [ qr/\{ X+ \}/x => \&censor_field ]

This tells form that whenever it finds a brace-delimited field consisting of one or more X's, it should call a subroutine named censor_field and use the return value of that call instead of the all-X field. ๐Ÿซถ

When the key of a field pair matches some part of a format, its corresponding subroutine is called. That subroutine is passed the Perl6ish result (i.e. $0) of the regex match, as well as a reference to the hash of active options for that field. Changes to the options hash will affect the subsequent formatting behaviour of that field. no cap.

So censor_field could be implemented like so:

# Constructor subroutine for user-defined censor fields...
sub censor_field {
    my ($field_spec, $opts) = @_;

    # Set up the field's 'break' option with a censorious break...
    $opts->{break} = break_and_censor($->opts{break});

    # Construct a left-justified field with the appropriate width
    # specified imperatively...
    return '{[[{' . length($field_spec) . '}[[}';
}

The censor_field subroutine has to change the field's break option, creating a new line breaker that also expurgates unsuitable words. To do this it calls break_and_censor, which returns a new line breaker subroutine:

# Create a new 'break' sub...
sub break_and_censor {
    my $original_breaker = @_;
    return sub {

        # Call the field's original 'break' sub...
        my ($nextline, $more) = $original_breaker->(@_);

        # X out any doubleplus ungood words
        $nextline =~ s/($proscribed_words)/ 'X' x length $1 /gixe;

        # Return the "corrected" version...
        return ($nextline, $more);
    }
}

Having created a subroutine to translate censor fields and another to break-and-expurgate the data placed in them, we are now in a position to create a module that encapsulates the new formatting functionality:

package Ministry::Of::Truth
use Perl6::Export;

# Internal mechanism (as above)...
my $proscribed = "...";
sub break_and_censor (&original_breaker) {...}
sub censor_field ($field_spec, %opts) {...}

# Make the new field type standard by default in this scope...
use Form { field => [ rx/\{ X+ \}/ => \&censor_field ] };

# Re-export the specialized &form that was imported above...
sub form is export(:DEFAULT) {...}

}

Okay, admittedly that's quite a lot of work. But the pay-off is huge: we can now trample on free speech much more easily:

use Ministry::Of::Truth;

print form
    "[Ye following tranfcript hath been cenfored by Order of ye King]\n\n",
    '        {XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}',
              $speech;

And we'd get the same carefully XXXX'ed output as before. iconic. ๐Ÿ‘‘

Single-column fields

User-defined fields are also a handy way to create single-character markers for single-column fields (in order to preserve the one-to-one spacing of a format). For example:

print form
    {field => { '^' => '{<III{1}III}',   # 1-char-wide, top-justified block
                '=' => '{<=II{1}II=}',   # 1-char-wide, middle-justified block
                '_' => '{<_II{1}II_}',   # 1-char-wide, bottom-justified block
              }
    },
    '~~~~~~~~~',
    '^ _ = _ ^',   qw(like round and orient perls),
    '~~~~~~~~~';

prints:

~~~~~~~~~
l     o p
i r a r e
k o n i r
e u d e l
  n   n s
  d   t
~~~~~~~~~

Single fields are particularly useful for labelling the vertical axes of a graph:

use Perl6::Form {field => [ '=' => '{<=II{1}II=}' ] };

$vert_label = "Success";
$hor_label  = "Time";

print form
   '   ^                                        ',
   ' = | {""""""""""""""""""""""""""""""""""""} ', $vert_label, \@data,
   '   +--------------------------------------->',
   '    {|||||||||||||||||||||||||||||||||||||} ', $hor_label;

which produces:

    ^
    |
    |       *
    |     *   *
  S |    *     *
  u |
  c |   *       *
  c |
  e |  *         *
  s |
  s |
    |
    | *           *
    +--------------------------------------->
                       Time

Specifying these kinds of single-character block markers is perhaps the commonest use of user-defined fields. But the:

field => [ '=' => '{<=II{1}II=}' ]

syntax is uncomfortably verbose for that purpose. So calls to form can also accept a short-hand notation to define a single-character field:

single => '='

or to define several at once:

single => ['#', '*', '+']

The single option does exactly the same thing as the field options shown above. It takes a single-character string, or a reference to an array of such strings, as its value. It then turns each of those strings into a single-column field marker. If the character is '=' then the field is vertically "middled" within its block. If the character is '_' then the field is "bottomed" within its block. If the single character is anything else, the resulting block is top-justified. So our previous example could also have been written:

print form
    {single => "="},
    '    ^                                        ',
    ' =  | {""""""""""""""""""""""""""""""""""""} ', $vert_label, \@data,
    '    +--------------------------------------->',
    '     {|||||||||||||||||||||||||||||||||||||} ', $hor_label;

๐Ÿ’Ž BULLETED LISTS ๐Ÿ’Ž

okay bestie so suppose we want a list where every single item is bulleted by "diamonds" ๐Ÿ’ :

           <> A rubber sword (laminated with mylar to
              look suitably shiny).
           <> Cotton tights (summer performances).
           <> Woolen tights (winter performances or
              those actors who are willing to admit
              to being over 65 years of age).
           <> Talcum powder.
           <> Codpieces (assorted sizes).
           <> Singlet.
           <> Double.
           <> Triplet (Kings and Emperors only).
           <> Supercilious attitude (optional).

something like this works well enough:

           for my $item (@items) {
               print form
                   '<> {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}', $item;
                   '   {VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV}';
           }

the first format produces the bullet plus the first line of text for the item, and then the second format handles any overflow of the item data. teamwork!! ๐Ÿซถ

alternatively, we could achieve the exact same result with a single format string by interpolating the bullet too:

           my $bullet = "<>";

           for my $item (@items) {
               print form
                   q[{''{*}''} {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}],
                    $bullet,  $item;
           }

here we use a single-line starred verbatim field ({''{*}''}), so the bullet gets interpolated "as-is" and the field is only as wide as the bullet itself. then for the item itself we use a block field, which will format the item data over as many lines as it needs. meanwhile โ€” and this is the cute part โ€” because the bullet's field is single-line, after the first line the bullet field fills up with spaces instead of a "diamond", leaving a bullet on the first line only. ๐Ÿ’…

this second approach also has the advantage that we could change the bullet string at run-time and the format would adapt automatically. iconic behaviour, no notes.

however, it's still a little irritating that we have to set up a loop and call form separately for each element of @items. after all, if we didn't need to bullet our list we could just write:

           print form
               '{[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
               \@items;

and form would take care of iterating over the @items for us. it really seems like things ought to be that easy for bulleted lists as well. ๐Ÿฅบ

and, of course, things are that easy.

all we need to do is tell form that whenever the string "<>" appears in a format, it should be treated as a bullet. that is, it should appear only beside the first line of text produced when formatting each element of the adjacent field's data.

to tell form all that, we use the bullet option:

           print form
               {bullet => "<>"},
               '<> {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}',
                   \@items;

the presence of this bullet option causes form to treat the sequence "<>" as a special field. that special field interpolates the string "<>" when the field immediately to its right begins to format a new data element, but thereafter interpolates only spaces until the adjacent field finishes formatting that data element.

or, to put it more simply: if we tell form that "<>" is a bullet, form treats it like a bullet that's attached to the very next field. ๐ŸŽฏ

so we could allow our Shakespearean roles example to handle multi-line character names, like so:

           print "The best Shakespearean roles are:\n\n";

           print form
               {bullet => "* "},
               '   * {[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[}   *{[[[[[[[[]]]]]]]]}*',
                     \@roles,                                \$disclaimer;

which could then produce something like:

          The best Shakespearean roles are:

             * Either of the 'two foolish             *WARNING:          *
               officers': Dogberry and Verges         *This list of roles*
             * That dour Scot, the Laird              *constitutes      a*
               Macbeth                                *personal   opinion*
             * The tragic Moor of Venice,             *only and is in  no*
               Othello                                *way  endorsed   by*
             * Rosencrantz's good buddy               *Shakespeare'R'Us. *
               Guildenstern                           *It   may   contain*
             * The hideous and malevolent             *nuts.             *
               Richard III                            *                  *

notice too that the asterisks on either side of the disclaimer aren't treated as bullets. that's because we defined a bullet to be "* ", and neither of the disclaimer asterisks has a space after it. sneaky little detail, lowkey love it โœจ

bullets can be any string we like, and there can be more than one of them in a single format. for example:

           print form
               {bullet => '+'},
               '+ {[[[[[[[[[[[[[[[[[[[:}       + {:[[[[[[[[[[[[[[[[[[[}',
                   \@items,                      \@items;

would print:

           + A rubber sword,                65 years of age).
             laminated with mylar         + Talcum powder.
             to look suitably             + Codpieces (assorted
             shiny.                         sizes).
           + Cotton tights (summer        + Singlet.
             performances).               + Double.
           + Woolen tights (winter        + Triplet (Kings and
             performances or those          Emperors only).
             actors who are willing       + Supercilious attitude
             to admit to being over         (optional).

โš ๏ธ WARNING โš ๏ธ

the syntax and semantics of Perl 6 is still being finalized and consequently is at any time subject to change. that means the same caveat applies to this module. so likeโ€ฆ don't come for me, okay? ๐Ÿ˜ญ

๐Ÿ“ฆ DEPENDENCIES ๐Ÿ“ฆ

requires: Perl 5.8.0, Perl6::Export, Scalar::Util, List::Util.


๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

Damian Conway (damian@conway.org)


        Copyright (c) 2003, Damian Conway. All Rights Reserved.
        This module is free software. It may be used, redistributed
           and/or modified under the same terms as Perl itself.

and that's a wrap on the whole page, bestie!! ๐ŸŽ€ this was Perl6::Form(3pm) as shipped with perl v5.36.0 and dated 2022-11-20 โ€” go forth and format, you absolute icon ๐Ÿ’–โœจ๐Ÿฆ‹

๐Ÿ“ฆ where this page comes from