# Convert any number (up to 10^3000) to text

I kinda got the idea after playing Adventure Capitalist, since that goes up to 10300. I found a website that explained how to go up to 10^3000, so I mainly did this as a challenge to see if I could do it.

Also, I'm fairly new to writing the documentation bits, so feedback on what I did well/not so well would be appreciated. I'm not expecting people to read the entire thing since it ended up quite long, just general feedback would be good.

Basically, it works by initially building a list of every number suffix (like thousand and million) up to nongennovemnonagintillion. Then, when you run the code, it'll probably do similar stuff to conversion codes, with the exception of going a lot higher.

It'll split the number into separate parts matching the exponential values (like 1550 is 1*10^3 + 5*10^2 + 50*10^0, or {3:1, 2:5, 0:50}), then add the remainder on after the iterations finish. I initially had it calculate the remainder too, like 2 hundredths and 62 millions, but I realised nobody speaks like that so took it out aha.

Then with converting that to text, I made a separate function to print any number as text between 0-999. Getting it all working initially was really easy, and most of the code is just fixes for various cases when things didn't work quite right (and I'm still occasionally finding bits that don't work).

Some examples:

>>> LargeNumber('154024500.5').to_text(use_fractions=True)
'154 million, 24 thousand, 5 hundred and 1/2'

>>> LargeNumber('154024500.5').to_text(use_fractions=True, digits=False)
'one hundred and fifty-four million, twenty-four thousand and five hundred point five'

>>> LargeNumber(Decimal('1'+'0'*2000)+Decimal('523.275')).to_text(digits=False)
'one hundred sescenquinsexagintillion, five hundred and twenty-three'

>>> LargeNumber(123456789).to_text(max_iterations=0, num_decimals=2)
'123.46 million'


The script including the documentation is over the character limit so I put it here.

Edit: Before reading the reply from Caridorc, I got all fractions working for text and digits (eg. 3.3333333333333 = 1/3rd or 3.33 = 33.3 hundredths). The result now runs about 10x slower due to the above code causing so many problems I had to patch over, but it's here :)

Here it is without the documentation anyway:

from decimal import Decimal, getcontext
class NumberNames:
#Name lists for 0 to 999
num_units = ['zero','one','two','three','four','five','six','seven','eight','nine']
num_teens = ['ten','eleven','twelve']+[i+'teen' for i in ['thir','four','fif']+num_units[6:]]
num_tens = [i+('ty' if i[-1]!='t' else 'y') for i in ['twen','thir','for','fif']+num_units[6:]]

#Name rules for 10^33 to 10^300
num_exp_prefix = ['', 'un','duo','tre','quattor','quin','sex','septen','octo','novem']
num_exp_amount = ['']+[i+'int' for i in ['vig','trig','quadrag','quinquag','sexag','septuag','octog','nonag']]

#Name rules for 10^303 to 10^3000
num_exp_units = [i+'illion' for i in ['m','b','tr','quadr','quint','sext','sept','oct','non']]
num_exp_tens = [j+'illion' for j in ['dec']+num_exp_amount[1:]]
num_exp_hundreds = ['']+[i+'en' for i in ['c','duoc','trec','quadring','quing','sesc','septing','octing','nong']]

#Set up dictionary and manually add hundred and thousand which don't follow the rules
num_dict = {}
num_dict = 'hundred'
num_dict = 'thousand'

#Create Million through Nonillion (10^6 to 10^30)
exp_current = 6
for i in num_exp_units:
num_dict[exp_current] = i
exp_current += 3

#Iterate through exponential hundreds (cen+)
for prefix_hundreds in num_exp_hundreds:
#Iterate through exponential tens (decillion+)
for prefix_tens in num_exp_tens:
#Iterate through exponential amounts (un, duo, tre, etc)
for prefix in num_exp_prefix:
num_dict[exp_current] = prefix_hundreds+prefix+prefix_tens
exp_current += 3

#Add 'tillion' before 'decillion' after the first run
if not prefix_hundreds:
num_exp_tens = ['tillion']+num_exp_tens

num_dict = ''
num_dict[-1] = ''

all_available_numbers = tuple(sorted(num_dict.keys()))

@classmethod
def num_to_text(self, input=0, as_digits=False, **kwargs):
use_fractions = kwargs.get('use_fractions', True)
only_return_decimal = kwargs.get('only_return_decimal', False)
fraction_precision = kwargs.get('fraction_precision', 100)
print_warning = kwargs.get('print_warning', False)

negative_num = False
#Fix for -0
if isinstance(input, str):
if input == '-':
input = input[1:]
negative_num = True

