AlgorithmsIT
DivergentKnuth-Morris-Pratt transcribed from the textbook, with a one-based array class so the code reads like the pseudocode.
- Version
0.0.4zef:tbrowder- Depends
none beyond the core- License
- Artistic-2.0
- Its own test suite
- 6 files, green
- Checked
- 2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Install it #
$ rakupp install AlgorithmsITzef install AlgorithmsIT writes the same store; either installer leaves the module usable by both engines.
What it is for #
Introduction to Algorithms indexes its arrays from 1. Transcribing its pseudocode into a 0-based language means re-deriving every index, and that is where transcription bugs come from. This distribution supplies a one-based array class so the code can be copied as written — and, as its first worked example, the Knuth-Morris-Pratt string matcher from chapter 32.
Matching #
use AlgorithmsIT :p1005, :p1006;
use AlgorithmsIT::Classes;
sub matches(Str $text, Str $pattern) {
KMP-Matcher(ArrayOneBased.new($text), ArrayOneBased.new($pattern))
}
for <abababacaba ababaca>, <aaaaa aa>, <abcabcabc abc>,
<abc zzz>, <aaa aaa> -> ($t, $p) {
my @brute = (0 .. $t.chars - $p.chars).grep({ $t.substr($_, $p.chars) eq $p });
say sprintf(' T=%-12s P=%-8s shifts %-14s brute %-14s %s',
$t, $p, matches($t, $p).raku, @brute.raku,
matches($t, $p).List eqv @brute.List ?? 'agree' !! 'DIFFER');
}
say '';
say 'the shifts are zero-based offsets into the text.'; T=abababacaba P=ababaca shifts [2] brute [2] agree
T=aaaaa P=aa shifts [0, 1, 2, 3] brute [0, 1, 2, 3] agree
T=abcabcabc P=abc shifts [0, 3, 6] brute [0, 3, 6] agree
T=abc P=zzz shifts [] brute [] agree
T=aaa P=aaa shifts [0] brute [0] agree
the shifts are zero-based offsets into the text.use AlgorithmsIT :p1005, :p1006;
use AlgorithmsIT::Classes;
my $pi = Compute-Prefix-Function(ArrayOneBased.new('ababaca'));
say 'Compute-Prefix-Function("ababaca") : ', $pi.gist;
say 'the textbook pi for ababaca : [ 0, 0, 1, 2, 3, 0, 1 ]';
say '';
say 'the two import tags are the book`s PAGE NUMBERS:';
say ' :p1005 gives KMP-Matcher';
say ' :p1006 gives Compute-Prefix-Function';
say 'a plain `use AlgorithmsIT` imports NEITHER.';Compute-Prefix-Function("ababaca") : [ 0, 0, 1, 2, 3, 0, 1 ]
the textbook pi for ababaca : [ 0, 0, 1, 2, 3, 0, 1 ]
the two import tags are the book`s PAGE NUMBERS:
:p1005 gives KMP-Matcher
:p1006 gives Compute-Prefix-Function
a plain `use AlgorithmsIT` imports NEITHER.ArrayOneBased #
use AlgorithmsIT::Classes;
my $a = ArrayOneBased.new('abc');
say 'new("abc") : ', $a.gist;
say ' .elems : ', $a.elems, ' .length : ', $a.length;
say ' [1] : ', $a[1].raku, ' <- ONE-based';
say ' [0] : ', $a[0].raku, ' <- the sentinel';
say ' [4] : ', $a[4].raku, ' <- past the end, does not grow';
say ' .arr : ', $a.arr.raku;
say '';
say 'three constructors:';
say ' new(5) : ', ArrayOneBased.new(5).gist;
say ' new(3, 7) : ', ArrayOneBased.new(3, 7).gist;
say ' new((10,20)) : ', ArrayOneBased.new((10, 20)).gist;
say '';
my $r = try ArrayOneBased.new(1);
say ' new(1) : ', $! ?? 'refused — end must exceed start' !! $r.gist;
say ' use new((1,)) for a one-element array.';
say '';
say 'A1B is the same type : ', (A1B === ArrayOneBased);new("abc") : [ a, b, c ]
.elems : 3 .length : 3
[1] : "a" <- ONE-based
[0] : -1 <- the sentinel
[4] : Any <- past the end, does not grow
.arr : [-1, "a", "b", "c"]
three constructors:
new(5) : [ 1, 2, 3, 4, 5 ]
new(3, 7) : [ 3, 4, 5, 6, 7 ]
new((10,20)) : [ 10, 20 ]
new(1) : refused — end must exceed start
use new((1,)) for a one-element array.
A1B is the same type : TrueThe one thing to know #
ArrayOneBased composes Iterable but never defines iterator, so every list operation on it silently produces nothing.
use AlgorithmsIT::Classes;
my $a = ArrayOneBased.new('abc');
say 'it claims to do Iterable : ', ($a ~~ Iterable);
say '';
say 'so a guard that checks for iterability passes — and then';
say '`for $a { … }` quietly does nothing at all on Rakudo: no exception,';
say 'no warning, no elements. On Raku++ it runs once with $_ bound to the';
say 'object itself. Neither is what you meant.';
say '';
say 'the two working idioms:';
say ' positional slice : ', $a[1 .. $a.elems].raku;
say ' the public .arr : ', $a.arr[1 .. *].raku, ' (index 0 is the -1 sentinel)';
say '';
say '.list gives you an undefined List type object on both engines,';
say 'and .map dies on Rakudo. Do not reach for either.';it claims to do Iterable : True
so a guard that checks for iterability passes — and then
`for $a { … }` quietly does nothing at all on Rakudo: no exception,
no warning, no elements. On Raku++ it runs once with $_ bound to the
object itself. Neither is what you meant.
the two working idioms:
positional slice : ("a", "b", "c")
the public .arr : ("a", "b", "c") (index 0 is the -1 sentinel)
.list gives you an undefined List type object on both engines,
and .map dies on Rakudo. Do not reach for either.Where the two engines differ #
Exactly that for behaviour, plus one diagnostic timing: KMP-Matcher('abc', 'b') with the wrong argument types is a compile-time refusal on Rakudo and a run-time X::Method::NotFound on Raku++.
use AlgorithmsIT :p1005, :p1006;
use AlgorithmsIT::Classes;
# always wrap the arguments, and always read the result as a slice
sub kmp(Str $text, Str $pattern) {
KMP-Matcher(ArrayOneBased.new($text), ArrayOneBased.new($pattern)).List
}
say 'kmp("abababacaba", "ababaca") = ', kmp('abababacaba', 'ababaca').raku;
say '';
say 'two edges to guard against, identical on both engines:';
say ' an EMPTY pattern returns the spurious shift [1]:';
say ' ', kmp('abc', '').raku;
say ' and Compute-Prefix-Function on an empty pattern returns [0]:';
say ' ', Compute-Prefix-Function(ArrayOneBased.new('')).gist;
say '';
say 'the matcher compares with eq/ne, so elements compare as STRINGS —';
say '2.0 matches 2 and "01" does not match 1. For text that is exactly';
say 'what you want; for numeric data it is not.';kmp("abababacaba", "ababaca") = (2,)
two edges to guard against, identical on both engines:
an EMPTY pattern returns the spurious shift [1]:
(1,)
and Compute-Prefix-Function on an empty pattern returns [0]:
[ 0 ]
the matcher compares with eq/ne, so elements compare as STRINGS —
2.0 matches 2 and "01" does not match 1. For text that is exactly
what you want; for numeric data it is not.