Algol 68 string processing
last modified November 13, 2026
A string is a row of characters, so the loops and slices of the rows article apply to text as well. What changes is the unit of work: a program that counts words, cuts a line into fields, or removes spaces thinks in runs of characters rather than in single ones.
This article counts words, splits a line into a row of words, trims a line, tests for palindromes, and reads comma-separated fields.
Counting words
A word is a run of characters that are not spaces. Counting words therefore
means counting the places where such a run begins, which is a transition from a
space to a non-space. The procedure is space accepts the space,
the tab, and the two line breaks.
PROC is space = (CHAR c) BOOL:
(c = " " ORF c = REPR 9) ORF (c = REPR 10 ORF c = REPR 13);
PROC word count = (STRING s) INT:
(
INT n := 0;
BOOL inside := FALSE;
FOR i TO UPB s DO
IF is space(s[i]) THEN
inside := FALSE
ELIF NOT inside THEN
n +:= 1;
inside := TRUE
FI
OD;
n
);
BEGIN
[1:3] STRING lines := (
"the quick brown fox",
" spaced out ",
"");
FOR i TO UPB lines DO
print((whole(word count(lines[i]), 0), newline))
OD
END
inside remembers whether the previous character belonged to a
word, so a run of several spaces counts once. The loop runs over the upper
bound of the string, and an empty string has upper bound 0, so the loop body
never runs and the answer is 0 without a special case.
The three lines hold 4, 2, and 0 words. Leading, trailing, and repeated spaces do not create empty words, because only the first character of a run starts a new one.
$ a68g wordcount.a68 4 2 0
Splitting into words
Splitting turns a line into a row of words. The elements of a row must share
one mode, and words have different lengths, so the row holds references to
strings on the heap: MODE WORD = REF STRING. Each element points
at a copy of one word.
MODE WORD = REF STRING;
PROC is space = (CHAR c) BOOL:
(c = " " ORF c = REPR 9) ORF (c = REPR 10 ORF c = REPR 13);
PROC add word = (REF FLEX [] WORD ws, STRING w) VOID:
(
INT n = UPB ws;
FLEX [1:n + 1] WORD bigger;
FOR i TO n DO
bigger[i] := ws[i]
OD;
bigger[n + 1] := HEAP STRING := w;
ws := bigger
);
PROC split = (STRING s) FLEX [] WORD:
(
FLEX [1:0] WORD result;
INT i := 1;
WHILE i <= UPB s DO
IF is space(s[i]) THEN
i +:= 1
ELSE
INT start := i;
WHILE i <= UPB s ANDF NOT is space(s[i]) DO
i +:= 1
OD;
add word(result, s[start:i - 1])
FI
OD;
result
);
BEGIN
FLEX [1:0] WORD words := split(" one two three ");
print((whole(UPB words, 0), " words", newline));
FOR i TO UPB words DO
print((whole(i, -2), ": ", words[i], " (", whole(UPB words[i], 0),
" letters)", newline))
OD
END
add word grows the flexible row by allocating one that is longer,
copying the old elements, and assigning the new row to its parameter. The
parameter is REF FLEX [] WORD, a reference to a flexible row, so
the assignment reaches the caller's variable.
The inner loop uses ANDF, which stops at the first operand that is
false. The standard AND evaluates both operands and would read past
the end of the string; ANDF and ORF are extensions of
Algol 68 Genie, and plain AND and OR remain the
operators of the language.
The result holds three words, and each one is a heap string whose length is
read with UPB. The spaces at the beginning, between, and after the
words are gone, because a word starts only after a run of spaces ends.
$ a68g split.a68 3 words 1: one (3 letters) 2: two (3 letters) 3: three (5 letters)
Trimming a line
Trimming removes spaces from both ends of a line and leaves the middle alone. Two indices move inwards: one from the first character while it is a space, and one from the last. The remaining part is a slice, which copies the characters into a new string.
PROC is space = (CHAR c) BOOL:
(c = " " ORF c = REPR 9) ORF (c = REPR 10 ORF c = REPR 13);
PROC trim = (STRING s) STRING:
(
INT first := 1;
INT last := UPB s;
WHILE first <= last ANDF is space(s[first]) DO
first +:= 1
OD;
WHILE last >= first ANDF is space(s[last]) DO
last -:= 1
OD;
IF first > last THEN
""
ELSE
s[first:last]
FI
);
BEGIN
[1:3] STRING lines := (
" padded ",
REPR 9 + "tabbed",
" ");
FOR i TO UPB lines DO
STRING t = trim(lines[i]);
print(("[", t, "] ", whole(UPB t, 0), newline))
OD
END
The two loops test the bounds before they read a character, because a line of nothing but spaces would otherwise walk off both ends. When the indices have crossed, the line was empty or contained only spaces, and the procedure answers the empty string.
The three lines lose their leading and trailing spaces, and the tab is removed like a space. The empty result has upper bound 0, which is the length that the last line reports.
$ a68g trim.a68 [padded] 6 [tabbed] 6 [] 0
Palindromes
A palindrome reads the same from both ends. Two indices start at the first and last characters and move towards the middle, comparing the characters they pass. A mismatch ends the test immediately.
PROC is palindrome = (STRING s) BOOL:
(
INT i := 1;
INT j := UPB s;
BOOL ok := TRUE;
WHILE ok ANDF i < j DO
IF s[i] /= s[j] THEN
ok := FALSE
FI;
i +:= 1;
j -:= 1
OD;
ok
);
BEGIN
[1:5] STRING words := (
"racecar", "level", "algol", "", "ab");
FOR i TO UPB words DO
print((words[i], " -> ", is palindrome(words[i]), newline))
OD
END
The loop condition combines the flag with the bound test using
ANDF, so a mismatch stops the loop before the indices meet. A
string of one character and the empty string are palindromes, because the loop
body never runs for them.
The first two words read the same backwards, algol does not, the
empty string is accepted, and ab fails on its first comparison.
The test is case sensitive, so a mixed-case sentence would need
toupper before the comparison.
$ a68g palindrome.a68 racecar -> T level -> T algol -> F -> T ab -> F
Comma-separated fields
A line of comma-separated values splits at every comma. The program walks the line, prints the slice between the previous comma and the current one, and remembers where the next field starts.
PROC show fields = (STRING line) VOID:
(
INT start := 1;
INT n := 0;
FOR i TO UPB line DO
IF line[i] = "," THEN
n +:= 1;
print((whole(n, -2), ": [", line[start:i - 1], "]", newline));
start := i + 1
FI
OD;
n +:= 1;
IF start <= UPB line THEN
print((whole(n, -2), ": [", line[start:UPB line], "]", newline))
ELSE
print((whole(n, -2), ": []", newline))
FI
);
BEGIN
show fields("alpha,beta,gamma");
print(newline);
show fields("one,,three,")
END
A trailing comma produces a final empty field, and the last field has no comma
after it, so it is printed after the loop. The guard start <= UPB line
covers both cases: when the line ends with a comma, the slice would begin one
character past the end, which is an error rather than an empty string.
The first line has three fields and the second has four, two of which are empty. Quoted fields that contain commas are not handled here; recognising them would need a flag that records whether the walk is inside quotes.
$ a68g csv.a68 1: [alpha] 2: [beta] 3: [gamma] 1: [one] 2: [] 3: [three] 4: []
More text tools
The procedures of this article are built from the two operations that a string
supports: a subscript that reads one character and a slice that copies a range.
Patterns need more than that, and Algol 68 Genie adds grep in string
and sub in string for searching and replacing with regular
expressions.
Numbers become text with whole, fixed, and
float, and formatted transput aligns columns of strings and
numbers in one call. A program that reads its text from a file uses the
procedures of the files article, and the
regular expressions article covers
the pattern procedures.
Next steps
The strings article explains the subscripts and slices that every procedure here uses, and flexible rows covers the growth of the row of words. Regular expressions adds pattern matching and replacement, and formatted transput aligns the results in columns.
Source
Revised Report on the Algorithmic Language Algol 68
In this article we have counted words, split a line into a row of heap strings, trimmed spaces from both ends, tested strings for palindromes, and read comma-separated fields.
Author
List all Algol 68 tutorials.