Numbers for People

MISSION: To print a number in human-readable format
SKILLS: Modular arithmetic, String operations

Numbers are pretty awesome. People are also pretty awesome. However, the relationship between humans and numbers is fuzzy to most. We do some pretty complex stuff with numbers in our heads without even knowing it. Let's take reading numbers for instance.

464,723,104,643

If I asked you to read this number aloud, you'd do without issue. What's going on in your head when you do this? Well, a few things pop into my head.

  • Well, there are 4 groups of numbers, so we'll start at billion
  • Okay the first one is 4. four hundred
  • The second is 6. sixty
  • And lastly, 4. four
  • Putting this all together, we start with four hundred sixty-four billion

I don't need to teach you how to read numbers - you all know how. But modeling this process with computer code is pretty interesting! So that's what we'll be doing today.

Now, guidelines are kind of fuzzy - but I'm hoping you can closely match my output. A lot of people get confused when to use 'and' and when to use, hyphens, etc. As usual, feel free to do whatever looks best or makes the most sense for you! However, many of you will try to match my output, which is cool too.

initech:numbers-for-people jordan$ ./num-to-human.py 9
nine
initech:numbers-for-people jordan$ ./num-to-human.py 12
twelve
initech:numbers-for-people jordan$ ./num-to-human.py 64
sixty-four
initech:numbers-for-people jordan$ ./num-to-human.py 587
five hundred eighty-seven
initech:numbers-for-people jordan$ ./num-to-human.py 590
five hundred ninety
initech-2:numbers-for-people jordan$ ./num-to-human.py 10000000000
ten billion
initech:numbers-for-people jordan$ ./num-to-human.py 45671
fourty-five thousand six hundred seventy-one
initech:numbers-for-people jordan$ ./num-to-human.py 123456789
one hundred twenty-three million four hundred fifty-six thousand seven hundred eighty-nine
initech:numbers-for-people jordan$ ./num-to-human.py 1234567890
one billion two hundred thirty-four million five hundred sixty-seven thousand eight hundred ninety
initech:numbers-for-people jordan$ ./num-to-human.py 464723104643
four hundred sixty-four billion seven hundred twenty-three million one hundred four thousand six hundred forty-three
initech:numbers-for-people jordan$

