﻿// Charl Marais
// 4 March 2010
// Truncates long numbers to shorter ones, tacking on a suffix
// like bn, m, etc . Currently only truncates to a trillion
//
// USAGE:
//    var trunc = new longNumberTruncator(x);
//    var truncatedNumber = trunc.truncate();
//
//
//    var trunc = new longNumberTruncator(x, "trlj", "milj", "mil");  //<-- The suffixes are optional params
//    var truncatedNumber = trunc.truncate();
//
//    var num = LongNumberTruncator.truncateNumber(x); //<-- Also has optional params for suffixes
function LongNumberTruncator(number, trSuffix, bnSuffix, mSuffix, ensureTwoDecimals) {
    var roundedToTwoDecimals = Math.round(number * 100) / 100;
    var _number = String(roundedToTwoDecimals);

    var twoDecimals = false;
    if (ensureTwoDecimals == true)
    	twoDecimals = true;

    if (twoDecimals) {
    	if (_number.indexOf('.') == -1) {
    		_number = _number + '.00';
    	}
    	else {
    		//Make sure number has two decimals.
    		var nrParts = _number.split('.');
    		var dec = nrParts[1];
    		if (nrParts[1].length >= 2)
    			dec = nrParts[1].substr(0, 2);
    		else
    			dec = nrParts[1] + '0';

    		_number = nrParts[0] + '.' + dec;
    	}
    }

    var _numberSections;
    var _trSuffix = trSuffix ? trSuffix : "tr";
    var _bnSuffix = bnSuffix ? bnSuffix : "bn";
    var _mSuffix = mSuffix ? mSuffix : "m";

    //Initialize
    parseIntoThousandsFactors();

    //Public Methods
    this.truncate = function () {
        try {
            if (_numberSections && _numberSections.length > 2)
                return getFirstPart() + "." + getSecondPart() + getTruncatorPostFix();
        }
        catch (e) { /*Ignore and pass back original number*/ }

        return _number;
    }

    //Private Methods
    //Returns the part before the dec of the number
    function getFirstPart() {
        var firstPart = _numberSections[0];
        if (_numberSections.length > 5) {

            for (var index = 1; index <= _numberSections.length - 5; index++)
                firstPart += "," + _numberSections[index];
        }
        return firstPart;
    }

    //Returns the decimal part of the number
    function getSecondPart() {
        var secondPart = "";
        if (_numberSections.length > 5) {
            secondPart = _numberSections[_numberSections.length - 4]
        }
        else {
            secondPart = _numberSections[1]
        }
        secondPart = (+secondPart);
        secondPart = String(gaussianRound(secondPart / 10) * 10); //String(Math.round(secondPart / 10) * 10);
        return secondPart.substr(0, 2);
    }

    function getTruncatorPostFix() {
        if (_numberSections.length == 3)
            return _mSuffix;
        else if (_numberSections.length == 4)
            return _bnSuffix;
        else if (_numberSections.length >= 5)
            return _trSuffix;
        return "";
    }

    //Backup method for whent the string is not in the expected format.
    function parseIntoThousandsFactorsWithoutSplitter() {
        //Strategy, get number without any markup (like "," between thousands). 
        //Then split it into thousands by taking 3 at a time.
        var tmpNumber = _number.replace(/,/g, "");
        var dec = tmpNumber.indexOf('.', 0);  //<-- The decimal needs to be taken into account
        dec = dec < 0 ? _number.length : dec;

        var indexSeed = dec / 3;
        var length = index = parseInt(String(indexSeed), 10);
        if (length < indexSeed) { //<-- When the number cannot be evenly split into portions of 3, ensure a extra loop to get the very first part
            length++; index++;
        }
        var sections = new Array(index);
        var start = dec;
        while (index > 0) {
            var to = start;
            start -= 3;
            if (start < 0)
                start = 0;
            sections[--index] = tmpNumber.substring(start, to);
        }
        sections[length - 1] += tmpNumber.substring(dec, tmpNumber.length); //<--Add back the decimal!
        _numberSections = sections;
        _number = sections.join(",");
    }


    //Number strings are expected in "123,456,789.00" format.
    function parseIntoThousandsFactorsWithSplitter() {
        _numberSections = _number.split(/,/g);

        //Do a sanity check
        for (var index in _numberSections) {
            var dec = 0;
            if (index == _numberSections.length - 1) {
                dec = _numberSections[index].indexOf('.', 0);
                if (dec < 0)
                    dec = 0;
            }
            //Use the backup method.
            if (index > 0 && _numberSections[index].length - dec != 3)
                parseIntoThousandsFactorsWithoutSplitter();
        }
    }

    //Selects a method to parse out the string
    function parseIntoThousandsFactors() {
        if (_number.indexOf(",", 0) >= 0)
            parseIntoThousandsFactorsWithSplitter();
        else
            parseIntoThousandsFactorsWithoutSplitter();
    }
}


LongNumberTruncator.truncateNumber = function (number, trSuffix, bnSuffix, mSuffix, ensureTwoDecimals) {
    try {
		var twoDecimals = false;
		if(ensureTwoDecimals == true)
			twoDecimals = true;

		var truncator = new LongNumberTruncator(number, trSuffix, bnSuffix, mSuffix, twoDecimals);
        return truncator.truncate();
    }
    catch (e) { /*Ignore and pass back original number*/ }
    return number;
}


/**
 * Gaussian rounding (aka Banker's rounding) is a method of statistically
 * unbiased rounding. It ensures against bias when rounding at x.5 by
 * rounding x.5 towards the nearest even number. Regular rounding has a
 * built-in upwards bias.
 */
function gaussianRound(x) {
    var absolute = Math.abs(x);
    var sign     = x == 0 ? 0 : (x < 0 ? -1 : 1);
    var floored  = Math.floor(absolute);
    if (absolute - floored != 0.5) {
        return Math.round(absolute) * sign;
    }
    if (floored % 2 == 1) {
        // Closest even is up.
        return Math.ceil(absolute) * sign;
    }
    // Closest even is down.
    return floored * sign;
}


LongNumberTruncator.divByAndDropDecimals = function (value, divBy) {
	var nr = (value / divBy);
	return numberFormat(nr, '#,###');
}