#Convert to float or int
num_zeroes = 0
input = str(input)

#Get number of zeroes that are being removed to add again later
no_zeroes = input
if '.' in input:
no_zeroes = input.rstrip('0')
num_zeroes = len(input)-len(no_zeroes)

#Fix number of zeros being 1 too high if all decimal points = 0
if no_zeroes[-1] == '.':
num_zeroes -= 1
no_zeroes += '0'

input = Decimal(no_zeroes)

#Make sure number is positive
if input < 0:
input = input*-1
negative_num = True

#If only integers should be returned
if as_digits:
output = str(input)+'0'*num_zeroes

if negative_num:
output = '-' + output

if '.' in output:

#Split output for when it's been combined with another exponential value (must be between 0 and 1)
decimal_output = '0.'+output.split('.')

#Attempt to calculate fraction of decimal point
fraction_output = CalculateFraction.find(float(decimal_output), fraction_precision)
if fraction_output and use_fractions:
fraction_suffix = CalculateFraction.suffix(*fraction_output)
fraction_prefix = ''

#Add prefix if not only returning the decimal
if not only_return_decimal:
fraction_prefix = output.split('.')+' and '

return '{p}{0}/{1}{s}'.format(*fraction_output, s=fraction_suffix, p=fraction_prefix)

else:
#Return decimal points if 0.x
if only_return_decimal:
return '.'+str(output.split('.'))
#Return number with decimals if not between 0 and 1
else:
return str(output)

return output

#If number out of range, recursively use LargeNumber
if 0 > input or input > 999:
if print_warning:
print "Warning: Number out of range!"
return LargeNumber(input).to_text(digits=as_digits)

#Break down number into separate parts
output_hundreds = int(input/100)
output_tens = int((input%100)/10)
output_units = int(input%10)
output_decimals = str(input).split('.')

#Fill output decimals with extra zeroes
if len(output_decimals)>1:
output_decimals += '0'*num_zeroes

output_text = ''
#If number is negative
if negative_num:
output_text += 'negative '
#If number is above 100
if output_hundreds:
output_text += self.num_units[output_hundreds] + ' hundred'
#If last 2 digits are above 10
if output_tens:
if output_hundreds:
output_text += ' and '
if output_tens != 1:
output_text += self.num_tens[output_tens-2]
else:
output_text += self.num_teens[output_units]
#If last digit is above 0
if output_units:
#If last two digits are not between ten and nineteen (added in the tens)
if output_tens != 1:
if output_tens:
output_text += '-'
elif output_hundreds:
output_text += ' and '
output_text += self.num_units[output_units]
if not (output_hundreds or output_tens or output_units) and not only_return_decimal:
output_text += self.num_units[output_units]

if len(output_decimals)>1:
output_text += ' point'
if only_return_decimal:
output_text = 'point'

#Write list of decimals
for i in output_decimals:
output_text += ' '+self.num_units[int(i)]

return output_text

class CalculateFraction(object):

@staticmethod
def find(input, precision=100):
#Convert to float
if not isinstance(input, float):
input = float(input)

#Make sure it is between 0 and 1
original_input = int(input)+1
if not (0 < input < 1):
input = input%1

#Loop through all calculations until a match is found
for j in xrange(2, precision+1):
j = float(j)
for i in xrange(1, int(j)):
if i/j == input:
return i*original_input, int(j)

@staticmethod
def suffix(x, y):
#Convert to str and get important characters
y = str(y)
x = str(x)
last_num = y[-1]
try:
second_num = y[-2]
except IndexError:
second_num = 0

#Define rules
if y == '1':
suffix = ''
elif last_num == '3':
suffix = 'rd'
elif second_num == '1':
suffix = 'th'
elif last_num == '2':
if second_num:
suffix = 'nd'
else:
suffix = ''
elif last_num == '4' and not second_num:
suffix = ''
elif last_num == '1':
suffix = 'st'
else:
suffix = 'th'

#Make plural if x is above 1
if x not in ('1', '0') and suffix:
suffix += 's'

return suffix

class LargeNumber(Decimal):

def __init__(self, input, **kwargs):

#Copy over class objects from decimal
Decimal.__init__(input)
self.input = self.format_input(input, True)
self.all_available_numbers = NumberNames.all_available_numbers

def __repr__(self):
#Remove the exponent if a large int/float value is input
#Squared length seems to stop precision errors but increase if error happens
if 'E+' in str(self.input):
original_precision = getcontext().prec
getcontext().prec = len(str(self.input))**2
formatted_input = self.remove_exponent(self.input)
getcontext().prec = original_precision
else:
formatted_input = self.input

