Hints and Solutions to Selected Exercises

I have provided answers for some of the simple exercises which seem to have right answers, but for the larger ones I can only offer some hints. Even where there are answers, sometimes the questions are open to different interpretations, and you may disagree with mine. The important thing is to get a Perl script that behaves the way you think it ought to -- this isn't an exam or anything like that.

Chapter 2

2.1
Download this script and run it. If you don't like the format of the output, rewrite it once you have learned enough Perl.
Back to the question.
2.3
  1. Never mind the arithmetic, just think about the form of these numbers (how do they end?).
  2. \b(\d)(\d)\2\1\b
    The \bs make sure that we don't match longer numbers that contain a palindromic substring, such as 12411489.
  3. A nice one is [^\WQ] i.e. any character that isn't not a word character or Q. Got that?
Back to the question.
2.5
The brute-force approach of looking for a string with blue followed by arbitrary characters then green, or green similarly followed by blue is OK, and all that I would have thought of, but, for the connoisseurs of weirdness, this one is due to Randal Schwartz: /^(?=.*blue)(?=.*?green)/. Don't worry that you don't understand it; you will need to look up the lookahead assertions under regular expression extensions in the documentation or the Camel book. But don't worry if you still don't understand it then.
Back to the question.

Chapter 3

3.2
The operator in question being && (and).
Back to the question.
3.4
This is adequate:
#!perl -w
use English;

$total_lines = $quoted_lines = 0;
while (<>)
{
  ++$total_lines;
  ++$quoted_lines if /^>/;
}

$percent = ($quoted_lines * 100)/$total_lines;
print "$quoted_lines lines were quoted out of a total of $total_lines: $percent%\n";
though you might want to screen out entirely blank lines, and you should be worried about the spurious accuracy in output like this:
4 lines were quoted out of a total of 14: 28.5714285714286%
See Chapter 6 for ways of controlling this sort of thing.
Back to the question.
3.5
#!perl -w
while (<>)
{
  s/_([a-z])/\u$1/g ;
  print;
}
Slick, but it doesn't handle underlines at the very beginning or end, or two of them in a row. Can you do better?

(Everyone I showed this question to told me how awful they thought the mixed-case convention was, so maybe you ought to be doing the vice versa option to this one anyway.)
Back to the question.

Chapter 4

4.2
The stuff about JavaBeans is only window-dressing. All you need to do is distinguish between names matching five simple patterns. These patterns all share the common sub-pattern for property name, so I would define that separately and interpolate it, giving you something like this:
#!perl -w
use English;

$property_name = '[A-Z]\w*';
while (<>)
{
  chomp;
  if (/add($property_name)Listener/)
  {
    print $ARG, " : listener adder for $1 event\n";
  }
  elsif (/remove($property_name)Listener/)
  {
    print $ARG, " : listener remover for $1 event\n";
  }
  elsif (/get($property_name)/)
  {
    print $ARG, " : get method for $1 property\n";
  }
  elsif (/set($property_name)/)
  {
    print $ARG, " : set method for $1 property\n";
  }
  elsif (/is($property_name)/)
  {
    print $ARG, " : test method for Boolean property $1\n";
  }
  else
  {
    print $ARG, " : ordinary method or something\n";
  }
}
Here, I am just assuming the data consists of one name per line, as it does in my test data file. Try changing the script so it extracts the names from a Java program.
Back to the question.
4.5
A sequence of elsifs is always good enough, but ingenious Perl programmers have devised ways of abusing loop commands to make something that looks and behaves like a switch. I have to say that the effort doesn't strike me as worth it, but you can see what I'm talking about in the Perl manual/Camel book: look at the section Basic blocks and switch statements in the manual, or look up switch statement in the index to the Camel book (same material).
Back to the question.

Chapter 5

