098e5c1a565b47e9860539fbebc3fa98

During a code review we found the following function buried deep in the web UI written by someone no longer here. There has to be a better way. One of our requirements is that prices be displayed with four decimal places. (ex: 0.0001)

function formatCurrency(num) {
    num = num.toString().replace(/\\$|\\,/g,'');
    if (isNaN(num)) num = '0';
    sign = (num == (num = Math.abs(num)));
    num = Math.floor(num*100+0.50000000001);
    cents = num%100;
    num = Math.floor(num/100).toString();
    if (cents < 10) cents = '0' + cents;
    
    for (var i = 0; i < Math.floor((num.length-(1+i))/3); i++)
        num = num.substring(0,num.length-(4*i+3))+','+ num.substring(num.length-(4*i+3));
        
    return (((sign)?'':'-') + '$' + num + '.' + cents);
}

Refactorings

No refactoring yet !

543d0222ea3269453527c07141d48e4b

Rene Saarsoo

January 20, 2009, January 20, 2009 22:36, permalink

No rating. Login to rate!

You sayd: "One of our requirements is that prices be displayed with four decimals."

What do you mean by that? Is four decimals like 1234,5678.00 or is it like 0.1234? Or even something else?

Anyway... first of all I just wrote a set of unit tests to understand, whet this function actually does. Most of it was as expected, but the first line in this function does some pretty weird stuff with input (look at the test "strange input"). Are you sure that it really needs to do that?

test("fractions", function() {
  equals( formatCurrency(0), "$0.00" );
  equals( formatCurrency(5.5), "$5.50" );
  equals( formatCurrency(.25), "$0.25" );
});

test("rounding", function() {
  equals( formatCurrency(5.204), "$5.20" );
  equals( formatCurrency(5.205), "$5.21" );
  equals( formatCurrency(5.206), "$5.21" );
});

test("thousands separator", function() {
  equals( formatCurrency(100), "$100.00" );
  equals( formatCurrency(1000), "$1,000.00" );
  equals( formatCurrency(10000), "$10,000.00" );
  equals( formatCurrency(100000), "$100,000.00" );
  equals( formatCurrency(1000000), "$1,000,000.00" );
});

test("negative", function() {
  equals( formatCurrency(-5), "-$5.00" );
});

test("wrong type of input", function() {
  equals( formatCurrency(""), "$0.00" );
  equals( formatCurrency({}), "$0.00" );
  equals( formatCurrency(true), "$0.00" );
  equals( formatCurrency(false), "$0.00" );
});

test("strange input", function() {
  equals( formatCurrency("2\\"), "$2.00" );
  equals( formatCurrency("2\\,5"), "$25.00" );
  equals( formatCurrency("2\\,5\\,\\,6"), "$256.00" );
  equals( formatCurrency("\\,\\,\\,\\"), "$0.00" );
});
098e5c1a565b47e9860539fbebc3fa98

slf

January 22, 2009, January 22, 2009 18:29, permalink

No rating. Login to rate!

Probably does not need to do that, although it is not clear from the requirements what should happen if a user would type that. If it makes it better take it out, if I as a user typed that it would probably have been a copy/paste mistake on my part. I've taken a quick stab at it below and left it in. The only unit test that fails for me is "rounding2". Not sure why that's happening because "rouding3" works. Probably something esoteric inside Math.toFixed()

function formatCurrency(num) {
	var stripped = num.toString().replace("$","").replace(",",""); // strip off the $ and thousands seperators
	var n = (isNaN(stripped)) ? 0 : stripped*1; // coerce into a number, default to zero
	var dollars = Math.floor(n);
	var unformatted = dollars + ""; // coerce just the left side of the decimal into a string
	
	// from right to left, inject commas
	var formatted = "";
	for (var i=unformatted.length; i>0; --i) {
		var offset = i - 1;
		formatted = unformatted.substring(offset,i) + formatted;
		var position = offset - unformatted.length;
		if ( (i != 1) && (position != 0) && ((position % 3) == 0) ) {
			formatted = "," + formatted;
		}
	}
	
	// format the four decimal places and rip off the left side and decimal
	var cents = ((n - dollars).toFixed(4) + "").split(".")[1];
	
	// return the concatinated string and swap the sign to the other side if needed
	return ("$" + formatted + "." + cents).replace("$-","-$");
}