return "LargeNumber('{}')".format(formatted_input)

@staticmethod
def format_input(input, set_prec=False):
input = str(input).replace(" ","").replace("\n","")
getcontext().prec = max(28, len(input))
return Decimal(input)

@staticmethod
def _round_with_precision(input, num_decimals=None, force_decimals=False):
input = str(input)
if '.' in str(input):
original_input = input.rstrip('0')
else:
original_input = input+'.0'
input = Decimal(original_input)

#Accurately round the number
if num_decimals is not None:

#Increase the precision to stop errors on large num_decimals values
current_context = getcontext().prec
getcontext().prec = max(current_context, num_decimals*1.1)
if num_decimals:
input = input.quantize(Decimal('0.'+'0'*(num_decimals-1)+'1'))
else:
input = input.quantize(Decimal('1'))
getcontext().prec = current_context
input = str(input)

if force_decimals:

#Convert output to string and fill in decimals
if '.' not in input:
if num_decimals is not None:
input += '.'+'0'*num_decimals
else:
input += '.0'

elif num_decimals is not None:
current_decimals = len(input.split('.'))
input += '0'*(num_decimals-current_decimals)

#Trim decimal places if not forcing decimals, and if decimal places haven't been cut off
# eg. 70500 = 70.5 thousand, no matter the decimal places
# however, 70500.005 = 70.500005, or to 3 decimal places is 70.500 thousand
if not force_decimals and original_input == input.rstrip('0'):
input = input.rstrip('0')
if input[-1] == '.':
input = input[:-1]

return input

@classmethod
def remove_exponent(self, input):
try:
if input == input.to_integral():
return input.quantize(Decimal(1))
else:
return input.normalize()
except AttributeError:
return self.remove_exponent(self.format_input(input))

@staticmethod
def _find_matching_exp(input, all_available_numbers):
for i in xrange(len(all_available_numbers)):
try:
#Return first match
if input < all_available_numbers[i+1]:
return all_available_numbers[i]
#If number is higher than max index
except IndexError:
return all_available_numbers[i]

def _calculate_number_parts(self, **kwargs):
max_iterations = kwargs.get('max_iterations', -1)
num_decimals = kwargs.get('num_decimals', None)
force_decimals = kwargs.get('force_decimals', False)

min_amount = Decimal(str(kwargs.get('min_amount', 1)))
min_amount_limit = Decimal(str(kwargs.get('min_amount_limit', -1)))

#If more than one iteration, set the min amount to 1 otherwise you get an infinite loop
if max_iterations and min_amount < 1:
min_amount = Decimal('1')

min_offset = min_amount.logb()

input = self.input
num_output = {}
num_exp = 1
first_run = True

#Get multiplier from the min_amount variable
min_amount_multiplier = Decimal(('%.2E'%min_amount).split('E'))

#Match values to exponentials
count = 0
while num_exp > 0 and (input >= 1 or not count):

#Reset min_amount to default
if count == min_amount_limit:
min_offset = Decimal('1').logb()
count += 1

#Figure which name to use
if input:
num_digits = (input/min_amount_multiplier).logb()
else:
#Fix to stop logb() error if input is zero
num_digits = Decimal(1).logb()

num_digits -= min_offset
num_exp = self._find_matching_exp(num_digits, self.all_available_numbers)

#Fix when given a high min_amount value that pushes num_exp below 0
if num_exp <= 0:
num_exp = 0

#Fix for values between 0 and 1
if -1 < input < 1:
num_exp = 0

#Get matching amount
current_multiplier = pow(Decimal(10), Decimal(num_exp))
current_output = input/current_multiplier

if len(num_output)+1 > max_iterations and max_iterations >= 0:
current_output = self._round_with_precision(current_output, num_decimals, force_decimals)
num_output[num_exp] = (str(current_output))
input = 0
break

else:

#Continue
num_output[num_exp] = (str(current_output).split('.'))
input = input%pow(Decimal(10), Decimal(num_exp))

#Make number positive after first run
if input < 0:
input *= -1

num_output[-1] = str(input)

#Re-run the code if only one iteration is output (basically a fix for low min_amount not working)
if len(num_output) == 2 and min_amount != Decimal(str(kwargs.get('min_amount', 1))) and max_iterations != 0:
kwargs['max_iterations'] = 0
num_output = self._calculate_number_parts(**kwargs)

return num_output

def to_text(self, **kwargs):
use_fractions = kwargs.get('use_fractions', False)
fraction_precision = kwargs.get('fraction_precision', 100)

