Convert Number to Textual Equivalent

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

User avatar
Jonah Bron
DevNet Master
Posts: 2764
Joined: Thu Mar 15, 2007 6:28 pm
Location: Redding, California

Convert Number to Textual Equivalent

Post by Jonah Bron »

Hello, world!

This is a little piece of code I wrote just for fun. It converts any number you enter into the equivalent in words. For example, if you pass 234832039, it will return *"two hundred thirty-four million, eight hundred thirty-two thousand thirty-nine". There is one boolean switch: $commas. This defaults to true. If set to false, no commas will be displayed.

Usage:[text]$var = new WordedNumber(mixed $number [, boolean $commas_enabled defaults true]);
echo $var->read();[/text]

The functions work by looping through the number three digits at a time. This is really cool because it's totally extensible. Just add more large number names to the $w_names array (note the leading space). This was quite a logical challenge (for me), but I finally figured it out.

Code: Select all

class WordedNumber {
	private $number;
	private $text;
	private $w_ones = array('zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine');
	private $w_tens = array(null, null, 'twenty', 'thirty', 'fourty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninty');
	private $w_teens = array('ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'ninteen');
	private $w_name = array('', ' thousand', ' million', ' billion', ' trillion', ' quadrillion');
	private $comma_index = 0;
	private $name_index = false;
	private $digits = 0;
	private $comma_enabled;
	
	// Parent processor
	public function __construct($int, $commas=true) {
		if (intval($int) == 0) {
			$this->text = $this->w_ones[0];
			return;
		}
		$this->formatInput($int);
		$this->setNamePos();
		$this->comma_enabled = $commas;
		foreach ($this->number as $chunk) {
			$chunk = array_pad($chunk, 3, 0);
			$tens = $this->getTens($chunk);
			$hundreds = $this->getHundreds($chunk[2]);
			$this->text .= $hundreds . $tens;
		}
	}
	
	// Process first two digits
	private function getTens($int) {
		$have_comma = ($this->comma_index > 1 && $this->comma_enabled ? ', ' : ' ');
		$len = count($int);
		$name = prev($this->w_name);
		$result = '';
		if ($int[1] == 1) {
			$teen_num = $this->w_teens[$int[0]];
			$result = $teen_num . $name . $have_comma;
			$this->comma_index++;
			$this->name_index = true;
		} elseif ($int[0] > 0 || $int[1] > 0) {
			$one_digit = $this->w_ones[$int[0]];
			$ten_digit = $this->w_tens[$int[1]];
			$have_dash = $ten_digit && $one_digit ? '-' : '';
			$result = $ten_digit . $have_dash . $one_digit . $name . $have_comma;
			$this->comma_index++;
			$this->name_index = true;
		}
		return $result;
	}
	
	// Process hundred's place
	private function getHundreds($int) {
		$result = '';
		if ($int > 0) {
			$one_digit = $this->w_ones[$int];
			$num_name = ($this->name_index ? '' : prev($this->w_name));
			$have_comma = ($this->comma_index > 1 && !$this->name_index && $this->comma_enabled ? ', ' : ' ');
			$result = $one_digit . ' hundred' . $num_name . $have_comma;
			$this->comma_index++;
		}
		return $result;
	}
	
	// Get input ready for processing
	private function formatInput($int) {
		$this->number = array_map('intval', array_reverse(str_split(strval($int))));
		$this->digits = count($this->number);
		$this->number = array_reverse(array_chunk($this->number, 3));
	}		
	
	// Set initial index position for number names
	private function setNamePos() {
		for ($i = 0; $i < count($this->number); $i++) {
			next($this->w_name);
		}
	}
	
	// Pass result up
	public function read() {
		return $this->text;
	}
}
Ain't code just beautiful? :mrgreen:

See the array_map() in the constructor? This is the first time I've used it, and I've gotta say, it's pretty nice. Before, I had the code for whole loop. I took up way too much space, I said, "Hey, there's gotta be something for this in the manual.", looked it up, and there it was.

P.S., I've optimized/compressed the tar out of this thing, so critique at your own risk! :P

* Actual result

Edit: changed it, trimmed off 3 lines. Function now loops through the number forward instead of backward.
Edit 2: opened up a bit, moved the conditional statements into multiple lines where necessary
Edit 3: be sure to pass only numbers in string form, as numbers too big (as per AbraCadaver's post below) give a bad result. Thanks for pointing that out.
Edit 4: changed the format a bit to be more readable.
Edit 5: turned into class
Last edited by Jonah Bron on Mon Jul 12, 2010 11:37 pm, edited 13 times in total.
User avatar
Apollo
Forum Regular
Posts: 794
Joined: Wed Apr 30, 2008 2:34 am

Re: Convert Number to Textual Equivalent

Post by Apollo »

Nice piece of code Jonah!
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Convert Number to Textual Equivalent

Post by josh »

Jonah Bron wrote:P.S., I've optimized/compressed the tar out of this thing, so critique at your own risk! :P
You should ask yourself if "compressing" helps or hurts readability. Do you have an old version of the file for us to compare it with? Trying to do stuff in less lines of code results in programs that are readable by computers and not humans. Martin Fowler wrote in his Refactoring book: "any fool can write a program a computer can read, its a lot more tricky to write one a human can read". If "compressing" your code doesn't do anything to help the readability then you should ask yourself what you're even doing compressing it in the first place ;-) I mean yeah you want to remove duplication - but you've inlined conditionals which mixes it with other code that is concatenating strings. My mind can only juggle so much code. I'm glad you were able to juggle it but do you want to be a programmer or an entertainer? hah.. its entertaining to see what you were able to get the code down to, but that's about it. Sorry to say. I'd rather see it take up 1,000 lines but be easier to read. Other than that good job.

* Note that "compressing" code is a good thing a lot of times, but "compressing" should not be the goal in and of itself. Code readability should be the end, and compressing should only be a means to that end. Un-compressing is just as important. Example renaming from $i to $numberOfDigits - The computer couldn't care and the shorter one is "sexier". Unfortunately the shorter one is also the one that is going to cause your team mates to curse you out. lol
User avatar
AbraCadaver
DevNet Master
Posts: 2572
Joined: Mon Feb 24, 2003 10:12 am
Location: The Republic of Texas
Contact:

Re: Convert Number to Textual Equivalent

Post by AbraCadaver »

You don't want to pass an int to this function, and you probably don't want to cast anything to an int within the function either. I haven't tested, but you probably want to deal exclusively with strings:

Code: Select all

$int = 100000000000000;
echo int_to_words($int) . " ($int)\n";
32 bit
[text]two hundred seventy-six million, four hundred fourty-seven thousand, two hundred thirty-two (1.0E+14)[/text]

Code: Select all

$string = '100000000000000';
echo int_to_words($string) . " ($string)\n";
32 bit
[text]two billion, one hundred fourty-seven million, four hundred eighty-three thousand, six hundred fourty-seven (100000000000000)[/text]

Code: Select all

$int = 100000000000000000000;
echo int_to_words($int) . " ($int)\n";
64 bit
[text](1E+20)[/text]

Code: Select all

$string = '100000000000000000000';
echo int_to_words($string) . " ($string)\n";
64 bit
[text]nine, two hundred twenty-three quadrillion, three hundred seventy-two trillion, thirty-six billion, eight hundred fifty-four million, seven hundred seventy-five thousand, eight hundred seven (100000000000000000000)[/text]
EDIT: Ahhh, I see you stopped at quadrillion. Still won't work on 32 bit. Maybe some error checking for invalid values. No time to look now, but you might be able to do something with floats, not sure.
mysql_function(): WARNING: This extension is deprecated as of PHP 5.5.0, and will be removed in the future. Instead, the MySQLi or PDO_MySQLextension should be used. See also MySQL: choosing an API guide and related FAQ for more information.
User avatar
Jonah Bron
DevNet Master
Posts: 2764
Joined: Thu Mar 15, 2007 6:28 pm
Location: Redding, California

Re: Convert Number to Textual Equivalent

Post by Jonah Bron »

Now there's an angle of critique I didn't anticipate. :lol: Mostly what I meant by "compressing" was really simplifying the algorithm and removing unnecessary code. I am guilty of trying to make it as small as possible though. :( I have read that quote before, and generally endeavor try to code by it. I'll open up the code a bit.

Thanks for the input, AbraCadaver. I'm on a 64-bit system, so I guess I just didn't test any numbers large enough to observe that bug. I wish the conversion to string was more straightforward. :roll:

Edit: commented the code.
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Convert Number to Textual Equivalent

Post by josh »

Jonah Bron wrote:Edit: commented the code.
http://franksworld.com/blog/archive/201 ... 12035.aspx

its less readable now to me. But yeah I did say something nice in my post (after the constructive criticism). I mean it in the nicest way possible I swear!

you write it like this

Code: Select all

$res = $twos[$int[$i1]] .                                                                       // Add 10's place
                                ($int[$i] != 0 ? '-' . $ones[$int[$i]] : '') .          // Add one's place if appropriate
                                current($ext) .                                                                         // Append number name
                                ($comma > 1 && $commas ? ', ' : ' ') .                          // Append comma if appropriate
                                $res;    
You have 2 conditionals and 5 strings being concat'd, all in one line. Thats 7 operations and its hard to read. 11 operations in one statement If you count accessing array indexes as an operation.

I'd do it like this these days

Code: Select all

$tensPlaceDigit = $tens[ $tensNumeral ];
$onesPlaceDigit = $numerPlace ? '' : $ones[ $onesNumeral ];
$result = $tensPlace . $onesPlace;
 
see how after every few "mental operations" I "checkpoint" myself by storing the result in a variable? No more than one or two operations per line, ideally. Now I can refer to different concepts by a name, "the tens place" ($tensPlaceDigit). Instead of juggling 7 different operations at once to make sense of it, I can digest it in small pieces.

Press that semi colon key followed by return key early & often.
User avatar
Jonah Bron
DevNet Master
Posts: 2764
Joined: Thu Mar 15, 2007 6:28 pm
Location: Redding, California

Re: Convert Number to Textual Equivalent

Post by Jonah Bron »

josh wrote:
Jonah Bron wrote:Edit: commented the code.
its less readable now to me
Yeah, the code box here on the forum wraps all of the text. It's more readable in a code editor.

I've only coded to very loose syntax standards before now. I was trying to cut down on variables, but at the cost of readability. The improvement in the second version you gave is clear. Perhaps it's time I read the Zend Coding Standards. I'll read that link you gave me.

Cheers.
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Convert Number to Textual Equivalent

Post by josh »

Jonah Bron wrote: I was trying to cut down on variables, but at the cost of readability. The improvement in the second version you gave is clear. Perhaps it's time I read the Zend Coding Standards.
Glad my example made sense. Coding standards is one thing that helps readability. Zend's code standards (and most code standards) focus on formatting of the code. In my opinion you've formatted it fine. However, there's lots of other things you should learn in addition to formatting. I suggest the book "Clean Code" for you (its the one depicted in the link in my previous post). It will take you to a whole different level very quickly.
User avatar
Apollo
Forum Regular
Posts: 794
Joined: Wed Apr 30, 2008 2:34 am

Re: Convert Number to Textual Equivalent

Post by Apollo »

josh wrote:Martin Fowler wrote in his Refactoring book: "any fool can write a program a computer can read, its a lot more tricky to write one a human can read".
Related quote:
dunno who wrote:Reading code is harder than writing it.
Very true :)
User avatar
Jonah Bron
DevNet Master
Posts: 2764
Joined: Thu Mar 15, 2007 6:28 pm
Location: Redding, California

Re: Convert Number to Textual Equivalent

Post by Jonah Bron »

Apollo wrote:
dunno who wrote:Reading code is harder than writing it.
That's a good one. I just might get that book some time. I made some changes to the code again. :idea:
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Convert Number to Textual Equivalent

Post by josh »

That improved readability a bit. Like a good amount but it should use methods to separate those "kludges" of code.

A kludge is like a block of code in your case that you've separated by a couple "new lines" and prefixed each kludge with a comment describing that kludge. The problem is variables from one kludge can interfere with variables from a kludge 1,000 lines later. When you use methods to separate it you ease the reader's mind because he knows the variables inside each function/method don't interfere with the variables from another method.

Kluding is one of the things people refer to when they talk about use of global variables. But They are really well done kludges that are a LOT better than the original code! Nice work.

Consider another before & after.
Before

Code: Select all

// this is the "kludge 1"
/* Process if first two digits are between 20 and 99 */
                elseif ($len > $i1 && $int[$i1] > 1)
                {
                        // snipped
                }
               
// kludge 2
                /* Process if there is a hundred's place digit */
                if ($len > $i2 && $int[$i2] > 0)
                {
                        // snipped
                }
 
After (would look roughly but not exactly like this)

Code: Select all

$result = '';
foreach( $triplet as $eachThreeDigits)
{
 $result .= $this->appendHundredsPlace(); // this function would encapsulate one of your kludges
 $result .= $this->appendTensPlace(); // this function would encapsulate another one of your kludges
 $result .= $this->appendOnesPlace(); // same
}
return $result;
 
If there were no hundreds place, the appendHundredsPlace() method would just return an empty string, that way I don't have to worry about reading code that says "if ___ then ___ else". Each whole kludge including its "if statement" would live *inside* these new functions.

Also don't get me wrong. I think your code is extremely readable in its current form. However if you wrote a 10,000 line program using your current techniques it would be very hard to follow. So this little program is a good playground for you to learn.
User avatar
Jonah Bron
DevNet Master
Posts: 2764
Joined: Thu Mar 15, 2007 6:28 pm
Location: Redding, California

Re: Convert Number to Textual Equivalent

Post by Jonah Bron »

That improves things majorly. Everything is so... modular. I couldn't separate the one's and ten's place function, but turning it into a class enabled me to cut it down from 3 elseifs to 2. Plus, I added support for zero.
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Convert Number to Textual Equivalent

Post by josh »

Jonah Bron wrote:That improves things majorly.
Agreed. I'd move the __construct() method to the top of the class so the reader reads that method first.
User avatar
Jonah Bron
DevNet Master
Posts: 2764
Joined: Thu Mar 15, 2007 6:28 pm
Location: Redding, California

Re: Convert Number to Textual Equivalent

Post by Jonah Bron »

Done. I thought it had to go after any functions it called, but apparently not.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: Convert Number to Textual Equivalent

Post by VladSun »

I've tried to rewrite your class by using a recursive like processing. It seemed to be very elegant but it failed on several tests. The final version I got is:

Code: Select all

<?php

class Humanizer
{

