Mastering Algorithms with Perl by Jon Orwant, Jarkko Hietaniemi & John Macdonald Unconfirmed error reports are from readers. They have not yet been approved or disproved by the author or editor and represent solely the opinion of the reader. This page was updated April 17, 2007. Here's a key to the markup: [page-number]: serious technical mistake {page-number}: minor technical mistake : important language/formatting problem (page-number): language change or minor formatting problem ?page-number?: reader question or request for clarification UNCONFIRMED errors and comments from readers: {3} pseudocode, line 6: "try" should be "try-1." (9) last line: There's an extra "with". {12} Comment in quadratic() function: The comment "Compute the larger root of a quadratic polynomial" isn't correct. The root found isn't necessarily the larger one. For instance: f(x) = -x^2 + 2x + 3 The roots are 3 and -1, and the subroutine will return -1. {12} last line of the code sample: I believe the last line of the code sample contains an error. My copy of the book has: bruteforce => 'forloop(0, 1)' }); However, I believe that the word forloop should be changed to "bruteforce" so the code looks like: bruteforce => bruteforce(0, 1)' }); {85} 5th line from top: The comment says "node" could be undef, but it can't. (112) Fourth code block: need a space before the closing paren. {133} middle of figure 4-6: caption for "d" should be "shell = 2" {146} the fourth line of the source code; for ( my $i = length $array->[ 0 ] - 1; $i >= 0; $i-- ) { seems should be for ( my $i = length ($array->[ 0 ]) - 1; $i >= 0; $i-- ) { {156} It says that Shell's sort is stable, it isn't unless the only h-sort pass is h=1, which would then be called Insertion Sort, and that is not Shell's sort. If you don't believe me, se Table 1 P.382 of "The Art..., Volume 3: Sorting and Searching", 2nd Ed. Also, there is a missing ')' on the Shell's sort line of Table 4-2. [163] source code in ora.com: The @keywords array is not sorted even when they say on page 162: The prerequisite for a binary search is that the collection must be already sorted. {163} sub binary_string function: In the subroutine binary_string: my ($low, $high) = (0, calar@$array)); should be: my ($low, $high = (0, scalar(@$array)); {196} middle of table: 30km should be 12km. {200} Chart and first line of text following chart: The conclusion for the branch and bound algorithm states that the best route goes thru Wolfbane Corners. It should be Sula Junction. Also, the run chart shows the cost of urchin -> wolfbane corners -> sula center to be 44, it should be 46. {200} 1st paragraph: Best route from Urchin to Sula Center is to go through "Sula Junction", not through "Wolfbane Corners." And, on the chart right above the paragraph, the 'Cost So Far' column of the route Urchin->Wolfbane Corners -> Sula Center should be 56, not 44 as in the book, nor 46 which a reader pointed out on the unconfirmed errata. (205) 4th line from bottom: delete $Mammal { platypus } # Nor is platypus a mammal This is incorrect; the Platypus is a mammal. It is warm blooded, has fur and lactates. [215] middle of the page; The second and third versions of sub union() are wrong. The expected return value is a hashref where the keys are the elements of the set and the values are either undef or 1 or anything - it does not matter. The anonymous hash that is returned from the second and third versions of union() is initialized with keys only, no values. The value of the first key would be the second key. Not what was intended! Something like this would be correct: sub union {+{ map { $_, () } map { keys %$_ } @_ }} [246] last two lines: "$row" and "$column" should be switched. should read: $elem = at($matrix, $column, $row); # access set($matrix, $column, $row, $value); # modify [262] 3rd paragraph under "Computing the Determinant": "A 2x2 matrix defines a square (and the determinant gives its area)" I think this is confused. A 2x2 matrix defines a parallelogram. The easiest ones to think about are ones with only positive values for the coordinates, so I'll do that case. The pair (1,2), (4,2) defines a parallelogram whose vertices are at (0,0), (1,2), (4,2), and (5,4). The magnitude of the cross product of the vectors [4,2]x[1,2] is the area of that parallelogram, which is the determinant. This *does* correspond to the area of a rectangle (still not a square), but not one with vertices at (1,2), (4,2). Again, I'll illustrate with a simpler case: Draw a parallelogram with vertices (0,0), (1,1), (3,1), (2,0). The area is, again, given by a determinant built from the components of the vectors [1,1] and [2,0]. | 1 1 | | 2 0 | If you shear the parallelogram top leftwards, you can create a rectangle with the same area, with vertices at (0,0), (0,1), (2,1), (2,0). Notice that this is not a rectangle with any vertex at (1,1). The treatment of "Polygon Area" on pp. 430-432 assumes this section is correct, and so also needs a rewrite. {277} I think the display of ASCII representation on the graph: print "g = $g\n"; a-b,b-c,c-d,d-e should be: print "g = $g\n"; a-c,b-c,c-d {277} 2nd paragrah; I also agree with another readers' comment that the ASCII representation for Figure 8-4 should be: g = a-c,b-c,c-d There's no edge defined between vertices a and b. The inclusion of a-b in the list of edges is incorrect. [290] Figure 8-24; Found this in the August 1999 edition of the book: The {Pred} part in the Perl representation of Graph G is possibly incorrect, IMHO. It should be: $G->{Pred}->{c} = ['a'] and $G->{Pred}->{d} = ['c', 'c', 'b'] instead of $G->{Pred}->{c} = ['a', 'b'] and $G->{Pred}->{d} = ['c', 'c'] {299} Final paragraph; The "" operator is not overloaded in the base class. Delete from the word "all" to the end of the line in the first sentence of the final paragraph. The complete first sentence of the final paragraph should read: We overload the "" operator in the two derived classes, Graph::Directed and Graph::Undirected. (311) Paragraph 3: Now reads: "at most two of the vertex degrees If there are...", Should read: "at most two of the vertices have odd degrees. If there are..." {315} Figure 8-17; error in the First Edition, Aug 1999 Since there is no node 'f' in the picture, there should be no node 'f' mentioned in the annotations. (321) sample code articulation_points; Code fragment below is in Error. The line: my $v = $T[ -$i ]; is wrong, should be: my $v = $S[ -$i ]; I tested and works fine. ------------------------------------ my $articulate = sub { my ( $u, $T ) = @_; my $ap = $T->{ vertex_found }->{ $u }; my @S = @{ $T->{ active_list } }; # Current stack. $T->{ articulation_point }->{ $u } = $ap unless exists $T->{ articulation_point }->{ $u }; # Walk back the stack marking the active DFS branch # (below $u) as belonging to the articulation point $ap. for ( my $i = 1; $i < @S; $i++ ) { my $v = $T[ -$i ]; last if $v eq $u; $T->{ articulation_point }->{ $v } = $ap if not exists $T->{ articulation_point }->{ $v } or $ap < $T->{ articulation_point }->{ $v }; } }; (326-329) Reading through the section on Kruskal's Algorithm (also known as the Greedy Algoithm), I was perusing the pseudo-code for it, page 326. The worked example, on page 328, does not appear to be in error, but the pseudo-code is, somewhat. Assuming that I have the following graph: A-B 2 A-B 4 A-C 2 B-C 2 Using the pseudo-code, I take an edge in my graph, G, that would not create a cycle - I choose A-B with a weight of 4, which is perfectly legitimate by the pseudo-code, which specifies nothing else. As my next edge, I choose A-C with a weight of 2. Both of the other edges create cycles, so I finish. I now have my Minimum Spanning Tree with a total weight of 6, which is clearly wrong, since A-C 2 and B-C 2 (as one option) create an MST with a weight of 4. The pseudo-code needs to be altered to reflect the fact that, when passing through the list of edges, they need to be taken in weight order, starting with the least and working upwards, with any edges of equal weight having an arbitrary order (e.g. if I were to be listing my edges above in weight order, it would be inconsequential whether I listed B-C 2 before A-C 2). As I say, the worked example on 328 reflects this perfectly, and the code MST_Kruskal on page 329 contains the section "Sort by weights", so hopefully no-one has been confused, but I thought I should submit this for completeness sake, given that some of those reading the section may never have encountered Kruskal's algorithm in a non-computer based setting and so may be confused by the disparity. I hope this is of use. {344} After the comment # Do the O(N**3) loop: I think there is a significant optimization to be had in Graph::Base::TransitiveClosure_Floyd_Warshall. The speedup is sufficient (hours become minutes, in my case), that it could be construed as an error. Consider the central loop: # Do the O(N**3) loop. for ( my $k = 0; $k < @V; $k++ ) { my @nC = ( '' ) x @V; # new @C for ( my $i = 0; $i < @V; $i++ ) { for ( my $j = 0; $j < @V; $j++ ) { vec( $nC[ $i ], $j, 1 ) = vec( $C[ $i ], $j, 1 ) | vec( $C[ $i ], $k, 1 ) & vec( $C[ $k ], $j, 1 ); } } @C = @nC; # Update the closure. } I believe that 1) The adjacency matrix C can be modified in-place, and more significantly: 2) The the vec( $C[ $], $k, 1) test can be broken out of the innermost loop. This can be much faster. The modified code would look something like: # Do the O(N**3) loop. for ( my $k = 0; $k < @V; $k++ ) { for ( my $i = 0; $i < @V; $i++ ) { next unless vec( $C[ $i ], $k, 1 ); for ( my $j = 0; $j < @V; $j++ ) { vec( $C[ $i ], $j, 1 ) |= vec( $C[ $k ], $j, 1 ); } } } {369} The authors appear to claim that the unpack('%32C*', $P) construct is equivalent to Rabin-Karp. In fact, the Perl construct only sums the ascii values of each character in the string and is thus much weaker than RK. {372} 2nd section of code ... knuth_morris_pratt; Should my $m = knuth_morris_pratt_next( $P ); my ($n, $i, $j) = (length($T), 0, 0); my @next; be my ($m, @next) = knuth_morris_pratt_next( $P ); my ($n, $i, $j) = (length($T), 0, 0); (389) This isn't so much errata as just out of date. Text::Metaphone is listed as "still experimental". It has been out of alpha and stable since January. If you would remove the note about it being experimental I'd appreciate it, thanks. (389) In the code example for using Text::Metaphone, the function is Metaphone() with a capital M. $metaphone_a = Metaphone $a; $metaphone_b = Metaphone $b; (389) Now reads: Soundex trades precision for space/time simplicity Shouldn't it be: Soundex trades precision for space/time/simplicity [399] first paragraph: the source of p. 403: - does not give most error messages. - does not work with the example query of p.399 abc and not (def or ghi) - when you type a non-gramatical string or simply a query with (), it hungs up and does not process any other string from . {406} 3/5 down the page, definition of @token: The token INTEGER will not match 0. Perhaps this is an old example supplied with the module, or perhaps it could be renamed to POSITIVE_INTEGER (or NATURAL) to match its function. {415} second example; #!/usr/bin/perl -pi.bak s/(\x10)(.)(.)/ $2 x (ord($3) || 256) /eg; should be: #!/usr/bin/perl -pi.bak s/(\x10)(.)(.)/ $2 x (ord($3) || 256) /seg; {419} next to last line of code: The code snippet: $a->{value} <=> $b->{value} gives a warning (non-numeric in ncmp) because the value is sometimes a string, not a single character. [This is online in the file ch9/huffman.] Also, in the online example, in ch9/huffman-example, sub get_bit is defined as: sub get_bit { return $bit{shift @bits}; } but this generates a "Use of unitialized value" warning when it runs off the end of @bits. Instead, something like this is warranted: sub get_bit { defined( @bits ) ? return $bit{shift @bits} : undef; } {420} top picture; The picture erroneously has an "a" in the left child of the root node where it should apparently be an "f". [421] 1st paragraph; If my memory serves, gzip/pkzip and compress are two entirely different algorithms. The former is based on the 1977 paper. The latter is based on their 1978 paper and Welch's 1984 paper. I don't recall if someone proved that the two algorithms are equivalent; but at the very least, Welch's 1984 paper isn't revising the 1977 paper -- they're different algorithms. [432] Paragraph 5 from bottom. Area of example pentagon.; The area of example pentagon formed by points (0,1), (1,0), (3,2), (2,3) and (0.2) is 5 and not 2 as stated. It is quite easy to verify that. The pentagon given is enclosed in a square formed by (0,0), (3,0), (3,3) and (0,3) whose area is, of course, 9. In this square, apart from the pentagon, there are 4 right angled trianlges given by (a) (0,0),(1,0) and (0,1) (b) (1,0), (3,0) and (3,2) (c) (3,2), (3,3) and (2,3) (d) (2,3), 0,3) and ((0,2). Now the areas of these triangles are 0.5, 2, 0.5 and 1 sq. units respectively. So the area of the pentagon is 9 - (0.5+2+0.5+1) = 5. Also, there is a better algorithm for finding the area of a polygon. Stated simply, it is as follows: 1. List the points in order (clockwise or anti-clockwise)and append the first point to the end of the list. 2. Against each point tabulate product of y value against the x value of the previous point. 3. Against each point tabulate the product of x value against the y value of the previous point. 4. Sum the tabulations and take half of the diffrence of these sums, which is the area of the polygon. Example: Polygon (0,1), (1,0), (3,2), (2,3) and (0,2). Working: x y 0 1 1 1 0 0 0 3 2 2 0 2 3 9 4 0 2 4 0 0 1 0 ------------------ 5 15 The area of the polygon is abs(15 -5)/2 = 5. This works all polygons, whether convex or concave. This algorithm is quite easy to implement also! [455] Code Block Near: "# Backtrack: while there are right turns or no turns, shrink the hull."; while ( clockwise( $x[ $h[ $#h - 1 ] ], $y[ $h[ $#h - 1 ] ], $x[ $h[ $#h ] ], $y[ $h[ $#h ] ], $x[ $i ], $y[ $i ] ) < epsilon Should read: while ( clockwise( $x[ $h[ $#h - 1 ] ], $y[ $h[ $#h - 1 ] ], $x[ $h[ $#h ] ], $y[ $h[ $#h ] ], $x[ $i ], $y[ $i ] ) > epsilon Note greater than/less than symbols clockwise returns negative for left and positive for right, thus if the code reads "less than epsilon", and epsilon is an non negative value, the code will pop elements of the hull. [455] Code block near: "# Interlace x's and y's of the hull back into one list, and return"i; return map { ( $x[ $_ ], $y[ $_ ] ) } 0 .. $#h; Should read: return map { ( $x[ $_ ], $y[ $_ ] ) } @h; In the first case the wrong values are used, namely all elements of @x and @y up to $#h. The correct version uses the values contained in @h as the indices into @x and @y as desired. {461} 9th line from bottom: The if() statement lacks a check for the "equality" case, so closest_points (1, 2, 2, 5, 3, 1, 2, 1 ) hangs the algorithm. {465} GD Section; GD no longer works with GIF files. PNG is the new format. {465} within GD subsection, second last line of code example: print GIF $gif; should read: print GIF $gif->gif; Otherwise, the file will not correctly as a GIF. It will just write as ascii with jibberish. [473] both blocks of code ; SUMMARY Use of epsilon in comparing two floating point numbers for (approximate) equality is incorrect in both code blocks and for the same reason each time. Also there is textual error. This refers to the August 1999 first edition of the book. DETAILS The authors write if (abs($value1 - $value2) < epsilon) { # They match ... However the correct code is if (abs($value1 - $value2) < abs($value1 * epsilon)) { It is incorrect to compare a raw difference to epsilon. Rather one must compare a scaled difference (ie., divide the difference by $value1 or $value2). That's what my correction accomplishes (and does so in a way that is not subject to divide by zero errors). Technically, epsilon is the smallest distinguishable difference relative to one (not zero, as the claimed in the book -- middle of p. 473 -- this is another error to correct). Thus for comparison to epsilon, one needs to use differences normalized or scaled to one. For more about epsilon, see Numerical Recipes in C (2nd ed.). Also, see Bruce Bush's The Perils of Floating Point HTML page www.lahey.com/float.htm [474] 2nd code block, the floor function: According to the definition in my ancient K&R: "largest integer not greater than x, as a double" The sub provided rounds UP for negative numbers. This is not a direct replacement for the POSIX floor/ceil functions as implied in the text. {482} There's a program snippit for calculating how many of the bits in your computer are ones. Other than the obvious "you need to be root to do this", on some hardware architechtures this is a *really bad idea* that will cause you pain. I once spent several hours pounding my head into a wall with an hp300, where the machine would crash when trying to use its serial card, but only if the X server had been started. I had to resort to my guru to figure this one out, and he had to scratch his head for a minute before he figured it out. Turns out the hp300 maps device memory into the first 16 mb of /dev/mem, and the serial card in question uses activate on read registers. X's magic cookie auth generates its random number seed by checksumming /dev/mem. Reading from /dev/mem caused the card to change state without the OS knowing about it. Oops. [483] sub base { }; print base(17, 9, 2) ,"\n"; print base(10000,2,9) ,"\n\n"; print base("fff", 16, 3) ,"\n"; print base(12121200,3,16),"\n\n"; print base("g", 17, 10) ,"\n"; print base(16 , 10, 17) ,"\n\n"; print base(121,10,15),"\n"; print base(132,10,15),"\n\n"; ====> get result as below 10000 17 12121200 151515 16 16 81 812 # May change as below # Convert the number form base 10 to $outbase. # logbase() is defined below. for($i=int(logbase($realnum, $outbase)); $i>= 0; $i--) { $digit = int($realnum / ($outbase ** $i)); $realnum -= $digit * ($outbase ** $i); if($digit >=10) { $digit=$digit+87; $digit=chr($digit); } #if(ord($digit) >57 ) # { # $digit = ord($digit +49) ; # } $output .=$digit; } # To Get the result as below: 10000 17 12121200 fff 16 g 81 8c {518} exp_fast subroutine code: In the code for the non-recursive exponential subroutine "exp_fast" there is a block that tests whether or not the variable representing the power is odd. At the end of this conditional block the variable ($j) is decremented, which would cause it to become an even number. Presumably this is done so that the division by 2 which immediately follows does not coerce $j into a floating-point number. This would be Perl's normal behavior, however, since the entire subroutine falls under the scope of a "use integer" pragma there is no danger of integer-to-float conversion. Consequently, the $j-- is not needed. {523} sub collatz routine: $seen{$n} should be 'my'-ed in the subroutine. (541) bottom, subroutine rot13; Instead of: $val = tr/a-zA-Z/n-za-mN-ZA-M/; should be: $val =~ tr/a-zA-Z/n-za-mN-ZA-M/; [557] code block at top of page; The extract subroutine builds up the extracted message in the lexical variable $message, but doesn't return it. This line should be inserted at the end of the extract subroutine: return $message; {584} smartie_weight = 0; @smartie_order = sort { $dist->{$a} <=> $dist->{$b} } keys %smartie_weights; for (@smartie_order) { $smartie_weight += $smartie_weight{$_} } As it is right now, @smartie_order would contain nothing since $dist is a part of the subroutine rand_dist_weighted. It should read: @smartie_order = sort { $smartie_weights{$a} <=> $smartie_weights{$b} } keys %smartie_weights; {584} sub rand_dist_weighted() code at top of page; While not an error, the linear search in this routine can be made a tad faster by sorting the large weights first: $key_order = [ sort { $dist->{$b} <=> $dist->{$a} } keys %$dist ] Likewise for the "@smartie_order =" line at the bottom of the page. {585} sub binomial, first two return statements: replace with return(0) if ((($p == 0) && ($k != 0)) || (($p == 1) && ($k != $n))); return(1) if ((($p == 1) && ($k == $n)) || (($p == 0) && ($k == 0))); or something equivalent but more beautiful. At present some cases $p==1, $k!=$n gets return1 rather than 0 $p==1, $k==$n gets undefined rather than 1 $p==0, $k!=$n gets undefined rather than 0 tested on Perl 5.005_03 built for i686-linux {592} 2nd to last paragraph: The authors egregiously misinterpret the probability density (mass) function for continuous distributions. To compute probabilities from a density function, it is necessary to integrate the function over some range, not simply to evaluate the density at a point. As an example of the sort of nonsense that can result from the authors' error, if we reduce the variance in the example on page 592 to 0.1 and compute gaussian(0, 0, 0.1), we arrive at the conclusion that 126% of leaflets will hit the target. Let r be the sum of the radii of the target and a leaflet; the correct answer is the integral of the Gaussian(0, 0.1) density from -r to +r. [596] Hypergeometric DIstributions ; my $val = hypergeometric(3,5,53,48); print "Hyper = $val\n"; # will give: 0.00393074501208321 Proposed changes to: sub hypergeometric { my ($x,$k,$m,$n) = @_; return unless $m>0 && $m == int($m) && $n > 0 && $n == int($n) && $k > 0 && $k <= $m+$n; return 0 unless $x <= $k && $x == int($x); return choose($k,$x) * choose($n, $k-$x)/ choose($m,$k); # changes here } Such that my $val = hypergeometric(3,5,53,48); print "Hyper = $val\n"; # will give: 0.00393074501208321 The proposed changes follows the example from this link: http://www.lottery.state.mn.us/hypergeo.html (600) First full paragraph: The Statistics::Descriptive module is by Colin Kuskie and Jason Kastner, not Lastner. Neither Colin, Jason, nor S::D show up in the index. {603} The line: my @array = sort @$arrayref; Should be written: my @array = sort { $a <=> $b } @$arrayref; Otherwise the array is not numerically sorted and the answer is wrong. (603) Reads: If there are two or more equally common elements, there are two options: declare that there is no mode (that is, return *undef*), or return the median of the modes. The following subroutine does the latter." Neither of these options is mathematically correct, though the latter hints at the correct answer, in that you return the median of the 'modes'. In a sequence such as 1, 2, 2, 3, 3, 4, the mode is in actual fact both 2 and 3, the distribution being a 'multi-modal' distribution. I appreciate that in this instance, returning both 2 and 3 would probably require a heavy alteration of the subroutine. As it stands, it currently returns a scalar, whereas in this instance it would probably have to return [2, 3] (i.e. a reference to an anonymous array, effectively, since any private variables will have passed out of scope), which would be more unwieldy than returning the median. This would mean that either single modes would have to be passed back in referenced arrays or as scalars, with the programmer than differentiating between a reference and a numeric scalar themself, both of which are somewhat more difficult than the current option. However, this *would* be the correct answer, since the mode is *all* of the most common values, and not just the median thereof. I hope this is of some use, even if it means a far more unwieldy algorithm being used, or presented as an alternative use. {605} The "equivalent formulation of the standard deviation" at the bottom of the page is mathematically, but _NOT_ numerically equivalent to the formula on p. 604. In fact, an understanding of the differences between these two formulae is a common screening question for hiring scientific programmers. Since many readers may try to implement the second formula, it would be a service if the text provided a warning that the two methods can give different results. {611} 5th line of the 1st code block: $confidence += binomial ($trials, $hits, $probability) should read $confidence += binomial ($trials, $_, $probability) {613} formula; The formula given for computation of the z statistic is: (mean of data - expected mean)/standard deviation. It seems to me that it should be: (mean of data - expected mean)/(standard deviation / sqrt (sample size)). (The code presented in the middle of the page is right, since it uses the good formula). {614} bottom (after the t formula); It is written that "the estimate of the standard error of the mean is the square root of the estimate of the population variance". It seems to me that this is false. The estimate of the standard error of the mean is the square root of the estimate of the population variance, divided by the square root of the sample size. [622] 2nd paragraph; The mistake of In the correlation sub, the following line is present: sqrt(((@$array1ref * $sum1_squared) - ($sum1 ** 2)) * ((@$array1ref * $sum2_squared) - ($sum2 ** 2))); The second mention of @$array1ref should be @$array2ref. is mentioned in the errata section, but is not corrected in the downloadable sample code. I downloaded the code and spent quite a buit of time trying to troubleshoot the problem in my code when actually it was a problem in your sample code. Could you please make this change in the downloadable examples so that others do not have the same problem that I did. [630] third row of matrix: On page 629 appears the following equation: h(x, y, z) = xyz On page 630, the partial derivatives of this equation are listed as (x - 4)(y + 2), (x - 4)(z + 7), and (x - 4)(y + 2). These partial derivatives should be listed as yz, xz, and xy, respectively. (631) upper third of page: An equation has a missing term. @jacobian = jacobian( [\&f, \&g], [3, 4, -2] ); should be modified to read as follows: @jacobian = jacobian( [\&f, \&g, \&h], [3, 4, -2] ); {635} 1st code block: $b/abs($b) will fail if $b is 0. As a replacement, $b <=> 0 will not fail and is about 20% faster on my machine. It might need an explanation in the prose, however. The code on p. 637 includes $sgn = $b/abs($b) if $b, which doesn't fail but could also be sped up. {646} The 5th line from the comment "# Decompositon phase ..." in the code: $coeffs[$i] = ($delta - 1) / @points; should read $coeffs[$i] = ($delta - 1) / $temp; Accordingly, the example on the next page 647 should be: Spline coefficients: 0 -8.8 11.2 0 [-1.00, 1.00] [-0.50, 2.05] [0.00, 2.00] [0.50, 0.35] [1.00, -1.00] [1.50, -0.20] [2.00, 2.00] [2.50, 4.20] [3.00, 5.00] The error is so minor that the difference is invisible in Figure 16-3, however. {647} On line 2, the code reads: my $coeffs = spline_generate @points; It should read: my $coeffs = spline_generate(@points);