Search code examples
algorithmx86-16octalnumber-systems

Converting quaternary to octal. ASM 8086


I have to prepare program for 8086 processor which converts quaternary to octal number.

My idea:

Multiplying every digit by exponents of 4 and add to register. Later check the highest exponent of 8 not higher than sum from first step. Divide by exponents of 8 until remainder equals 0. Every result of dividing is one digit in octal. But for 16-digits number last exponent of 4 is 4^15. I suppose It isn't optimal algorithm.

Is there any other way? Maybe to binary and group by 3 digits.


Solution

  • Turns out you can indeed process values 3 digits at a time. Done this way, you can process strings of arbitrary length, without being limited by the size of a register. Not sure why you might need to, unless aliens try to communicate with us using ascii strings of quaternary digits with huge lengths. Might happen.

    It's possible to do the translation either way (Right to Left or Left to Right). However, there's a bit of a challenge to either:

    If you are processing RtL, you need to know the length of the output string before you start (so that you know where to write the digits as you compute them). This is do-able, but a bit tricky. In simplest terms, the length is ((strlen(Q) + 2) / 3) * 2. That almost gets it. However, you can end up with a blank space at the beginning for a number of cases. "1" as well as "10" will give the blank space. "20" won't. The correct value can be computed, but it's annoying.

    Likewise, processing LtR has a similar problem. You don't have the problem of figuring out where to write digits, but consider: If the string to convert it "123", then the conversion is simple (33 octal). But what if you start processing, and the complete string is "1231" (155 octal)? In that case what you need to process it like "001231" (01 55). IOW, digits can be processed in groups of 3, but you need to handle the initial case where the number of digits doesn't evenly divide by 3.

    Posting solutions to homework is usually something I avoid. However I doubt you are going to turn this in as your 'solution,' and it's (barely) possible that google might send someone here who needs something similar.

    A few things to note:

    1. This code is intended to be called from C using Microsoft's fastcall (it made testing easier) and compiled with masm.
    2. While it is written in 32bit (my environment), there's nothing that particularly requires 32bit in it. Since you said you were targeting 8086, I've tried to avoid any 'advanced' instructions. Converting to 16bit or even 64bit should not present much of a challenge.
    3. It processes from left to right.
    4. As with any well-written routine, it validates its parameters. It outputs a zero length string on error, such as invalid digits in the input string.
    5. It will crash if the output buffer is NULL. I suppose I could return a bool on error (currently returns void), but, well, I didn't.
    6. I'm sure the code could be tighter (couldn't it always?), but for "homework project quality," it seems reasonable.

    Other than that, that comments should explain the code.

    .386
    .model flat
    .code
    
    ; Call from C via:
    ; extern "C" void __fastcall PrintOct(const char *pQuat, char *pOct);
    
    ; On Entry:
    ; ecx: pQuat
    ; edx: pOct
    
    ; On Exit:
    ; eax, ecx, edx clobbered
    ; all others preserved
    ; If pOct is zero bytes long, an error occurred (probably invalid digits)
    
    @PrintOct@8 PROC
    
    ; -----------------------
    ; If pOct is NULL, there's nothing we can do
        test edx, edx
        jz Failed
    
    ; -----------------------
    ; Save the registers we modify (except for
    ; eax, edx and ecx which we treat as scratch).
        push esi
        push ebx
        push edi
    
        mov esi, ecx
        mov edi, edx
        xor ebx, ebx
    
    ; -----------------------
    ; esi: pQuat
    ; edi: pOct
    ; ebx: zero (because we use lea)
    ; ecx: temp pointer to pQuat
    
    ; Reject NULL pQuat
        test esi, esi
        jz WriteNull
    
    ; -----------------------
    ; Reject 0 length pQuat
        mov bl, BYTE PTR [esi]
        test bl, bl
        jz WriteNull
    
    ; -----------------------
    ; How many chars in pQuat?
        mov dl, bl ; bl is first digit as ascii.  Preserve it.
    
    CountLoop:
        inc ecx ; One more valid char
    
    ; While we're counting, check for invalid digits
        cmp dl, '0'
        jl WriteNull
        cmp dl, '3'
        jg WriteNull
    
        mov dl, BYTE PTR [ecx] ; Read the next char
        test dl, dl ; End of string?
        jnz CountLoop
    
        sub ecx, esi
    
    ; -----------------------
    ; At this point, there is at least 1 valid digit, and
    ; ecx contains # digits
    ; bl still contains first digit as ascii
    
    ; Normally we process 3 digits at a time.  But the number of
    ; digits to process might not be an even multiple of 3.
    
    ; This code finds the 'remainder' when dividing ecx by 3.
    ; It might seem like you could just use 'div' (and you can),
    ; but 'div' is so insanely expensive, that doing all these
    ; lines is *still* cheaper than a single div.
        mov eax, ecx
        mov edx, 0AAAAAAABh
        mul edx
        shr edx, 1
        lea edx, [edx+edx*2]
        sub ecx, edx ; This gives us the remainder (0-2).
    
    ; If the remainder is zero, use the normal 3 digit load
        jz LoadTriplet
    
    ; -----------------------
    ; Build a triplet from however many leading 'odd' digits
    ; there are (1 or 2).  Result is in al.
    
        lea eax, DWORD PTR [ebx-48] ; This get us the first digit
    
    ; If there was only 1 digit, don't try to load 2
        cmp cl, 1
        je OneDigit
    
    ; Load the other digit
    
        shl al, 2
        mov bl, BYTE PTR [esi+1]
        sub bl, 48
        or al, bl
    
    OneDigit:
    
        add esi, ecx ; Update our pQuat pointer
        jmp ProcessDigits
    
    ; -----------------------
    ; Build a triplet from the next 3 digits.
    ; Result is in al.
    
    ; bl contains the first digit as ascii
    LoadTriplet:
    
        lea eax, DWORD PTR [ebx-48]
    
        shl al, 4 ; Make room for the other 2 digits.
    
        ; Second digit
        mov cl, BYTE PTR [esi+1]
        sub cl, '0'
        shl cl, 2
        or al, cl
    
        ; Third digit
        mov bl, BYTE PTR [esi+2]
        sub bl, '0'
        or al, bl
    
        add esi, 3 ; Update our pQuat pointer
    
    ; -----------------------
    ; At this point
    ; al: Triplet
    ; ch: DigitWritten (initially zeroed when computing remainder)
    ProcessDigits:
    
        mov dl, al
        shr al, 3 ; left digit
        and dl, 7 ; right digit
    
        ; If we haven't written any digits, and we are
        ; about to write a zero, skip it. This deals
        ; with both "000123" and "2" (due to OneDigit,
        ; the 'left digit' might be zero).
    
        ; If we haven't written any digits yet (ch == 0), and the
        ; value we are are about to write is zero (al == 0), skip
        ; the write.
        or ch, al
        jz Skip1
    
        add al, '0' ; Convert to ascii
        mov BYTE PTR [edi], al ; Write a digit
        inc edi ; Update pointer to output buffer
    
        jmp Skip1a ; No need to check again
    
    Skip1:
        or ch, dl ; Both check and update DigitWritten
        jz Skip2
    
    Skip1a:
    
        add dl, '0' ; Convert to ascii
        mov BYTE PTR [edi], dl ; Write a digit
        inc edi ; Update pointer to output buffer
    
    Skip2:
    
    ; Load the next digit.
        mov bl, BYTE PTR [esi]
        test bl, bl
        jnz LoadTriplet
    
    ; -----------------------
    ; All digits processed.  We know there is at least 1 valid digit
    ; (checked on entry), so if we never wrote anything, the value
    ; must have been zero.  Since we skipped it to avoid
    ; unnecessary preceding zeros, deal with it now.
    
        test ch, ch
        jne WriteNull
        mov BYTE PTR [edi], '0'
        inc edi
    
    ; -----------------------
    ; Write the trailing NULL.  Note that if the returned string is
    ; 0 bytes long, an error occurred (probably invalid digits).
    WriteNull:
        mov BYTE PTR [edi], 0
    
    ; -----------------------
    ; Cleanup
        pop edi
        pop ebx
        pop esi
    
    Failed:
        ret
    
    @PrintOct@8 ENDP
    
    end
    

    I've run a string with 1,000,000,000 quaternary digit thru it as well as all the values from 0-4,294,967,295. Seems to work.

    I for one welcome our new 4-digited alien overlords.