Algol 68 number formatting
last modified November 14, 2026
Algol 68 has no separate formatting library: the prelude returns text for a
number with one of three procedures, and the program decides how wide the
field should be. whole formats an integer, fixed
formats a real with a fixed number of decimals, and float
formats a real in a field whose mantissa and exponent share the available
space.
This article uses those procedures to align a table, then converts integers
to other bases, writes Roman numerals, and groups digits with commas. Every
procedure returns a STRING, so a formatted number can be stored,
concatenated, or written like any other text.
Integers with whole
whole(n, w) returns the integer n in a field of
w characters. A positive width reserves one column for the sign
and prints a plus for a positive number, a negative width left-aligns the
digits and drops the plus, and a width of zero gives the digits with no
padding at all.
BEGIN
print(("[", whole(42, 6), "]", newline));
print(("[", whole(-42, 6), "]", newline));
print(("[", whole(42, -6), "]", newline));
print(("[", whole(42, 1), "]", newline));
print(("[", whole(0, 3), "]", newline));
print(("[", whole(1234567, 0), "]", newline))
END
The brackets show the exact field: whole(42, 6) produces three
spaces, a plus, and the two digits. A negative width moves the digits to the
left edge of the field, which is what a left-aligned column needs. When the
number and its sign need more columns than the width allows, the result is a
row of asterisks, one for each requested column, and not an error.
$ a68g widths.a68 [ +42] [ -42] [ 42] [*] [ +0] [1234567]
Reals with fixed and float
fixed(r, w, d) writes a real with d digits after
the decimal point in a field of w characters. The value is
rounded, and a width of zero returns the number without padding and without a
sign, which is convenient when the text is embedded in a sentence.
float(r, w, d, e) writes a real in a field of w
characters with d decimals and an exponent field of
e characters. The point floats to fill the field: a wider field
extends the mantissa and lowers the exponent, so the same value can be written
with different powers of ten.
BEGIN
print(("[", fixed(3.14159, 8, 3), "]", newline));
print(("[", fixed(3.14159, 0, 2), "]", newline));
print(("[", fixed(2.5, 6, 0), "]", newline));
print(("[", float(1234.5678, 12, 2, 3), "]", newline));
print(("[", float(0.000123, 12, 4, 3), "]", newline))
END
The first call rounds 3.14159 to three decimals inside an eight-character
field, and the second leaves the width to the number itself. The third call
shows that rounding at zero decimals gives 3, so fixed is not a
truncation. In the last two calls the last parameter is the width of the
exponent, and the exponent is chosen so that the mantissa occupies the rest of
the field: 0.000123 is the same number as 12.3000e-5.
$ a68g reals.a68 [ +3.142] [3.14] [ +3] [+1234.57e +0] [+12.3000e -5]
Another base
A base conversion reads the number from the right: the last digit is the remainder of a division by the base, the next digit is the remainder of the quotient, and the loop stops when the quotient reaches zero. Prepending each digit builds the result in the correct order.
PROC to base = (INT n, INT b) STRING:
(
STRING digits = "0123456789abcdef";
IF n = 0 THEN
"0"
ELSE
STRING s := "";
INT m := n;
WHILE m > 0 DO
s := digits[m MOD b + 1] + s;
m := m OVER b
OD;
s
FI
);
BEGIN
print(("255 in base 2: ", to base(255, 2), newline));
print(("255 in base 16: ", to base(255, 16), newline));
print(("1000 in base 8: ", to base(1000, 8), newline));
print(("0 in base 2: ", to base(0, 2), newline))
END
The digit string works for any base up to 16, and its lower-case letters keep
the hexadecimal output readable next to the upper-case keywords of the
language. The subscript m MOD b + 1 maps a remainder of 0 to the
first character. Zero is a special case, because the loop would otherwise
produce an empty string. The procedure assumes a non-negative number; a sign
would have to be handled before the conversion.
$ a68g base.a68 255 in base 2: 11111111 255 in base 16: ff 1000 in base 8: 1750 0 in base 2: 0
Roman numerals
Roman numerals are written by subtracting the largest symbol that still fits,
again and again, until nothing remains. The subtractive pairs such as
CM for 900 are entries of their own, so the loop never has to
special-case a digit.
PROC roman = (INT n) STRING:
(
[]INT vals = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1);
[]STRING syms = ("M", "CM", "D", "CD", "C", "XC", "L", "XL",
"X", "IX", "V", "IV", "I");
STRING r := "";
INT m := n;
FOR i TO UPB vals DO
WHILE m >= vals[i] DO
r +:= syms[i];
m -:= vals[i]
OD
OD;
r
);
BEGIN
[1:6] INT ns := (1, 4, 9, 14, 40, 1994);
FOR i TO UPB ns DO
print((whole(ns[i], 5), " = ", roman(ns[i]), newline))
OD
END
The two rows are parallel: vals[i] is the value of
syms[i]. The outer loop visits the values from largest to
smallest, and the inner WHILE repeats a symbol as many times as
it fits. Because the pairs 900, 400, 90, 40, 9, and 4 come before the smaller
symbols, 1994 is built as 1000, 900, 90, and 4.
$ a68g roman.a68 +1 = I +4 = IV +9 = IX +14 = XIV +40 = XL +1994 = MCMXCIV
Aligned columns
A table is a row of structures whose fields are printed with fixed widths.
The names need padding on the right, the numbers use whole with a
positive width, and the prices use fixed with the same number of
decimals. The header uses the same widths, so every column lines up.
MODE ITEM = STRUCT(STRING name, INT qty, REAL price);
PROC pad right = (STRING s, INT w) STRING:
(
STRING r := s;
FOR i TO w - UPB s DO r +:= " " OD;
r
);
PROC pad left = (STRING s, INT w) STRING:
(
STRING r := "";
FOR i TO w - UPB s DO r +:= " " OD;
r + s
);
BEGIN
[]ITEM items = (
("bolts", 25, 0.35),
("nuts", 100, 0.12),
("washers", 8, 1.25));
print((pad right("item", 10), pad left("qty", 6),
pad left("price", 9), pad left("total", 10), newline));
FOR i TO UPB items DO
ITEM it = items[i];
REAL total = REAL(qty OF it) * price OF it;
print((pad right(name OF it, 10),
whole(qty OF it, 6),
fixed(price OF it, 9, 2),
fixed(total, 10, 2), newline))
OD
END
pad right appends spaces until the string has the requested
width, and pad left prepends them. The header needs the second
one because a positive width in whole includes the sign column,
so whole(25, 6) and the word qty are aligned only
when the word is pushed to the right as well. The total is computed from the
quantity and the unit price with a widening of the integer quantity to
REAL.
$ a68g table.a68 item qty price total bolts +25 +0.35 +8.75 nuts +100 +0.12 +12.00 washers +8 +1.25 +10.00
Grouping digits
A number written for people often has a separator every three digits. The
grouping loop walks the digits from the right, copies each one to the front of
the result, and inserts a comma after every third digit. A descending loop is
written with BY -1 and the lower bound as its TO
part.
PROC group = (INT n) STRING:
(
STRING s = whole(n, 0);
STRING r := "";
INT count := 0;
FOR i FROM UPB s BY -1 TO 1 DO
r := s[i] + r;
count +:= 1;
IF count MOD 3 = 0 AND i > 1 THEN r := "," + r FI
OD;
r
);
BEGIN
[1:4] INT ns := (1234567, 1000, 999, 0);
FOR i TO UPB ns DO print((group(ns[i]), newline)) OD
END
The test i > 1 stops a comma from being placed before the
first digit, so 1000 becomes 1,000 and not ,1,000.
The counter starts at zero on each call, and the descending subscript never
leaves the row because the loop ends at 1.
$ a68g group.a68 1,234,567 1,000 999 0
Next steps
printf and putf describe the same layout with format
denotations, which is the subject of the
formatted transput article. Values
too large for an INT need the long modes of the
big integers article, and
strings explains the concatenation that the
procedures here use to build their results.
Source
Revised Report on the Algorithmic Language Algol 68
In this article we have formatted integers and reals in fixed fields, converted integers to other bases, written Roman numerals, aligned a table, and grouped digits with commas.
Author
List all Algol 68 tutorials.