All the bases that are a power of two, like the binary (21), quaternary (22), octal (23), hexadecimal (24) bases, have an integral number of bits per digit1.
Thus to retrieve each digit2 of a numeral we simply break the number intro group of n bits starting from the LSb (the right).
For example for the quaternary base, we break a 16-bit number in groups of two bits. There are 8 of such groups.
Not all power of two bases have an integral number of groups that fits 16 bits; for example, the octal base has 5 groups of 3 bits that account for 3·5 = 15 bits out of 16, leaving a partial group of 1 bit3.
The algorithm is simple, we isolate each group with a shift followed by an AND operation.
This procedure works for every size of the groups or, in other words, for any base power of two.
In order to show the digits in the right order the function start by isolating the most significant group (the leftmost), thereby it is important to know: a) how many bits D a group is and b) the bit position S where the leftmost group starts.
These values are precomputed and stored in carefully crafted constants.
The parameters must be pushed on the stack.
Each one is 16-bit wide.
They are shown in order of push.
Parameter | Description |
---|---|
N | The number to convert |
Base | The base to use expressed using the constants BASE2 , BASE4 , BASE8 and BASE16 |
Print leading zeros | If zero no non-significant zeros are print, otherwise they are. The number 0 is printed as "0" though |
push 241
push BASE16
push 0
call print_pow2 ;Prints f1
push 241
push BASE16
push 1
call print_pow2 ;Prints 00f1
push 241
push BASE2
push 0
call print_pow2 ;Prints 11110001
Note to TASM users: If you put the constants defined with EQU
after the code that uses them, enable multi-pass with the /m
flag of TASM or you'll get Forward reference needs override.
;Parameters (in order of push):
;
;number
;base (Use constants below)
;print leading zeros
print_pow2:
push bp
mov bp, sp
push ax
push bx
push cx
push dx
push si
push di
;Get parameters into the registers
;SI = Number (left) to convert
;CH = Amount of bits to shift for each digit (D)
;CL = Amount od bits to shift the number (S)
;BX = Bit mask for a digit
mov si, WORD PTR [bp+08h]
mov cx, WORD PTR [bp+06h] ;CL = D, CH = S
;Computes BX = (1 << D)-1
mov bx, 1
shl bx, cl
dec bx
xchg cl, ch ;CL = S, CH = D
_pp2_convert:
mov di, si
shr di, cl
and di, bx ;DI = Current digit
or WORD PTR [bp+04h], di ;If digit is non zero, [bp+04h] will become non zero
;If [bp+04h] was non zero, result is non zero
jnz _pp2_print ;Simply put, if the result is non zero, we must print the digit
;Here we have a non significant zero
;We should skip it BUT only if it is not the last digit (0 should be printed as "0" not
;an empty string)
test cl, cl
jnz _pp_continue
_pp2_print:
;Convert digit to digital and print it
mov dl, BYTE PTR [DIGITS + di]
mov ah, 02h
int 21h
_pp_continue:
;Remove digit from the number
sub cl, ch
jnc _pp2_convert
pop di
pop si
pop dx
pop cx
pop bx
pop ax
pop bp
ret 06h
This data must be put in the data segment, the one reached by `DS`.
DIGITS db "0123456789abcdef"
;Format for each WORD is S D where S and D are bytes (S the higher one)
;D = Bits per digit --> log2(BASE)
;S = Initial shift count --> D*[ceil(16/D)-1]
BASE2 EQU 0f01h
BASE4 EQU 0e02h
BASE8 EQU 0f03h
BASE16 EQU 0c04h
To port the code to NASM remove the PTR keyword from memory accesses (e.g. mov si, WORD PTR [bp+08h]
becomes mov si, WORD PTR [bp+08h]
)
The function can be easily extended to any base up to 2255, though each base above 216 will print the same numeral as the number is only 16 bits.
To add a base:
BASEx
where x is 2n.DIGITS
.Example: adding base 32
We have D = 5 and S = 15, so we define BASE32 EQU 0f05h
.
We then add sixteen more digits: DIGITS db "0123456789abcdefghijklmnopqrstuv"
.
As it should be clear, the digits can be changed by editing the DIGITS
string.
1 If B is a base, then it has B digits per definition. The number of bits per digit is thus log2(B). For power of two bases this simplifies to log2(2n) = n which is an integer by definition.
2 In this context it is assumed implicitly that the base under consideration is a power of two base 2n.
3 For a base B = 2n to have an integral number of bit groups it must be that n | 16 (n divides 16). Since the only factor in 16 is 2, it must be that n is itself a power of two. So B has the form 22k or equivalently log2(log2(B)) must be an integer.