Be sure to save this code (why wouldn't you?) because we'll be using it for some analysis next time.

All the best,
Jordan

3 thoughts on “Numbers for People

  1. 
    import math, sys
    
    def say_number(n):
        # Cheap way out.
        if n >= 10 ** 39:
            raise ValueError, 'Number ridiculously large'
    
        numbers = [
            None, 'one', 'two', 'three', 'four', 'five', 'six',
            'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve',
            'thirteen', 'fourteen', 'fifteen', 'sixteen',
            'seventeen', 'eighteen', 'nineteen',
        ]
    
        tens = [
            None, None, 'twenty', 'thirty', 'fourty', 'fifty',
            'sixty', 'seventy', 'eighty', 'ninety'
        ]
    
        large_names = [
            None, 'thousand', 'million', 'billion', 'trillion',
            'quadrillion', 'quintillion', 'sextillion',
            'septillion', 'octillion', 'nonillion', 'decillion',
            'undecillion'
        ]
    
        chunks = []
        words = []
    
        if n < 0:
            words.append('minus')
            n = -n
    
        # Split number into chunks
        while n != 0:
            chunks.append(n % 1000)
            n /= 1000
    
        # Combine them with large number names: 1,234,567 =>
        # [(1, 'million'), (234, 'thousand'), (567, None)]
        for n, u in reversed(zip(chunks, large_names)):
            # If the chunk is zero, skip it entirely
            if n == 0:
                continue
    
            # Split off hundred part, then handle the final digits:
            if n >= 100:
                words.append(numbers[n / 100])
                words.append('hundred')
                n %= 100
    
            # 00 is nothing, [01]x and x0 are special, xx are combined
            if n == 0:
                pass
            elif n < 20:
                words.append(numbers[n])
            elif n % 10 == 0:
                words.append(tens[n / 10])
            else:
                parts = tens[n / 10], numbers[n % 10]
                words.append('{0}-{1}'.format(*parts))
    
            if u is not None:
                words.append(u)
    
        return ' '.join(words) or 'zero'
    
    for i in sys.argv[1:]:
        print say_number(int(i))
    
  2. Not quite as elegant as nooodl’s, but…

    import sys
    
    def humanize_num(num):
      res = []
      if num > 10 ** 15:
        raise ValueError("Only handles numbers up to 999,999,999,999,999")
      ones = (None, "one", "two", "three", "four", "five", "six", "seven", "eight",
              "nine", "ten", "eleven", "twelve", "thirteen", "fourteen",
              "fifteen", "sixteen", "seventeen", "eighteen", "nineteen")
      tens = (None, None, "twenty", "thirty", "forty", "fifty", "sixty", "seventy",
              "eighty", "ninety")
      hundreds = (None, "thousand", "million", "billion", "trillion")
      idx = 0  # Where in the "hundreds" the chunk is
      while num > 0:
        # Extract only the three-digit chunk we're looking at
        sec = num % 1000
        num = num // 1000
        # Append the "hundreds" separator
        if hundreds[idx] and sec:
          res.append(hundreds[idx])
        # Split off ones, tens, and hundreds place
        one = sec % 10
        ten = (sec // 10) % 10
        hundred = (sec // 100) % 10
        if ten == 1:  # We're in the "teens"
          res.append(ones[sec % 100])
        else:
          if ones[one]:
            res.append(ones[one])
          if tens[ten]:
            res.append(tens[ten])
        if ones[hundred]:
          res.append("hundred and")
          res.append(ones[hundred])
        idx += 1
    
      if res:
        return " ".join(reversed(res))
      else:
        return "zero"
    
    if __name__ == "__main__":
      n = int(sys.argv[1])
      print humanize_num(n)
    
  3. First forray into c++ after hello world. :s Be advised, this thing’s REALLY big.

    #include <iostream>
    #include <string>
    #include <stdexcept>    //an interesting note. g++ doesn't incluide stdexcept by default for some reason, visual studio does so you don't need it in VS.
    using namespace std;
    
    /*
    A method to translate a real number and convert it into a a human readable number.
    
    @param String input A string representation of the number we're going to change over. We're using a string as that makes it number container independant and most number containers have some sort of toString capability.
    @returns A string representing the number that was inputted in a human format (ie. twelve, fifteen, twenty nine). 
    
    This method is NOT  fast, efficient, and consumes a considerably larger amount of memory. It is also much more complex, requires vigorous error checking, and is a lot bigger line wise then other solutions.
    What it looses is speed and effeciency however it makes up for in flexibility and modularity. It can be adapted easily with minimal refactoring to suit an application (at least I like to think so).
    
    Examples:
    123456  - "one hundred twenty three thousand four hundred fifty six"
    123.456 - "one hundred twnety three point four five six"
    312.000 - "three hundred twelve"
    -0.01233 - "Negative point zero one two three three"
    1.64300  - "one point six four three"
    It's not strictly technically how it's supposed to be read but that's how my brain handles it.
    
    Tested and confirmed to compile in visual studio 2010 express and linux mint 12.
    */  
    
    string convertNumberToHumanReadable(string inputNumber)
    {	//we're using a define to deal with the problem of handling numbers larger then are scale quantifier can handle along with it's error handling.
    	// To get this number it is (N/3 + 1) where N is (x*10^N). N must be a multple of 3 as that's how our numbering system works. +1 is needed to handle 10^1 which doesn't have a quantifier.
    	#define scaleQuantifierNumberOfElements 22
    	const string baseTenNumbers[10] = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
    	const string nteensNumbers[10] = { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen,", "eighteen", "nineteen" };
    	const string nTiesNumbers[10]  = {"", "", "twenty", "thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninety" };
    	const string scaleQuantifier[scaleQuantifierNumberOfElements] = { "\b", "thousand", "million", "billion", "trillion",
    																	  "quadrillion", "quintillion", "sextillion",
    																	  "septillion", "octillion", "nonillion",
    																	  "decillion", "undecillion", "duodecillion",
    																	  "tredicillion", "quattourdecillion", "quindecillion",
    																	  "sexdecillion", "septendecillion", "octodecillion",
    																	  "novemdecillion", "vigintillion" };
    	bool isNegative = false;
    	int decimalPoint = inputNumber.length(); //this puts the decimal point past the right most number by default which is where it would exist on an integer.
    
    	//sanity checks start here, check for empty string, for negative symbol at start, and for the '.', if the number is too large for this method to handle, and if the input is just zero.
    	if (inputNumber.length() == 0)
    		throw runtime_error("Input is empty");
    	if (inputNumber.find('-') == 0)
    	{
    		isNegative = true;
    		inputNumber = inputNumber.substr(1, (inputNumber.length() - 1));
    	}
    	if (inputNumber.find_first_of('.') != inputNumber.find_last_of('.'))   //if the first instance and the last instance don't equal each other, means more the one . characters, means not a real number
    		throw runtime_error(inputNumber.append(" is not a real number"));
    	else if (inputNumber.find('.') < string::npos && inputNumber.find('.') != inputNumber.length() - 1 )
    		decimalPoint = inputNumber.find('.');
    
    	//now is the character check.
    	//48 = '0', 57 = '9', 46 = '.'
    	for(int counter=0 ; counter <  (int) inputNumber.length() ; counter++)
    	{
    		if ((inputNumber.at(counter) < 48 || inputNumber.at(counter) > 57) && inputNumber.at(counter) != '.')
    			throw runtime_error(inputNumber.append(" is not a real number"));
    	}
    
    	if (decimalPoint > ((scaleQuantifierNumberOfElements - 1) *3))
    		throw runtime_error("Number is too large for this system to handle");
    
    	if ( inputNumber.length() == 1 && inputNumber.at(0) == '0' )
    		return "zero";
    
    	//Sanity checks are now completed. 
    
    	string returnedString = "";
    	/*
    	//for some reason this is not trimming off non zero numbers.
    	if(decimalPoint < (int)inputNumber.length())
    		inputNumber = inputNumber.substr(inputNumber.find_last_not_of('0'));   
    
    	This is suppose to trim of trailing zero's if there's a decimal point, since it'd be redundant to have them. It's not incorrect to leave them there technically but that's not how we would generally read it.
    	C++ string library has the find_last_not_of method but looks like I'm still a bit muddy on how it's suppose to work. Right now all it does is trim all but the first character in a string.
    	So I'm going to cheat for a bit and use a for loop to trim off any trailing zeros after the decimal point, at least until I can figure out just how that method is supposed to work.
    	*/
    	for (int counter = inputNumber.length() - 1 ; counter >= decimalPoint ; counter --)
    	{
    		if (inputNumber.at(counter) == '0' || inputNumber.at (counter) == '.')
    			inputNumber = inputNumber.substr(0 ,inputNumber.length() - 1);
    		else
    			counter = -1;
    	}
    
    	//we handle numbers to the right of the decimal point now, creating the right half of the string.
    	for(int counter = inputNumber.length() - 1 ; counter > decimalPoint ; counter -- )
    	{
    		returnedString = string(baseTenNumbers[inputNumber.at(counter) - 48]).append(" ").append(returnedString);
    	}
    	if(decimalPoint < (int)inputNumber.length())
    		returnedString = string("point ").append(returnedString);
    
    	//we appending leading 0's to the front if neccessary. By doing this we can take steps of 3 in the for loop and not worry about the < 3 cases which would require much more complex if statements.
    	while((decimalPoint ) % 3 != 0)
    	{
    		inputNumber = string("0").append(inputNumber);
    		decimalPoint++;
    	}
    
    	//now we deal with numbers to the left of the decimal point.
    	for (int counter = decimalPoint -1 ; counter > 0 ; counter -= 3)
    	{
    		string tmp = "";
    		if (inputNumber.at(counter - 2) != '0')//we'll look at each set of numbers in a triplet going left. Look at the left most number in the triplet to determine the hundred number.
    			tmp = string(baseTenNumbers[inputNumber.at(counter - 2) - 48]).append(" hundred ");
    		if(inputNumber.at(counter - 1) == '1') //next is the next number, if it's 1, it's a nTeen, otherwise it's a nTies
    			tmp = tmp.append(nteensNumbers[inputNumber.at(counter ) - 48]).append(" ");
    		//nTies
    		else if (inputNumber.at(counter - 1) > '1')
    			tmp = tmp.append(nTiesNumbers[inputNumber.at(counter - 1) -48]).append(" ");\
    		//ones if the number before isn't a teen.
    		if (inputNumber.at(counter) != '0' && inputNumber.at(counter - 1) != '1')
    			tmp = tmp.append(baseTenNumbers[inputNumber.at(counter) - 48]).append(" ");
    
    		//finally we append the quantifier
    		if(tmp != "")
    		{
    			tmp = tmp.append(scaleQuantifier[((decimalPoint - 1) / 3) - (counter / 3)]).append(" " );
    			returnedString = tmp.append(returnedString);
    		}
    	}
    	if(isNegative)
    		returnedString = string("Negative ").append(returnedString);
    	return returnedString;
    }
    
    int main( int argc, const char* argv[] )
    {
    	if (argc != 2 )
    	{
    		cout << "Can only handle one argument. Please hang up and try your number again." << endl;
    		return 1;
    	}
    	try
    	{
    		cout << convertNumberToHumanReadable(argv[1]) << endl;
    	}
    	catch (std::runtime_error &e)
    	{
    		cout << e.what() << endl;
    		return 1;
    	}
    }
    

Leave a Reply