Sample solutions and discussion Perl Quiz of The Week #1 (20021016) ---------------------------------------------------------------- Write a function, 'center', whose argument is a list of strings, which will be lines of text. 'center' should insert spaces at the beginning of the lines of text so that if they were printed, the text would be centered, and return the modified lines. For example, center("This", "is", "a test of the", "center function"); should return the list: " This", " is", " a test of the", "center function" because if these lines were printed, they would look like: This is a test of the center function I had in mind a solution something like this one: sub center { my $maxwidth = 0; my @s = @_; for (@s){ $maxwidth = length($_) if length($_) > $maxwidth; } for (@s) { $_ = " " x (($maxwidth - length($_))/2) . $_; } @s; } Most of the solutions posted on the perl-qotw-discuss list did look very much like this. Peter Haworth used 'substr' to insert the spaces at the beginning of the lines. Using this technique in the code above would replace the "$_ = ..." line with: substr($_, 0, 0) = " " x ($maxwidth - length($_))/2; Other notes: 1. A lot of people seem not to understand what it means to "write a function which will return [some value]". Several people posted functions which would *print* centered text; this was not what the function was supposed to do. This always surprises me when I'm doing training classes, and it continued to suprise me when it came up this week. A function which prints out centered text is only about one one-hundredth as useful as a function which centers strings and returns them. If the function returns the values without printing them, the centered values can be modified and printed later; they can be incorporated into some larger data structure; they can be printed to STDERR as part of a warning message or to a file; they can be sent into some other command via a pipe; they can be sent over the network; they can be analyzed by some other part of the program and then discarded. A function which prints out the lines cannot be used in any of these ways. If I were teaching a class in how to be a professional programmer, I would mention this in the first week and then harp on it every day for the next two years. Most of what a programmer does is to design functions to be used by other programmers, and one of the biggest obstacles to making good software is that many functions are designed with broken interfaces so that they can't be used in more than one way. Functions should almost never print *anything* out. 2. Several people asked how wide a line the text should be centered in. The problem statement makes no remark about centering the text within a fixed-width line. In particular, the example should make clear that the text is not to be centered within an 80-character line, since if it were then the sample output would look like this: " this", " is", " a test of the", " center function", But it doesn't; as I said in the original message, it looks like this: " This", " is", " a test of the", "center function" Assuming that the output device is always 80 characters wide was a bad practice even in the 1970s when there was a physical reality that underlay it. Now, when I can have a 93-character-wide terminal at the touch of a button, it makes no sense at all. If you did decide to center the text within a fixed-width line, you should have provided the function with a parameter to determine the line width, perhaps something like this: sub center { my $linewidth = 80; if (ref $_[0]) { my $args = shift; $linewidth = $args->{LINEWIDTH}; } ... } Then the user has the option of calling 'center(...)' to center the text within your default-width line, or 'center({LINEWIDTH => 17}, ...)' to choose a different width. 3. Here's one minor point I found puzzling. Several people wrote expressions like this one: ( $maxlen / 2 ) - ( length( $_ ) / 2 ) When I see something like this, I usually want to rewrite it as ( $maxlen - length ( $_ ) ) / 2 unless for some reason the first one is a lot more perspicuous. Here the second one seems simpler to me: How many spaces? Take the length of the line, and subtract that from the width of the line; that says how much extra space is on the line; the amount of space to the left of the text is exactly half the extra space. For some reason this reminds me of a time when my aunt came to me to ask me for help with a math problem. She wanted a method for finding the number halfway in between two other numbers. Her method was to find the difference between the two numbers, divide it by 2, and then add the result back to the smaller number. She wanted to know if there was an easier way. There were two things about this that surprised me. The first was that she did not recognize that the average of two numbers is exactly the same as the number that is halfway in between them. The other surprsising thing is that in spite of years of mathematical training in high school and college, she was not able to frame her original method algebraically, discover that she was computing the expression x + (y-x)/2, and then reduce this to 2x/2 + (y-x)/2, to (2x+y-x)/2, and finally to (x+y)/2. I don't know what the point of this story is, except perhaps that something similar is going on with the (maxlen/2)-(length/2) expressions. A couple of people wrote something like: ' ' x ($max - length($_) / 2) apparently not realizing that this inserts far too many spaces. 4. Steve Smoot pointed out something I hadn't considered: The input strings themselves might contain newlines. The problem statement seems to preclude this, since it says that the argument strings "will be lines of text". But if you want to do something reasonable, it's quite easy: sub center { my @lines = map split /\n/, @_; # now adjust @lines... } 5. If a line couldn't be exactly centered in the space available, most people just shifted it a half-space to the left. Some people used 'int' to throw away the remainder after dividing by 2; some just took advantage of the implicit behavior of the 'x' operator to do the same thing. One person used 'use integer', which I think might be risky. 'integer' doesn't just mean 'integer'; it really means 'use the underlying C semantics for your operators', and so it may also change the behavior of operators like '&' and '%'. 6. Some people were tempted to use the centering in Perl's built-in 'format' feature. This seemed to be more trouble than it was worth. One such solution went like this: sub center { eval ("format STDOUT = \n" . ( "@" . ( "|" x (length (join "", @_ )))) . "\n" . '$_' . "\n.\n\n" ); for(@_) {write} } The idea here is to build up a format definition of a format that looks like this: format STDOUT = @||||||||||||||||||||||||| $_ . The '@|||||' tells the 'format' system that you want text centered. There are a number of defects here. One, easy to correct, is that it centers the text within a column whose width is equal to the *sub* of lengths of the input. This means that if you ask it to center fifty strings of ten characters each, the strings each get 245 spaces appended to the beginning. Another defect is that the centered data is printed, instead of being returned as a list. A more problematic defect is that if there was a 'STDOUT' format before, the subroutine has destroyed it. A better approach when dealing with dynamic formats is to use the 'formline' function, which provides access to the same internal formatting functions that Perl uses. Ron Isaacson did this, basing a solution on an 'swrite' function. 'swrite' formats data in the same way that Perl's 'format' feature would have, but returns the resulting string instead of printing it. use Carp; sub swrite { croak "usage: swrite PICTURE ARGS" unless @_; my $format = shift; $^A = ""; formline($format,@_); return $^A; } sub center { my @in = @_; my $len = (sort {$b <=> $a} map (length, @in))[0]; my $format = '@' . '|' x $len; map { swrite ($format, $_) } @in; } This works, but it does seem to use a lot of code and obscure features in proportion to the amount of work it does. 7. Randy J. Ray wondered if there wasn't any way to get the result with a single pass over the argument list instead of two passes. If there was, nobody found it. 8. Aaron Crane pointed out that instead of scanning the arguments to find the longest one, you could use the List::Util::max function. 9. As Tom Phoenix pointed out, Thereisneitherbonusnorhonoraccruedforomittingmostofthewhitespace. Perl Golf is three doors down on the left; the obfuscated contest is at the end of the hall down the stairs, and watch that first step; it's a doozy. I've decided it's too much trouble to decipher obfuscated solutions, so there may have been points of interest in some of them, but I don't really care. Thanks to everyone who has subscribed to this list, and to everyone who participated in the discussion. I'll send another quiz on Wednesday.