	private static $ones	= array('', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine');
	private static $twos	= array('twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety');
	private static $teens	= array('ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'ninteen');
	private static $ext	= array('', ' thousand', ' million', ' billion', ' trillion', ' quadrillion');

	private static $result;

	const ZER0	= 1;
	const MATCH     = 1;

	public static function intToWords($value)
	{
		if (!intval($value))
			return 'zero';

		self::$result = '';

		$triples = str_split(str_pad(strval(intval($value)), ceil(strlen($value)/3)* 3, '0', STR_PAD_LEFT), 3);
		array_walk($triples, array(self, 'processTriple'), count($triples));

		return trim(self::$result, ' ,');
	}

	private static function processTriple($triple, $tripleIndex, $tripleCount)
	{
		$value = intval($triple);

		if (!$value)
			return;

		$lt = array
		(
			Humanizer::ZER0
				=> '',
			$value > 0
				=> self::$ones[$value],
			$value > 9
				=> self::$teens[$value - 10],
			$value > 19
				=> self::$twos[$triple[1] - 2].'-'.self::$ones[$triple[2]],
			$value > 19 && !($value % 10)
				=> self::$twos[$triple[1] - 2],
			$value > 99		
				=> self::$ones[$triple[0]].' hundred '.self::$twos[$triple[1] - 2].'-'.self::$ones[$triple[2]],
			$value > 99 && !($value % 10)
				=> self::$ones[$triple[0]].' hundred '.self::$twos[$triple[1] - 2],
		);

		self::$result =  self::$result.' '.$lt[Humanizer::MATCH].' '.self::$ext[$tripleCount - $tripleIndex - 1].', ';
	}

	public static function test($value)
	{
		echo $value." ";
		echo self::intToWords($value);
		echo "<br> ";
	}
}




Humanizer::test(0);
Humanizer::test(4);
Humanizer::test(14);
Humanizer::test(20);
Humanizer::test(30);
Humanizer::test(24);
Humanizer::test(34);
Humanizer::test(320);
Humanizer::test(1000);
Humanizer::test(1024);
Humanizer::test(10246);
Humanizer::test(1000000);
Humanizer::test(1024432);
Humanizer::test(1024432432);
You've changed too much of your code compared to your initial version (which I use to test against). Now I'm not sure how and where commas should be placed :mrgreen:
There are 10 types of people in this world, those who understand binary and those who don't
Post Reply