if ( typeof fireunit === "object" ) {

	// fractions
	fireunit.compare( "$0.0000", formatCurrency(0), "fractions1");
	fireunit.compare( "$5.5000", formatCurrency(5.5), "fractions2");
	fireunit.compare( "$0.2500", formatCurrency(.25), "fractions3");

	// rounding
	fireunit.compare( "$5.0000", formatCurrency(5.00004), "rounding1");
	fireunit.compare( "$5.0001", formatCurrency(5.00005), "rounding2");
	fireunit.compare( "$5.0002", formatCurrency(5.00016), "rounding3");

	// thousands separator
	fireunit.compare( "$100.0000", formatCurrency(100), "thousands1");
	fireunit.compare( "$1,000.0000", formatCurrency(1000), "thousands2");
	fireunit.compare( "$10,000.0000", formatCurrency(10000), "thousands3");
	fireunit.compare( "$100,000.0000", formatCurrency(100000), "thousands4");
	fireunit.compare( "$1,000,000.0000", formatCurrency(1000000), "thousands5");

	// negative
	fireunit.compare( "-$5.0000", formatCurrency(-5), "negative");

	//wrong type of input
	fireunit.compare( "$0.0000", formatCurrency(""), "wrongtype1");
	fireunit.compare( "$0.0000", formatCurrency({}), "wrongtype2");
	fireunit.compare( "$0.0000", formatCurrency(true), "wrongtype3");
	fireunit.compare( "$0.0000", formatCurrency(false), "wrongtype4");

	// strange input
	fireunit.compare( "$0.0000", formatCurrency("2\\"), "strange1");
	fireunit.compare( "$0.0000", formatCurrency("2\\,5"), "strange2");
	fireunit.compare( "$0.0000", formatCurrency("2\\,5\\,\\,6"), "strange3");
	fireunit.compare( "$0.0000", formatCurrency("\\,\\,\\,\\"), "strange4");
	
	fireunit.testDone();
}
543d0222ea3269453527c07141d48e4b

Rene Saarsoo

January 23, 2009, January 23, 2009 08:52, permalink

No rating. Login to rate!

Alright, lets try this.

First of all I created utility function round(), because for some reason JavaScript lacks a rounding function that allows you to specify the number of digits after decimals separator. And toFixed() does rounding in an uncommon way.

The strange input handling I left out.

Another difference is, that formatCurrency(true) == "$1.0000" not "$0.0000".

function formatCurrency(num) {
  num = isNaN(num) ? 0 : num;
  var rounded = round(num, 4).toFixed(4);
  
  // split to three parts
  var parts = rounded.match(/^(-?)([0-9]+)(.[0-9]+)$/);
  var sign = parts[1];
  var integer = parts[2];
  var fraction = parts[3];
  
  // split integer part into thousands
  var thousands = [];
  while (integer.length > 3) {
    thousands.unshift( integer.substr(integer.length - 3, 3) );
    integer = integer.substr(0, integer.length - 3);
  }
  thousands.unshift(integer);
  
  // separate thousands with comma and put it all together
  return sign + "$" + thousands.join(",") + fraction;
}

// Better rounding than with Math.round()
// Allows to specify the number of digits after decimal point
// round(5.1256, 2) --> 5.13
function round(nr, digits) {
  digits = digits || 0;
  var multiplier = Math.pow(10, digits);
  return Math.round(nr * multiplier) / multiplier;
};
098e5c1a565b47e9860539fbebc3fa98

slf

January 26, 2009, January 26, 2009 17:15, permalink

No rating. Login to rate!

I especially like the unshift and join methodology. That's a nice touch, I looks cleaner and less cryptic than my for loop. Any way to make this smaller/faster/better?

F1e3ab214a976a39cfd713bc93deb10f

Tj Holowaychuk

February 27, 2009, February 27, 2009 00:53, permalink

No rating. Login to rate!

I would not use fireunit for starters, something DOM based would let you test in several browsers at least. check out http://github.com/visionmedia/jspec/tree/master

098e5c1a565b47e9860539fbebc3fa98

slf

March 2, 2009, March 02, 2009 17:12, permalink

No rating. Login to rate!

If you like jspec you might fall in love with http://www.sproutcore.com/

F1e3ab214a976a39cfd713bc93deb10f

Tj Holowaychuk

March 2, 2009, March 02, 2009 19:14, permalink

No rating. Login to rate!

what makes you say that? SproutCore is just a dev framework no? not a testing framework?

Your refactoring





Format Copy from initial code

or Cancel