num_decimals = kwargs.get('num_decimals', None)
force_decimals = kwargs.get('force_decimals', False)
min_decimals = kwargs.get('min_decimals', None)

as_digits = kwargs.get('digits', True)

num_name = []
num_name_joined = ''
num_output = self._calculate_number_parts(**kwargs)

#Get remaining decimals for later and remove from output
remaining_decimals = Decimal(num_output.pop(-1))

#Fix for values between 0 and 1
if not num_output:
num_output = 0

#Fix to merge decimal with lowest exponential value to stop errors such as 1.000.35
if force_decimals and remaining_decimals and num_output.get(0, 0):
only_exponential = sorted(num_output.keys())
num_output[only_exponential] = str(self._round_with_precision(str(Decimal(num_output[only_exponential])+
Decimal(remaining_decimals)*
Decimal('0.'+'0'*(only_exponential-1)+'1')),
num_decimals, force_decimals))
remaining_decimals = 0

#Convert numbers to words
sorted_keys = sorted(num_output.keys())[::-1]
for i in sorted_keys:
current_value = num_output[i]
if NumberNames.num_dict[i]:

#Avoid using fractions if using prefix (eg. 0.5 billion not 1/2 billion)
should_use_fractions = use_fractions
if i:
should_use_fractions = False

#Add decimals only if it's the last number, and there's not remaining decimals
if i == sorted_keys[-1] and not remaining_decimals:

if force_decimals:

if num_decimals is not None:

#Check if existing decimal points (fixes things like 46.500.000 million)
current_num_decimals = 0
if '.' in current_value:
current_num_decimals = len(current_value.split('.'))
else:
current_value += '.'
required_decimals = max(0, num_decimals-current_num_decimals)
current_value += '0'*required_decimals

else:
#Stip zeroes from end of number
if '.' in current_value:
current_value = current_value.rstrip('0')

#Only add zero if the last number is a decimal point, or also add a decimal point
if '.' not in current_value[:-1]:
if current_value[-1] != '.':
current_value += '.'
current_value += '0'

if min_decimals:
num_decimal_points = len(current_value.split('.'))
if min_decimals > num_decimal_points:
current_value += '0'*(min_decimals-num_decimal_points)

