Portable conversions (Integers)
By R. S. Doiel, 2021-11-26
An area in working with Oberon-07 on a POSIX machine that has proven problematic is type conversion. In particular converting to and from INTEGER or REAL and ASCII. None of the three compilers I am exploring provide a common way of handling this. I’ve explored relying on C libraries but that approach has it’s own set of problems. I’ve become convinced a better approach is a pure Oberon-07 library that handles type conversion with a minimum of assumptions about the implementation details of the Oberon compiler or hardware. I’m calling my conversion module “Types”. The name is short and descriptive and seems an appropriate name for a module consisting of type conversion tests and transformations. My initial implementation will focusing on converting integers to and from ASCII.
INTEGER to ASCII and back again
I don’t want to rely on the representation of the INTEGER value in the compiler or at the machine level. That has lead me to think in terms of an INTEGER as a signed whole number.
The simplest case of converting to/from ASCII is the digits from zero
to nine (inclusive). Going from an INTEGER to an ASCII CHAR is just
looking up the offset of the character representing the “digit”. Like
wise going from ASCII CHAR to a INTEGER is a matter of mapping in the
reverse direction. Let’s call these procedures DigitToChar
and CharToDigit*
.
Since INTEGER can be larger than zero through nine and CHAR can hold
non-digits I’m going to add two additional procedures for validating
inputs – IsIntDigit
and IsCharDigit
. Both
return TRUE if valid, FALSE if not.
For numbers larger than one digit I can use decimal right shift to
extract the ones column value or a left shift to reverse the process.
Let’s called these IntShiftRight
and
IntShiftLeft
. For shift right it’d be good to capture the
ones column being lost. For shift left it would be good to be able to
shift in a desired digit. That way you could shift/unshift to retrieve
to extract and put back values.
A draft definition for “Types” should look something like this.
DEFINITION Types;
(* Check if an integer is a single digit, i.e. from 0 through 9 returns
TRUE, otherwise FALSE *)
PROCEDURE IsIntDigit(x : INTEGER) : BOOLEAN;
(* Check if a CHAR is "0" through "9" and return TRUE, otherwise FALSE *)
PROCEDURE IsCharDigit(ch : CHAR) : BOOLEAN;
(* Convert digit 0 through 9 into an ASCII CHAR "0" through "9",
ok is TRUE if conversion successful, FALSE otherwise *)
PROCEDURE DigitToChar(x : INTEGER; VAR ch : CHAR; VAR ok : BOOLEAN);
(* Convert a CHAR "0" through "9" into a digit 0 through 9, ok
is TRUE is conversion successful, FALSE otherwise *)
PROCEDURE CharToDigit(ch : CHAR; VAR x : INTEGER; VAR ok : BOOLEAN);
(* Shift an integer to the right (i.e. x * 0.1) set "r" to the
value shifted out (ones column lost) and return the shifted value.
E.g. x becomes 12, r becomes 3.
x := IntShiftRight(123, r);
*)
PROCEDURE IntShiftRight(x : INTEGER; VAR r : INTEGER) : INTEGER;
(* Shift an integer to the left (i.e. x * 10) adding the value y
after the shift.
E.g. x before 123
x := IntShiftRight(12, 3);
*)
PROCEDURE IntShiftLeft(x, y : INTEGER) : INTEGER;
(* INTEGER to ASCII *)
PROCEDURE Itoa(src : INTEGER; VAR value : ARRAY OF CHAR; VAR ok : BOOLEAN);
(* ASCII to INTEGER *)
PROCEDURE Atoi(src : ARRAY OF CHAR; VAR value : INTEGER; VAR ok : BOOLEAN);
END Types.
NOTE: Oberon-07 provides us the ORD and CHR built as part of the language. These are for working with the encoding and decoding values as integers. This is not the same thing as the meaning of “0” versus the value of 0. Getting to and from the encoding to the meaning of the presentation can be done with some simple arithmetic.
Putting it all together
(* DigitToChar converts an INTEGER less than to a character. E.g.
0 should return "0", 3 returns "3", 0 returns "9" *)
PROCEDURE DigitToChar*(i : INTEGER) : CHAR;
BEGIN
RETURN (CHR(ORD("0") + i))
END DigitToChar;
(* CharToDigit converts a single "Digit" character to an INTEGER value.
E.g. "0" returns 0, "3" returns 3, "9" returns 9. *)
PROCEDURE CharToDigit(ch : CHAR) : INTEGER;
BEGIN
RETURN (ORD(ch) - ORD("0"))
END CharToDigit;
This implementation is naive. It assumes the ranges of the input values was already checked. In practice this is going to encourage bugs.
In a language like Go or Python you can return multiple values (in
Python you can return a tuple). In Oberon-07 I could use a RECORD type
to do that but that feels a little too baroque. Oberon-07 like Oberon-2,
Oberon, Modula and Pascal does support “VAR” parameters. With a slight
modification to our procedure signatures I can support easy assertions
about the conversion. Let’s create two functional procedures
IsIntDigit()
and IsCharDigit()
then update our
DigitToChar()
and CharToDigit()
with an a “VAR
ok : BOOLEAN” parameter.
(* IsIntDigit returns TRUE is the integer value is zero through nine *)
PROCEDURE IsIntDigit(i : INTEGER) : BOOLEAN;
BEGIN
RETURN ((i >= 0) & (i <= 9))
END IsIntDigit;
(* IsCharDigit returns TRUE if character is zero through nine. *)
PROCEDURE IsCharDigit(ch : CHAR) : BOOLEAN;
BEGIN
RETURN ((ch >= "0") & (ch <= "9"))
END IsCharDigit;
(* DigitToChar converts an INTEGER less than to a character. E.g.
0 should return "0", 3 returns "3", 0 returns "9" *)
PROCEDURE DigitToChar*(i : INTEGER; VAR ok : BOOLEAN) : CHAR;
BEGIN
ok := IsIntDigit(i);
RETURN (CHR(ORD("0") + i))
END DigitToChar;
(* CharToDigit converts a single "Digit" character to an INTEGER value.
E.g. "0" returns 0, "3" returns 3, "9" returns 9. *)
PROCEDURE CharToDigit(ch : CHAR; VAR ok : BOOLEAN) : INTEGER;
BEGIN
ok := IsCharDigit(ch);
RETURN (ORD(ch) - ORD("0"))
END CharToDigit;
What about values are greater nine? Here we can take advantage of our
integer shift procedures. IntShiftRight
will move the
INTEGER value right reducing it’s magnitude (i.e. x * 0.1). It also
captures the ones column lost in the shift. Repeatedly calling
IntShiftRight
will let us peal off the ones columns until
the value “x” is zero. IntShiftLeft
shifts the integer to
the left meaning it raises it a magnitude (i.e. x * 10).
IntShiftLeft
also rakes a value to shift in on the right
side of the number. In this way we can shift in a zero and get
x * 10
or shift in another digit and get
(x * 10) + y
. This means you can use
IntShiftRight
and recover an IntShiftLeft
.
(* IntShiftRight converts the input integer to a real, multiplies by 0.1
and converts by to an integer. The value in the ones column is record
in the VAR parameter r. E.g. IntShiftRight(123) return 12, r is set to 3. *)
PROCEDURE IntShiftRight*(x : INTEGER; VAR r : INTEGER) : INTEGER;
VAR i : INTEGER; isNeg : BOOLEAN;
BEGIN
isNeg := (x < 0);
i := FLOOR(FLT(ABS(x)) * 0.1);
r := ABS(x) - (i * 10);
IF isNeg THEN
i := i * (-1);
END;
RETURN i
END IntShiftRight;
(* IntShiftLeft multiples input value by 10 and adds y. E.g. IntShiftLeft(123, 4) return 1234 *)
PROCEDURE IntShiftLeft*(x, y : INTEGER) : INTEGER;
VAR i : INTEGER; isNeg : BOOLEAN;
BEGIN
isNeg := (x < 0);
i := (ABS(x) * 10) + y;
IF isNeg THEN
i := i * (-1);
END;
RETURN i
END IntShiftLeft;
I have what I need for implementing Itoa
(integer to
ASCII).
(* Itoa converts an INTEGER to an ASCII string setting ok BOOLEAN to
TRUE if value ARRAY OF CHAR holds the full integer, FALSE if
value was too small to hold the integer value. *)
PROCEDURE Itoa*(x : INTEGER; VAR value : ARRAY OF CHAR; ok : BOOLEAN);
VAR i, j, k, l, minL : INTEGER; tmp : ARRAY BUFSIZE OF CHAR; isNeg : BOOLEAN;
BEGIN
i := 0; j := 0; k := 0; l := LEN(value); isNeg := (x < 0);
IF isNeg THEN
(* minimum string length for value is 3, negative sign, digit and 0X *)
minL := 3;
ELSE
(* minimum string length for value is 2, one digit and 0X *)
minL := 2;
END;
ok := (l >= minL) & (LEN(value) >= LEN(tmp));
IF ok THEN
IF IsIntDigit(ABS(x)) THEN
IF isNeg THEN
value[i] := "-"; INC(i);
END;
value[i] := DigitToChar(ABS(x), ok); INC(i); value[i] := 0X;
ELSE
x := ABS(x); (* We need to work with the absolute value of x *)
i := 0; tmp[i] := 0X;
WHILE (x >= 10) & ok DO
(* extract the ones columns *)
x := IntShiftRight(x, k); (* a holds the shifted value,
"k" holds the ones column
value shifted out. *)
(* write append k to our temp array holding values in
reverse number magnitude *)
tmp[i] := DigitToChar(k, ok); INC(i); tmp[i] := 0X;
END;
(* We now can convert the remaining "ones" column. *)
tmp[i] := DigitToChar(x, ok); INC(i); tmp[i] := 0X;
IF ok THEN
(* now reverse the order of tmp string append each
character to value *)
i := 0; j := Strings.Length(tmp) - 2;
IF isNeg THEN
value[i] := "-"; INC(i);
END;
j := Strings.Length(tmp) - 1;
WHILE (j > -1) DO
value[i]:= tmp[j];
INC(i); DEC(j);
value[i] := 0X;
END;
value[i] := 0X;
END;
END;
ELSE
ok := FALSE;
END;
END Itoa;
Integers in Oberon are signed. So I’ve chosen to capture the sign in
the isNeg
variable. This lets me work with the absolute
value for the actual conversion. One failing in this implementation is I
don’t detect an overflow. Also notice that I am accumulating the
individual column values in reverse order (lowest magnitude first). That
is what I need a temporary buffer. I can then copy the values in reverse
order into the VAR ARRAY OF CHAR. Finally I also maintain the ok BOOLEAN
to track if anything went wrong.
When moving from an ASCII representation I can simplified the code by having a local (to the module) procedure for generating magnitudes.
Going the other way I can simplify my Atoi
if I have an
local to the module “magnitude” procedure.
(* magnitude takes x and multiplies it be 10^y, If y is positive zeros
are appended to the right side (i.e. multiplied by 10). If y is
negative then the result is shifted left (i.e.. multiplied by
0.1 via IntShiftRight().). The digit(s) shift to the fractional
side of the decimal are ignored. *)
PROCEDURE magnitude(x, y : INTEGER) : INTEGER;
VAR z, w : INTEGER;
BEGIN
z := 1;
IF y >= 0 THEN
WHILE y > 0 DO
z := IntShiftLeft(z, 0);
DEC(y);
END;
ELSE
WHILE y < 0 DO
x := IntShiftRight(x, w);
INC(y);
END;
END;
RETURN (x * z)
END magnitude;
And with that I can put together my Atoi
(ASCII to
integer) procedure. I’ll need to add some sanity checks as well.
(* Atoi converts an ASCII string to a signed integer value
setting the ok BOOLEAN to TRUE on success and FALSE on error. *)
PROCEDURE Atoi*(source : ARRAY OF CHAR; VAR value : INTEGER; VAR ok : BOOLEAN);
VAR i, l, a, m: INTEGER; isNeg : BOOLEAN;
BEGIN
(* "i" is the current CHAR position we're analyzing, "l" is the
length of our string, "a" holds the accumulated value,
"m" holds the current magnitude we're working with *)
i := 0; l := Strings.Length(source);
a := 0; m := l - 1; isNeg := FALSE; ok := TRUE;
(* Validate magnitude and sign behavior *)
IF (l > 0) & (source[0] = "-") THEN
INC(i); DEC(m);
isNeg := TRUE;
ELSIF (l > 0) & (source[0] = "+") THEN
INC(i); DEC(m);
END;
(* The accumulator should always hold a positive integer, if the
sign flips we have overflow, ok should be set to FALSE *)
ok := TRUE;
WHILE (i < l) & ok DO
a := a + magnitude(CharToDigit(source[i], ok), m);
IF a < 0 THEN
ok := FALSE; (* we have an overflow condition *)
END;
DEC(m);
INC(i);
END;
IF ok THEN
IF (i = l) THEN
IF isNeg THEN
value := a * (-1);
ELSE
value := a;
END;
END;
END;
END Atoi;
Here’s an example using the procedures.
Converting an integer 1234 to an string “1234”.
x := 1234; s := ""; ok := FALSE;
Types.Itoa(x, s, ok);
IF ok THEN
Out.String(s); Out.String(" = ");
Out.Int(x,1);Out.Ln;
ELSE
Out.String("Something went wrong");Out.Ln;
END;
Converting a string “56789” to integer 56789.
x := 0; src := "56789"; ok := FALSE;
Types.Atoi(src, x, ok);
IF ok THEN
Out.Int(x,1); Out.String(" = "); Out.String(s);
Out.Ln;
ELSE
Out.String("Something went wrong");Out.Ln;
END;
References and resources
Implementations for modules for this article are linked here Types, TypesTest and Tests.
Expanded versions of the Types
module will be available
as part of Artemis Project – github.com/rsdoiel/Artemis.