Stack Exchange Network
Stack Exchange network consists of 183 Q&A communities including
Stack Overflow
, the largest, most trusted online community for developers to learn, share their knowledge, and build their careers.
Visit Stack Exchange
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It only takes a minute to sign up.
Sign up to join this community
Teams
Q&A for work
Connect and share knowledge within a single location that is structured and easy to search.
Learn more about Teams
This is a Python 3 script that converts any real number (
int
s and
float
s) 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...
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?
\$\begingroup\$
\$\endgroup\$
–
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.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.