text_num = NumberNames.num_to_text(current_value, as_digits,
use_fractions=should_use_fractions,
fraction_precision=fraction_precision,

#Fix for min_amount under 0
if i and current_value[:10] == 'zero point':
text_num = text_num[5:]

num_name.append(text_num)

#Join list
if len(num_name)-1:
num_name_joined += ', '.join(num_name[:-1])
#If there are decimals, don't use a final and
if num_name_joined:
if remaining_decimals and as_digits:
num_name_joined += ', '
else:
num_name_joined += ' and '
num_name_joined += num_name[-1]

if remaining_decimals:

if not remaining_decimals:
remaining_decimals = '0.'
if num_decimals is not None:
remaining_decimals += '0'*num_decimals
else:
remaining_decimals += '0'

#Convert to text
remaining_decimals = self._round_with_precision(remaining_decimals, num_decimals, force_decimals)

decimal_num = NumberNames.num_to_text(remaining_decimals, as_digits,
use_fractions=use_fractions,
fraction_precision=fraction_precision,
only_return_decimal=True,
print_warning=True)

if not as_digits:
decimal_num = ' '+decimal_num

#Fix to stop fractions not removing a zero (eg. 0 and 2/5ths should be 2/5ths)
if '/' in decimal_num:
if num_name_joined and num_name_joined != '0':
num_name_joined += ' and '
else:
num_name_joined = ''

#Fix if there are no units (1 hundred.5 to 1 hundred and 0.5)
if 0 not in num_output:
if as_digits and '/' not in decimal_num:
num_name_joined += ' and 0'
else:
pass
#'one hundred point five' works, leaving this note here in case it needs to change
#num_name_joined += ' and zero'

num_name_joined += decimal_num

return num_name_joined

def quick(self, num_decimals=3):
kwargs = {}
kwargs['max_iterations'] = 0
kwargs['num_decimals'] = num_decimals
kwargs['force_decimals'] = True
kwargs['min_decimals'] = 3
kwargs['use_fractions'] = False
kwargs['digits'] = True
return self.to_text(**kwargs)

• Have a look at: codereview.stackexchange.com/questions/43744/… – sobolevn May 22 '15 at 17:20
• I like some of the bits he did (no idea why I didn't go for a simpler negative number approach). His subThousand is very similar to my num_to_text, though I imagine anyone doing this ends up with a similar function to that haha. The splitByThousands bit is a really good idea, but that method wouldn't really work with my min_amount value, and also wouldn't allow extra things such as a googol since he's presuming the exponents are all 3 apart. It fails after 10^62 though, and he never got the decimals working, which is a bit surprising since he'd just need to print the ByOne values :) – Peter May 22 '15 at 23:48

Use syntax highlighting

Many nice modern IDEs have a feature called Syntax highlighting that will give nice colours to your code making it more readable and help you avoid shadowing built-in:

def remove_exponent(self, input):


should be:

def remove_exponent(self, input_):


to avoid deleting the native input function.

>>> help(LargeNumber.__repr__)
Help on method __repr__ in module __main__:

__repr__(self) unbound __main__.LargeNumber method


:(

def __repr__(self):
"""
Remove the exponent if a large int/float value is input
Squared length seems to stop precision errors but increase if error happens
"""
if 'E+' in str(self.input):
original_precision = getcontext().prec
getcontext().prec = len(str(self.input))**2
formatted_input = self.remove_exponent(self.input)
getcontext().prec = original_precision
else:
formatted_input = self.input

return "LargeNumber('{}')".format(formatted_input)


The result is better:

>>> help(LargeNumber.__repr__)
Help on method __repr__ in module __main__:

__repr__(self) unbound __main__.LargeNumber method
Remove the exponent if a large int/float value is input
Squared length seems to stop precision errors but increase if error happens


:)

Must CalculateFraction be a class?

class CalculateFraction(object):

@staticmethod
def find(input, precision=100):
#Convert to float
if not isinstance(input, float):
input = float(input)

#Make sure it is between 0 and 1
original_input = int(input)+1
if not (0 < input < 1):
input = input%1

#Loop through all calculations until a match is found
for j in xrange(2, precision+1):
j = float(j)
for i in xrange(1, int(j)):
if i/j == input:
return i*original_input, int(j)

@staticmethod
def suffix(x, y):
#Convert to str and get important characters
y = str(y)
x = str(x)
last_num = y[-1]
try:
second_num = y[-2]
except IndexError:
second_num = 0

#Define rules
if y == '1':
suffix = ''
elif last_num == '3':
suffix = 'rd'
elif second_num == '1':
suffix = 'th'
elif last_num == '2':
if second_num:
suffix = 'nd'
else:
suffix = ''
elif last_num == '4' and not second_num:
suffix = ''
elif last_num == '1':
suffix = 'st'
else:
suffix = 'th'

#Make plural if x is above 1
if x not in ('1', '0') and suffix:
suffix += 's'

return suffix


When you write staticmethod you mean 'this should be free floating but I put it in a class just because it belongs there logically. But your class contains only 2 staticmethod functions, I would let those float around.

Noisy commenting

For example:

#Iterate through exponential hundreds (cen+)
for prefix_hundreds in num_exp_hundreds:
#Iterate through exponential tens (decillion+)
for prefix_tens in num_exp_tens:
#Iterate through exponential amounts (un, duo, tre, etc)
for prefix in num_exp_prefix:
num_dict[exp_current] = prefix_hundreds+prefix+prefix_tens
exp_current += 3


Almost 1/3 of the lines are comments, and you are telling the what, it is like:

# a is assigned to 10
a = 10


Many of your comments can be removed and the code will improve as they are just noise ad will get obsolete. (Docstrings are great because they give general information, keep them all)

• You write, "to avoid deleting the native input function" — presumably you mean "shadowing" rather than "deleting" (nothing gets deleted). Can you expand this advice to explain why it is important not to shadow the built-in function here? – Gareth Rees May 24 '15 at 10:01
• Thanks a lot for the feedback :) I was wondering if there was something wrong with using input and self.input separately, though with the help(LargeNumber.__repr__) bit, am I supposed to be returning exactly what calculation is performed as opposed to what output is expected? You got my thought process nailed on grouping the fraction class haha, is it really that bad using classes to group similar functions? – Peter May 25 '15 at 10:01
• I did kinda realise I added a few more comments than usual, though to be honest I find them really useful to keep track of what's going on. Before I never commented much, which made it a total nightmare to go back to old code since I'd have to print the values of variables for 10+ mins just to figure out which part does what :) – Peter May 25 '15 at 10:04
• @GarethRees shadowing a built-in ( a thing similar to max = 'foo' is bad because will make it impossible to use the (say) max function. – Caridorc May 25 '15 at 18:51
• @Caridorc: But in this case the function does not use the built-in input, so that can't be the reason. – Gareth Rees May 25 '15 at 18:52