3
\$\begingroup\$

This is a Python 3 script that converts any real number (ints and floats) from decimal notation to any positional number system representation with an integer base between 2 and 36 and vice versa, meaning it can convert negative fractions.

Technically it can convert any arbitrary base, but I struggle to find a way to represent the output, its output is like hexadecimal, but instead of only letters a to f, all of the English alphabet can be used, so there can be a unique symbol for all numbers up to 35.

I don't know how to represent number systems with bases higher than 36, if I continue to use strings, if I use the Greek alphabet after Latin alphabet, I can represent bases up to 60, but many Greek letters have homoglyph in Latin alphabet... And if I just use a list, it would be perfectly fine if they only represent positive integers, but here I need a list for the integral part and a list for the fractional part, and potentially a str before the first list...

The actual code that does the job is extremely simple, but I added many useless validations that slows the execution down tremendously, just so in case someone gives incorrect inputs...

Code

import re
from typing import Union

ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'

def convertfrom_decimal(number: Union[int, float], base: int=16) -> str:
    if not isinstance(number, (float, int)):
        raise TypeError('Argument `number` must be a numerical value (int or float)')
    
    if not isinstance(base, int):
        raise TypeError('Argument `base` must be an integer')
    
    if not 2 <= base <= 36:
        raise ValueError('This function can only support bases from 2 to 36')
    
    negative = False
    if number < 0:
        negative = True
        number = -number
    
    whole = int(number)
    fraction = number - whole
    
    if whole:
        integral = ''
        while whole:
            whole, digit = divmod(whole, base)
            integral = ALPHABET[digit] + integral
    else:
        integral = '0'
    
    if not fraction:
        return integral if not negative else '-'+integral
    
    fractional = ''
    bits = 0
    while bits < 48:
        bit, fraction = divmod(fraction*base, 1.0)
        fractional += ALPHABET[int(bit)]
        if fraction < 2**-48:
            break
        bits += 1
    
    result = integral + '.' + fractional
    return result if not negative else '-'+result

def convertto_decimal(string: str, base: int=0) -> float:
    if not isinstance(string, str):
        raise TypeError('Argument `string` must be an instance of `str`')
    
    if not isinstance(base, int):
        raise TypeError('Argument `base` must be an instance of `int`')
    
    if base and not 2 <= base <= 36:
        raise ValueError('Argument `base` is not between 2 and 36')
    
    if not re.match('^(\-|\+)?[0-9a-z]+(\.[0-9a-z]+)?$', string):
        raise ValueError('Argument string is not a valid numerical notation supported by this function')
    
    negative = False
    if string[0] == '-':
        negative = True
        string = string[1:]
    
    fraction = ''
    if '.' in string:
        whole, fraction = string.split('.')
    else:
        whole = string
    
    indicator = max(ALPHABET.index(d) for d in string if d in ALPHABET)
    if not base:
        for b in (2, 8, 16, 36):
            if indicator < b:
                base = b
                break
    
    if indicator >= base:
        raise ValueError('Argument `base` is incorrect')
    
    #integral = int(whole, base)
    integral = sum(
        ALPHABET.index(d)*base**i for i, d in enumerate(whole[::-1])
    )
    if not fraction:
        return integral if not negative else -integral
    fractional = sum(
        ALPHABET.index(d)*base**-(i+1) for i, d in enumerate(fraction)
    )
    number = integral + fractional
    return number if not negative else -number

if __name__ == '__main__':
    for i in range(2, 37):                               
        print(convertfrom_decimal(3.1415926535897932, i))

Output

11.001001000011111101101010100010001000010110100011
10.010211012222010211002111110221222020010102220122
3.021003331222202020112203
3.032322143033432411241211414143234130344233124014
3.050330051415124105232005511454424522431000231043
3.066365143203613410601052256200101122510662105012
3.1103755242102643
3.124188124074427866112818683125147474717151652050
3.141592653589793115997963468544185161590576171875
3.16150702865a484776333a98347444320a009a7420206870
3.184809493b9186459aaa3a83
3.1ac1049052a2c71005161571824ba0969c9c2497709caba9
3.1da75cda813752b70268a34a22a74bc41348ccb8c5b228a7
3.21cd1dc46c2b7ab624ee5cd3a5322906081b7e4cc8822538
3.243f6a8885a3
3.26fag579ed6gdea8g2f5a1a2386be4847dfa9c199955365a
3.29fdeh0g77186b7e590fg494559fgf1df946f3b06h5636d9
3.2d23982975gfh9b5f957e5005e7c16d3dg23bh47838i4h7i
3.2gceg9gbhj9cc1508a2e3jdf
3.2k961edi5h85d7fhkhd2idf09bjkafe8bf0513def9k2hkj3
3.32bek9a809gafkj34f0jlilchkcg9hach0d5acji5geh42gb
3.35kh9k813jk70fjjjl150i0ikg7mffmm4efak0ih8g3i0km4
3.39d911bclk3nn443
3.3dc9fine6e76llndlfjmi7k9n2fcnlh7m95927g497l0oaka
3.3higbebohjh2bh66ka19afih5lahe37b9h5ipiend7np4mjd
3.3m5q3m2dcpq63bohkl3n4gedlg4jjk1ii8g7qi8ngbeablk4
3.3r06liojplq9mr9eq1867957
3.4328n0cjqmic2nrmogpp06ff7hd864qe4kjg48db7da6hkcf
3.47d01ee07qs3an4tkttin4l91k8a7jrh1ot5gjqa431c5imf
3.4c25oe856s2t8tg5rue7psq0h3m72hd7tloa5ja67lj96li8
3.4gvml245kc
3.4m6dn4ow9qwe210nr3u0cdqkcnrbmwlh7kmfeapn9fijt38k
3.4rn5c8ianuxpep3owhg3n4m1o6r595s5kmr3djex4m1k6cph
3.4xfrgmtm53gd8tfed3xnstgi56yfaa7dvfrxe5vb5wq7qe4e
3.53i5ab8p5fc5vayqter60f6r

I have noticed that for binary I can have up to 48 fractional bits, 24 bits for quaternary, 16 bits for octal, 12 bits for hexadecimal and 9.6 bits for base-32, all other bases have precision limits under 48 bits, but interestingly ternary seems to have infinite precision...

So how is the code? Are there any areas that can be improved?

\$\endgroup\$
1

1 Answer 1

3
\$\begingroup\$

ALPHABET should be formed from ascii_letters and digits.

base having a default of 16 is kind of an odd choice? I think it would be less surprising to have no default at all and force the programmer to be explicit.

You call your validations useless but they seem fine to me (mostly). The one I would exclude is indicator. Rather than doing all of that hard work up front, it's just as good to attempt an index(d), catch an IndexError and raise ValueError(f'Digit {d} is invalid for base {base}').

Rather than hard-coding the number 36, it's best to assign that constant as len(ALPHABET) somewhere in the globals.

convertfrom_decimal (which should probably be called convert_from_decimal) has special treatment for the fractional component and integral component, which I think it shouldn't. You should just be able to get the mantissa, and then iteratively multiply and subtract until you hit a maximum number of digits or the remaining mantissa is zero.

Your use of if not re.match is a missed opportunity: the regex is not only useful as validation, but if you name and capture your groups, it can be used to parse the string as well.

index(d) is not as efficient as it could be. Rather than using the ALPHABET string opaquely, you could test to see whether the character is a letter or number and subtract accordingly, which will obviate any iteration through your ALPHABET string.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.