Here is my class R rebaser. I’ve named it such as it can handle real numbers, both as bases and as inputs. If you only ever intend to use integers for both bases and inputs, it’s better to use the class Z rebaser instead; it’s much faster.
It’s run similar to class Z, you initialize it first and then call its Rebase function:
>>> b=RBaser() >>> b.Rebase(3.141592653589793,10,2) '11.0010010000111111011010101000100010000101110111010011101110010000001111010000111100101011000111000'
You can also pass it numeric strings of an arbitrary length to be more precise. Here is pi in base e:
>>> b.Rebase('3.1415926535897932384626433832795', 10, '2.71828182845904523536')
'10.1010020200021111200201011200010102020001110120200210100212012000202'
But passing strings of such length doesn’t insure good precision. Converting back one gets:
>>> b.Rebase('10.1010020200021111200201011200010102020001110120200210100212012000202', '2.71828182845904523536', 10)
'3.14159265358979323846264338327'
The class uses the decimal module to be able to convert numbers precisely, but you’ll have to set the precision through the class by using its .prec function:
>>> b.prec(85)
85
>>> b.Rebase('3.1415926535897932384626433832795', 10, '2.71828182845904523536')
'10.1010020200021111200201011200010102020001110120200210100212012000202120110012120102010112110112120001021120010001102011112020102020211002100200120000201111101001010200201210011020010020011020212'
And converting back its:
>>> b.Rebase('10.1010020200021111200201011200010102020001110120200210100212012000202120110012120102010112110112120001021120010001102011112020102020211002100200120000201111101001010200201210011020010020011020212', '2.71828182845904523536', 10)
'3.141592653589793238462643383279499999999999999999999999999999999999999999999999999999'
It’s more precise, but there’s still the rounding issue at the very end there which can be handled by being more precise with the inputs.
You may copy and paste the code from below or download the .py file from here.
import warnings
#the warnings should always appear ('cause they're rare!)
warnings.simplefilter("always")
####warnings.simplefilter("ignore")
try:#try using mpmath if installed
from mpmath import mp as _mp
_Deci=_mp.mpf;_log=_mp.log
_using='mpmath'
except:#if not, use the built-in modules
from math import log as _log
from decimal import getcontext as _getCon
from decimal import Decimal as _Deci
_using='decimal'
##except:#don't use the decimal module (not recommended)
## from math import log as _log
## def _Deci(n):return float(n)
## _using=None
class RBaser:
'''A number rebasing system from Orthallelous
Class R
Can convert a number of any base of any real number(R).
Normal acceptable bases are >-1 or 1<
If a base requires a character that unicode does not have,
then the system returns a list containing the
positional values in base 10.
Bases 1, 0 and -1 are special cases:
Base 1 returns a string the length of the number.
Base -1 is similar to base 1, but follows how positive
and negative numbers behave in negative bases.
These bases are not considered to be normal acceptable bases.
Bases between -1 and 1 but not 1, 0 or -1 are fractional bases
These bases use characters that correspond to their inverted base.
'''
def __init__(self):
from sys import maxunicode
self._maxChr=maxunicode+1#base 0 uses no characters
#so bases 1 to 65536 are valid (instead of 0 to 65535)(or 1 to 1114112)
self._InitString()#build intial string
self._prec=30#precision
self._Base1_chr=self._str[0]#char for bases 1, -1
self._spec_case=True#enable/disable special case bases
self._neg='-'#internal neg and sep signs
self._sep='.'
self.Class='R'
#---- error related code -----
class RidiculousError(ValueError):pass
class BaseZero(ValueError):pass
self._BZErr=BaseZero
self._RidErr=RidiculousError
self._MaxLengthAllowed=100000#max allowed string length
self._rebuildDicts()
#--- Text display/print system ---------------------------------------------------------------------
def _Verify(self,Print=False):
'Determines if every character currently in ._str appears only once.'
#~19 minutes for len(str)=1114112(in which there are 1048576 repeats)
if Print:print('Verifying...')
Str=self._str[::-1]#remove the last occurance of the repeat chars (if found)
rp=len(Str)-len(set(Str))
if rp:
if Print:
print('Mismatch!\nFound {:,} Repeated character{}\nRepairing...'.format(rp,'s'if rp>1 else''))
for u in range(self._maxChr):
ch=unichr(u)
if Str.count(ch)<2:continue
while Str.count(ch)>1:Str=Str.replace(ch,'',1)
self._str=Str[::-1]
if Print:print('Rebuilding Dictionaries...')
self._rebuildDicts();self._Verify(Print)
else:
if Print:print('No repeated characters, verification complete.')
return
#--- string constructor ----------------
def _InitString(self):
'constructs the inital string'
R,L=[],'''0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$&#?!@:;<=>[\]"%'()*+,-./^_`{|}~ \t\n\r'''
for j in range(255,-1,-1):
if unichr(j)not in L:R.append(unichr(j))
self._str=''.join([L]+R)# get remaining characters
def _Constructor(self,B,invert=False):
'builds the string used for displaying the numbers'
rest,B=[],int(abs(B))
if B>len(self._str):#if the base is larger than the string, add to it
b=B
if B>=self._maxChr:b=self._maxChr-1# handle last unicode character cleanly
rest.extend(map(unichr,range(len(self._str),b+1)))
self._str=''.join([self._str,''.join(rest)])#add to string permanently
self._rebuildDicts()
#note: the Constructor! (raise your fist and have it emanate light while you say it)
# should always accept the base number and the bool invert
# and have the two return lines below. The rest one can strip out
# and replace with whatever builds the string in whatever order you'd like
if invert:return self._DictB#{1:'1'} -value:character
else:return self._DictF#{'1':1} -character:value
#--- end string constructor ------------
def _rebuildDicts(self):
'rebuilds the dicionaries storing the character:value data'
self._DictB=dict(enumerate(self._str))#{1:'1'}# value:character
self._DictF=dict(zip(self._str,range(len(self._str))))#{'1':1}# character:value
def _displayer(self,L,B,neg='-',sep='.'):
'''Converts a number list L of base B into a string
If abs(B) is higher than unicode, then it just returns the list'''
_n,_s=self._neg,self._sep
#list contains a negative number but the sign isn't in order(should always be first)
if _n in L and L[0]!=_n:L.remove(_n);L.insert(0,_n)
Max=abs(max([i for i in L if type(i)in[int,long]]))
#a value is in the list that is equal or greater than the base (it shouldn't be there)
if Max>=abs(B)or None in L:
E=TypeError(u'Unsupported value for base {}: {}'.format(B,Max))
raise E
#list contains value higher than maxunicode, return as a list (past the error above, so its fine)
if Max>=self._maxChr:return L
#if all the numbers after the sep are zero, lop them off
if _s in L:
if not any(L[L.index(_s)+1:]):L=L[:L.index(_s)]
syms,n,f=self._Constructor(abs(B),True),'',''#{1:'1'}
syms[_n]=neg;syms[_s]=sep;n=map(syms.get,L)
return''.join(n)
def _deplayer(self,num,B,neg='-',sep='.'):
'''Converts a string (num) of base B into a list in decimal.
Its the opposite of _displayer. Only works with bases,
abs(B), that are smaller than unicode'''
_n,_s=self._neg,self._sep
syms,N=self._Constructor(abs(B)),[]#{'1':1}
## try:num=_Deci(num)#for really small numbers
## except:pass#if you pass this something like 0.00002, python sees it as
## # 2e-05 and when it strings it, it becomes '2e-05', not '0.00002'
## # and that causes problems below
num=unicode(num)
if neg in num:N=[_n];num=num.replace(neg,'')
if sep in num:#if string gets here, sep should appear only once
Num,Ber=num.split(sep)
N.extend(map(syms.get,Num))
N.append(_s)
N.extend(map(syms.get,Ber))
#num=num.replace(sep,'.');syms[sep]='.'
else:N.extend(map(syms.get,num))
#a character was used that has a value equal or greater than the base (it shouldn't be there)
Max=abs(max([i for i in N if type(i)in[int,long]]))
if Max>=abs(B)or None in N:
ch=self._DictB.get(Max)#if there's a None, then the dict doesn't have a character for that value, much less know if its higher than B
E=TypeError(u'Unsupported character for base {}: {}'.format(B,ch))
raise E
return N
#--- END Text display/print system -----------------------------------------------------------------
#--- special case bases-----------------------------------------------------------------------------
#--- special case base -1 --------------
def _10_to_Neg1(self,n):
'Converts a number in base 10 to base -1'
if n>0:return self._Base1_chr*(n*2-1)
if not n:return''
if n<0:return self._Base1_chr*(-n*2)
def _Neg1_to_10(self,n):
'Converts a string of a number in base -1 to base 10'
l=len(n)
if not l%2:return-l/2
else:return l/2+1
#--- end special case base -1 ----------
#--- special case base 0 ---------------
def _Zero_to_10(self,n):
'Converts a number in base 0 to base 10'
E1=TypeError("A base zero number system doesn't have any characters")
E2=self._BZErr("Indeed. Makes you think, doesn't it?")
if not n:
raise E2
else:
raise E1
#--- end special case base 0 -----------
#--- special case base 1 ---------------
def _10_to_1(self,n):
'Converts a number in base 10 to base 1'
if n<0:return''.join([self._neg,self._Base1_chr*abs(int(n))])
elif n>0: return self._Base1_chr*int(n)
else:return''
def _1_to_10(self,n):
'Converts a string of a number in base 1 to base 10'
if self._neg in n:return -(len(n)-1)
else: return len(n)
#--- end special case base 1 -----------
#--- bases between -1 and 1 ------------
# to use, first convert into base 1/b, this call this
def _10_to_Frac_B(self,n,B):
'Converts a number from base 10 to base B Where -1<B<1 and B!=0'
_n,_s=self._neg,self._sep
if not -1<B<1 or B==0:
E="this function does not accept a base of this value: {:.4f}".format(B)
raise TypeError(E)
invert=_Deci(1)/_Deci(B)
if B<0:L=self._10_to_NB(n,invert)
else:L=self._10_to_B(n,invert)
if L==[0]:return L
add_neg=False
if _n in L:L.remove(_n);add_neg=True
shift=L.index(_s)-1
# for a base 0<b<1, convert to base 1/b, shift fractional separator to the left one digit, swap all the digits, see ref. 2
L.remove(_s);L.insert(shift,_s)
if add_neg:L.append(_n)
L.reverse()
#remove any leading zeros
while L[0]==0:
if L[1]==_s:break#leave at least one leading zero in front of the fractional separator
L=L[1:]
return L
#--- end bases between -1 and 1 --------
#--- END special case bases-------------------------------------------------------------------------
#--- Normal acceptable base systems ----------------------------------------------------------------
#--- bases <-1 -------------------------
def _10_to_NB(self,n,B):
'''Converts a number from base 10 to base b (<-1). returns a list
see ref. 8 for handling negative real bases'''
if n==0:return[0]
#these have to use the decimal module for precise conversion
n=_Deci(n);b=_Deci(abs(B))
d=_Deci(int(_log(abs(n),b)))
prec=self._prec;D=[]#precision, digits
l,r,X=-b/(b+1),1/(b+1),n
def T(x):return-b*x-int(-b*x-l)
if d<=self._MaxLengthAllowed:#don't do this if d is already this big
while not l<=X<r:d+=1;X=n/pow(-b,d)# d is better determined here, the log taken above just speeds it up
if d<=0:#starting with a fractional value
D.extend([0,self._sep])
#D.extend(([0]*-int(d)))
if d>self._MaxLengthAllowed:#don't deal with values this huge
E1='not going to make a string that is at least this length: {:,}'.format(int(d))
E=self._RidErr(E1)
raise E
#this prec in new base appears to work for negative bases so I'm going with it
prec=int(prec*_log(10)/_log(abs(B)))#insure precision is in new base
prec=d-prec#go only as far as precision is good
if prec>=0:prec=-1#but in this event, go as far as B^-1
while d>=prec:
if d==0 and self._sep not in D:D.append(self._sep)#don't add it if its already there!
xi=int(-b*X-l);X=T(X);d-=1
if xi==b:D.extend([xi-1,0])#not that it should ever give this result, but if so, you do this.
else:D.append(xi)
return D
#--- end bases <-1 ---------------------
#--- bases >1 --------------------------
def _10_to_B(self,n,B):
'Converts a number in base 10 to base b (>1). Returns a list'
if n==0:return[0]
prec=self._prec;D=[]#precision, digits
if n<0:D=[self._neg];n*=-1#negatives
n=_Deci(n);B=_Deci(B)#make sure all values are arbi. prec. form
P=_Deci(int(_log(abs(n),B))+1.)#get log power
X=n/pow(B,P)
#while not 0<=X<1:
# P+=1;X=n/pow(B,P)
if P<=0:#starting with a fractional value
D.extend([0,self._sep])
D.extend(([0]*-int(P)))
def T(x):return B*x-int(B*x)
if P>self._MaxLengthAllowed:
E=self._RidErr('not going to make a string that is at least this length: {:,}'.format(int(P)))
raise E
prec=int(prec*_log(10)/_log(B))#insure precision is in new base
prec=P-prec#go only as far as precision is good
if prec>=0:prec=-1#but in this event, go as far as B^-1
while P>prec:
a=int(X*B);X=T(X)
if P==0 and self._sep not in D:D.append(self._sep)#don't add it if its already there!
D.append(a);P-=1
return D
#--- end bases >1 ----------------------
#--- definition of base ----------------
def _B_to_10(self,n,B):
'Converts a list of numbers(n) from ANY base to base 10'
s,N=1,0;B=_Deci(B)
if self._neg in n:n.remove(self._neg);s=-1#handle negatives
#determine order of magnitude
try:P=_Deci(n.index(self._sep)-1);n.remove(self._sep)# floats
except:P=_Deci(len(n)-1)# ints, longs
for j in n:N+=j*pow(B,P);P-=1
return s*N
#--- end definition of base ------------
#--- END Normal acceptable base systems ------------------------------------------------------------
#--- Main functions --------------------------------------------------------------------------------
def __call__(self, num, B1, B2, neg='-', sep='.'):
return self.Rebase(num, B1, B2, neg, sep)
def Rebase(self, num, B1, B2, neg='-', sep='.'):
'''Convert a number(can be a string) from base B1 to base B2
num can be an int, long, float, string, Decimal or a list of postional values
B1 and B2 can be ints, longs, floats, Decimal or numeric strings
Set the fractional seperating sign (sep) to a character that
wouldn't show up as a numerical value in the base.
Set the negative sign (neg) to a character that
wouldn't show up as a numerical value in the base.
A fractional separator that shows up more than once in the
given number implies that it might already bein use as a
numerical value. Likewise for the negative sign.
The negative sign must be leading the number.
'''
# if the number is zero, why do any math? return a zero
if not num:
#special case bases handle zero differently
if-1<=B1<=1 or-1<=B2<=1:pass
else:return 0
#--------error handling---------------------------------------
E1=ValueError('Negative sign appears more than once')
E2=ValueError('Negative sign is not leading the number')
E3=ValueError('Fractional separator appears more than once')
l=[str,unicode]
if type(num)in l:
if num.count(neg)>1:
raise E1
if neg in num and num[0]!=neg:
raise E2
if num.count(sep)>1:
raise E3
#not an error check, see if the string is a zero
zeroQues=num.replace(neg,'').replace(sep,'')
if zeroQues.count(self._str[0])==len(zeroQues) and len(zeroQues):
return 0
if type(B1)in l:
try:B1=_Deci(B1)#see if its a numeric string, if not raise error
except:
V1=ValueError(u"'{}' is not a valid base".format(B1))
raise V1
if type(B2)in l:
try:B2=_Deci(B2)
except:
V2=ValueError(u"'{}' is not a valid base".format(B2))
raise V2
if abs(B1)in[0,1]or abs(B2)in[0,1]:
if not self._spec_case:
return'Special case bases are disabled'
#N0 - number converted from input to list
#N1 - number converted from base B1 to base ten(in list)
#N2 - number converted from base ten(in list) to base B2(in list)
#val- number converted from base B2(in list) to output
#handle bases -1, 0, 1: starting base ------------------------
if B1 in[-1,0,1]:
if B1==-1:N1=self._Neg1_to_10(num)
if B1==0:N1=self._Zero_to_10(num)
if B1==1:N1=self._1_to_10(num)
else:#handle all other starting bases
if type(num)==list:
N1=self._B_to_10(num,B1)
else:#all other bases
b1=B1
if -1<B1<1:b1=_Deci(1.)/B1#so it doesn't break the deplayer
N0=self._deplayer(num,b1,neg,sep)
N1=self._B_to_10(N0,B1)
#handle bases -1, 0, 1: ending bases--------------------------
if B2 in[-1,0,1]:
if B2==-1:return self._10_to_Neg1(N1)
if B2==0:return''
if B2==1:return self._10_to_1(N1)
else:#handle all the other ending bases
if B2<-1:N2=self._10_to_NB(N1,B2)
elif B2>1:N2=self._10_to_B(N1,B2)
else:#bases -1<B2<1 and not 1,0 or -1
N2=self._10_to_Frac_B(N1,B2)
B2=_Deci(1)/_Deci(B2)#for not breaking the displayer in the next line
#put the converted value into a readable format
val=self._displayer(N2,B2,neg,sep)
try:#val could have repeated neg and sep after base conversion
if val.count(neg)>1:# check for that and give a warning.
W1=E1.args[0]
warnings.warn(W1)
if val.count(sep)>1:
W2=E3.args[0]
warnings.warn(W2)
except:pass
#return the converted value-----------------------------------
if type(val)==list:return val#if its a list here, then it should return as one
try:return int(val)
except:
try:return str(val)
except:return unicode(val)
def InDecimal(self, num, B, neg='-', sep='.', as_str=False):
"""Leaves num in base B, but converts each symbol to its value in base 10
Will return a list unless as_str is True"""
num=unicode(num)
Dec=self._deplayer(num,_Deci(B),neg,sep)
if as_str:
dec=[];split=':;|'# this split is just in case the negative sign or fractional sign takes this symbol
split=split.replace(neg,'');split=split.replace(sep,'')
#if the number has both whole and fractional parts, 5.6 should be one block, not 5:.:6
if self._sep in Dec:
loc=Dec.index(self._sep);Dec.pop(loc)
whol=Dec.pop(loc-1);frac=Dec.pop(loc-1)
Dec.insert(loc-1,sep.join([str(whol),str(frac)]))
#if the number is negative, it should be -num, not -:num
if self._neg in Dec:
Dec.pop(0);num=Dec.pop(0)
Dec.insert(0,''.join([neg,str(num)]))
for j in Dec:dec.append(str(j))
Dec=split[0].join(dec)
return Dec
def Guesser(self, Num, B=10, neg='-', sep='.'):
'''Attempts to guess the base of Num and convert it to base B
Warning: The found base is the minimum base required for the
characters in Num. The actual base may be higher or
the negative of what is found. [ actual >= abs(found) ]
This function may also take some time'''
num=unicode(Num)
num=num.replace(neg,'')#replace these as they
num=num.replace(sep,'')# don't count as numerical characters
Current=257
step=1500#how many additional characters to add and search through at a time.
syms=self._Constructor(Current)#{'1':1}
LastIter=0#for ending the while loop if it ran out of characters (instead of throwing an unhelpful error)
High=[]#highest value found
reMove=[]#list of chars to remove from num
while len(num):
for char in num:#search for every character
try:High.append(syms[char]);reMove.append(char)
except:#found a character not currently on hand
Current+=step
syms=self._Constructor(Current)#get more characters
if Current >= self._maxChr:LastIter+=1#ready the last char error
if LastIter>1:#if == 1, may have found the last character but haven't removed it yet, so toss error when its 2 or more
E=ValueError(u"Unknown character: '{}'".format(num[0]))
#possibly hit a character not in the current encoding system
# but if that happens, then there'd be an error on the above line?
# I don't see how it can happen, but I've seen it happen, so... thus this check
raise E
for C in reMove:num=num.replace(C,'')#clear currently found characters
reMove=[]#reset this
Base=max(High)+1
#print("Possible base for given string: {}".format(Base))
return Base, self.Rebase(Num,Base,B,neg,sep)
#--- END Main functions ----------------------------------------------------------------------------
#--- Other -----------------------------------------------------------------------------------------
def prec(self, prec=None):
'Sets the precision. Pass None for the current precision.'
#note: precision is defined in base 10. As such it must be redefined in the
# base that is being used so as the number in the new base so it doesn't lose
# precision. That is, the number shouldn't return with trailing garbage (45.0000000000000452....)
# prec = log(X,10) -> prec = Ln(X)/Ln(10) -> e^prec*10 = X
# prec_in_base_B = ? -> ? = Ln(X)/Ln(B) -> ? = (prec*Ln(10))/Ln(B)
# thus prec_in_base_B = (prec*Ln(10))/Ln(B)
if prec is None:
return self._prec
prec=abs(prec)
if _using=='mpmath':#using mpmath
_mp.dps=prec
elif _using==None:
pass
else:#using built-in decimal module
_getCon().prec=prec
self._prec=prec
return prec
def references(self):
'Reference information'
Refs={'Note':'Some references may not have made it into the final version.\n',
1: 'Weisstein, Eric W. "Base." From MathWorld--A Wolfram Web Resource. http://mathworld.wolfram.com/Base.html',
2: "http://expertscolumn.com/content/how-convert-decimal-number-any-fractional-base-tutorial",
3: "http://mathforum.org/library/drmath/view/71589.html",
4: "http://code.activestate.com/recipes/81611-roman-numerals/",
5: "http://mathematics.knoji.com/how-to-convert-a-decimal-number-into-negative-base-a-tutorial",
6: "http://en.academic.ru/dic.nsf/enwiki/5844205",
7: "'Negative Based Number Systems' W. J. Gilbert, R. J. Green, 'Mathematics Magazine' Vol. 52, pg. 240, 1979",
8: "'Beta-expansions with negative bases' S. Ito, T. Sadahiro, 'Integers' Vol. 9, pg. 239, 2009"}
R=sorted(Refs)
for r in [R[-1]]+R[:-1]:print(' '.join([str(r).center(4)+' : '+Refs[r]]))
print('\nThis \nwork : https://orthallelous.wordpress.com/2014/06/28/number-bases-pt8/')
#---------------------------------------------------------------------------------------------------
#--- END CLASS -------------------------------------------------------------------------------------
Note: this code uses Python’s decimal module. But if you happen to have mpmath, it’ll use that instead.
So what can you do with this? You can use it to convert any real number between any integer base:
>>> b.Rebase(37.4453, -46, 12) '-AB.1030B9703040A78802953A34143B7430478547BA988236524532A9644A217A89A7BA587BB465A361279731144A'
Or convert any integer number between any real base:
>>> b.Rebase(77, 82.78321, 3.3345) '111102.11030021023012112121123000100002011010012210210301201211020302012101120003011113020002303100202022123100120002102022102000001001210002000120112102002300200022112023020010113021001213002'
Or even if you want, you can hide messages:
>>> b.Rebase("0.000000000000 I'm really small!", 97, 7)
'0.0000000000000000000000000000433546465550006433236341202663106450011115164261265540422211413466521613431354642132561333525263535356625122056152255'
>>> b.Rebase('0.171429246949113576245931485487408827188565505806152646272021169630125313515832684233932234173120435716932816', 10, 95)
'0.GREETINGS HUMAN! GREETINGS HUMAN! GREETINGS HUMAN'
———–
So now that every possible base has been covered, we’re done right? Well no, every possible real base has been covered. If negative number bases were deep waters, then complex bases are uncharted seas. And like uncharted territory, there be dragons…
Leave a comment