5.3
Here are the easy ones:
$x = shift(@a)         ==       $x = splice(@a, 0, 1)
unshift(@a, $x)        ==       splice(@a, 0, 0, $x)
(Actually, unshift can take any number of trailing arguments, just apply the same transformation to them.)
Back to the question.
5.4
My favourite version of this is the recursive one. To do the necessary checks on the input values and handle negative numbers correctly, it's best to use an auxiliary function.
sub num_to_base {
  my ($n, $b) = @ARG;
  if ($n < $b)
  {
    return sprintf("%X", $n);
  }
  else
  {
    return num_to_base($n/$b, $b) . sprintf("%X", $n%$b);
  }
}

sub number_to_base {
  my ($n, $b) = @ARG;
  ($b > 1 && $b <= 16) || die "number base must be between 2 and 16, not $b\n";
  return $n < 0? '-' . num_to_base(-$n, $b): num_to_base($n, $b);
}
I first came across this algorithm as the writen function in the BCPL standard library. (What do you mean, you never heard of BCPL?)

If you want to get rid of the restriction to bases less than 16, you have to provide a replacement for the call to sprintf, probably using a lookup table, but you also have to invent the extra digits.
Back to the question.

5.6
Frankly, I doubt that you can make the checks foolproof, and would want to see a formal proof in support of any claim that you had done so. (This is not an invitation and there are no prizes.)
Back to the question.
5.7
sub sort_strings {
  return sort {
    length($a) <=> length($b)
    } @ARG;
}
or define a separate comparison subroutine if you prefer.
Back to the question.
5.11
At the risk of insulting your intelligence, the phrase make no assumptions is emphasized because it means that you must use a hash (or possibly several) to remember which methods you have seen for each property or event name, and then check through them to see how each name should be classified. I would probably use five hashes, with code like this:
  elsif (/get($property_name)/)
  {
     ++$get_method{$1};
  }
and so on. You might even want to use a sixth, to record every name you encounter, so you can just iterate over its keys when you come to do the classification.
Back to the question.

Chapter 6

6.2
This gives you the longest paragraph measured in characters, which is not what the question asks for. You will need a way of measuring the number of words in a paragraph to replace the call to length. As in the text, I assume that paragraphs are separated by one or more blank lines.
$INPUT_RECORD_SEPARATOR = '';
$longest = '';
$longest_length = 0;
while (<>)
{
  length($ARG) > $longest_length and
    ($longest, $longest_length) = ($ARG, length($ARG))
}

print $longest;
My script is less efficient than it might be, too.
Back to the question.
6.4
Actually, this one is easier than I intended, because every image descriptor begins with a separator byte holding the value 0x2C, so all you have to do is scan the file from beginning to end counting those, although you would be advised to do some extra integrity checking. If you can get hold of a copy of The Encyclopedia of Graphics File Formats, you will find descriptions of the layout of almost any graphics file you are likely to meet. It is instructive to write Perl scripts to take them apart. You might also like to investigate modules on CPAN that other people have written to manipulate files in various formats.
Back to the question.
6.6
This is approximately the script we use to do this job. It falls somewhere between (a) and (b). Before we get on to this bit, there is some system-dependent stuff to set $folder to the pathname of the folder (directory) where the pictures are.
opendir DIR, $folder or die "I can't open $folder\n";
chdir $folder or die "couldn't change directory to $folder";  

@files = grep { /\.(\d+)$/ } (readdir DIR);
closedir DIR;

$highest = $#files;
$lowest = 0;

if (-e 'Reversed Temp File')
{
  die "Reverse temporary file already exists";
}

while ($highest > $lowest)
{
  rename $files[$highest], 'Reversed Temp File' or die "rename failed";
  rename $files[$lowest], $files[$highest] or die "rename failed";
  rename 'Reversed Temp File', $files[$lowest] or die "rename failed";
  ++$lowest;
  --$highest;
} 
The logic is essentially the same as you would use to reverse the elements of an array, if you had to do it by hand, except that instead of swapping elements we rename the files whose names they hold. In both cases, what you need to take care not to do is overwrite an element with the one you are swapping it with without saving its value somewhere. Here, I use a temporary file name where you would use a temporary variable while performing the exchange.
Back to the question.

Hints and solutions for later chapters | Perl: The Programmer's Companion