
/*

  SmartClient Ajax RIA system
  Version v9.0p_2021-05-01/LGPL Deployment (2021-05-01)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/
//> @class NumberUtil
// Static singleton class containing APIs for interacting with Numbers.
// @visibility external
//<
isc.defineClass("NumberUtil");

isc.NumberUtil.addClassProperties({

_jsDecimalSymbol : ".",

//> @classAttr NumberUtil.decimalSymbol (String : "." : IR)
// The decimal symbol to use when formatting numbers
// @group i18nMessages
// @visibility external
//<
decimalSymbol : ".",

//> @classAttr NumberUtil.groupingSymbol (String : "," : IR)
// The grouping symbol, or thousands separator, to use when formatting numbers
// @group i18nMessages
// @visibility external
//<
groupingSymbol : ",",

//> @classAttr NumberUtil.negativeSymbol (String : "-" : IR)
// The negative symbol to use when formatting numbers
// @group i18nMessages
// @visibility external
//<
negativeSymbol : "-",

//> @classAttr NumberUtil.currencySymbol (String : "$" : IR)
// The currency symbol to use when formatting numbers
// @group i18nMessages
// @visibility external
//<
currencySymbol : "$",

//> @classAttr NumberUtil.negativeFormat (Number : 1 : IR)
// The format to use when formatting nagative numbers.  Supported values are: 1 = before, 
// 2 = after, 3 = beforeSpace, 4 = afterSpace, 5 = parens
// @group i18nMessages
// @visibility external
//<
negativeFormat : 1, 

//> @classAttr NumberUtil.groupingFormat (Number : 1 : IR)
// The grouping-format for numbers
// @group i18nMessages
// @visibility external
//<
groupingFormat : 1, // 0 = none; 1 = 123,456,789; 2 = 12,34,56,789

    
//> @classMethod NumberUtil.setStandardFormatter()
// Set the standard "toString()" formatter for Number objects.
// After this call, all <code>numberUtil.toString()</code>  calls will yield a number
// in this format.
//
// @param functionName (string) name of a formatting function on the number object prototype
// @group stringProcessing
//<
setStandardFormatter : function (functionName) {
	if (isc.isA.Function(isc.NumberUtil[functionName]))
        isc.NumberUtil.formatter = functionName;
},

//> @classMethod NumberUtil.setStandardLocaleStringFormatter()
// Set the standard locale formatter for all Number objects.
// After this call, all  <code>isc.iscToLocaleString(number)</code> for number instances 
// calls will yield the string returned by the formatter specified.
//
// @param functionName (string) name of a formatting function (on number instances)
// @group stringProcessing
//<
setStandardLocaleStringFormatter : function (functionName) {
	if (isc.isA.Function(isc.NumberUtil[functionName]))
        isc.NumberUtil.localeStringFormatter = functionName;
},

_1zero : "0",
_2zero : "00",
_3zero : "000",
_4zero : "0000",

_getZeroString : function (length) {
    if (length <= 0) return;

	var nu = isc.NumberUtil,
        pad
    ;
    // with > 4 zeros (very rare), build up a leading pad 4 0's at a time
    while (length > 4) {
        if (pad == null) pad = nu._4zero;
        else pad += nu._4zero;
        length -= 4;
    }

    var finalPad;
    switch (length) {
        case 4: finalPad = nu._4zero; break; 
        case 3: finalPad = nu._3zero; break; 
        case 2: finalPad = nu._2zero; break; 
        case 1: finalPad = nu._1zero; break; 
    } 

    // no leading pad (less than 4 zeros total)
    if (pad == null) return finalPad;
    return pad + finalPad;
},

// Remove any exponent from a the formatted number, adding zeros where 
// necessary while preserving the precision represented in the string.
_expandExponent : function (formattedNumber) {

    return formattedNumber.replace(/^([+-])?(\d+).?(\d*)[eE]([-+]?\d+)$/, 

        // Search for an exponential in the formatted number, matching four groups:
        //     sign, natural, fraction, coeffcient
        //                                   
        //     sign        = sign of the number                               
        //     natural     = integer part of significand (a natural number since no sign)
        //     fraction    = fractional part of significand
        //     coefficient = coefficient of number, including sign

        function(matchedString, sign, natural, fraction, coefficient){

            // We define the following variables
            //     lessThanOne           - whether number's absolute value is less than one
            //     normalizedCoefficient - coefficient normalized for the number of digits in
            //                             natural, integer part of the significand (off by one);
            //                             this abstractly represents the total number of digits
            //                             (including any added zeros) to the left of the
            //                             decimal point in the final formatted number 
            //     digitsToCross         - when moving the decimal point left or right from its
            //                             place in the significand to remove the exponential,
            //                             the number of digits from the signficand that will
            //                             be crossed (excluding zeros added by our own logic)

            var lessThanOne = +coefficient < 0, 
                normalizedCoefficient = natural.length + (+coefficient), 
                digitsToCross = (lessThanOne ? natural : fraction).length;

            // Now, build a string of zeros whose length is determined by the absolute value
            // of the coefficient, less the number of digits to cross; this is the number of 
            // zeros needed to separate the number from the decimal point.

            coefficient = Math.abs(coefficient);

            var nZeros = coefficient >= digitsToCross ? 
                         coefficient - digitsToCross + lessThanOne : 0,
                zeros = nZeros > 0 ? isc.NumberUtil._getZeroString(nZeros) : "";

            // Form the significand (joining both parts together), and attach zeros
            var significand = natural + fraction;
            if (lessThanOne) significand  = zeros + significand;
            else             significand += zeros;

            // If absolute value of number is less than one, offset the
            // normalized coefficient by the number of zeros.
            if (lessThanOne) normalizedCoefficient += zeros.length;

            // Output the digits to the left of the decimal point; we may be done
            var result = (sign || "") + significand.substr(0, normalizedCoefficient);

            // If not, add the remaining fractional digits to the right of the decimal
            if (normalizedCoefficient < significand.length) {
                result += "." + significand.substr(normalizedCoefficient);
            }
            return result;
        });
},

//> @classMethod NumberUtil.stringify()
// Return the passed number as a string padded out to digits length.
//
// @param number (number) Number object to stringify
// @param [digits] (number) Number of digits to pad to.  (Default is 2)
// @return (string) Padded string version of the number
//
// @example var str = isc.NumberUtil.stringify(myNumberVar, 2);
// @group stringProcessing
// @visibility external
//<

stringify : function (number, totalDigits, predecimal) {
    if (!isc.isA.Number(number)) return "";

    
    return isc.NumberUtil._stringify(totalDigits, predecimal, number);
},

_stringify : function (totalDigits, predecimal, number) {
    if (number == null) number = this;
    // default to 2 digits
    if (!totalDigits) totalDigits = 2;

    var numberString = number.toString(),
        zeroes = totalDigits - numberString.length
    ;

    // predecimal: ignore any decimal digits, such that two numbers with differing decimal
    // precision get the same total number of characters before the decimal.
    if (predecimal) {
        var dotIndex = numberString.indexOf(isc.dot);
        if (dotIndex != -1) {
            zeroes += (numberString.length - dotIndex);    
        }
    }
    var pad = isc.NumberUtil._getZeroString(zeroes);

    if (pad == null) return numberString;
    return pad + numberString;
},

//> @classMethod NumberUtil.toCurrencyString()
// Return the passed number as a currency-formatted string, or an empty string if not passed a 
// number.
//
// @param number (Number) the number to convert
// @param [currencyChar] (string) Currency symbol, default taken from the locale and can be 
//                                set to an empty string. If not passed and missing from the
//                                locale, defaults to <code>"$"</code>.
// @param [decimalChar] (string) Decimal separator symbol, default taken from the locale. If 
//                                if not passed and missing from the locale, defaults to 
//                                <code>"."</code>.
// @param [padDecimal] (boolean) Should decimal portion be padded out to two digits? True
//                               by default.
// @param [currencyCharLast] (boolean) Should the currency symbol be shown at the end of the 
//                                      string?  If unspecified, it will prefix the number.
// @return (string) Currency-formatted string version of the number
// @group stringProcessing
// @visibility external
//<
toCurrencyString : function (number, currencyChar, decimalChar, padDecimal, currencyCharLast) {
    if (!isc.isA.Number(number)) return "";

    
    return isc.NumberUtil._toCurrencyString(currencyChar, decimalChar, padDecimal, currencyCharLast, number)
},

_toCurrencyString : function (currencyChar, decimalChar, padDecimal, currencyCharLast, number) {
    if (number == null) number = this;

    var negative = number < 0,
        wholeNumber = number < 0 ? Math.ceil(number) : Math.floor(number),
        decimalNumber = Math.abs(Math.round((number - wholeNumber)*100)),
        output = isc.StringBuffer.create()
    ;

    wholeNumber = Math.abs(wholeNumber);

    // default currency/decimal symbols and decimal padding on
    // allow empty string for no currency character
    currencyChar = currencyChar || isc.NumberUtil.currencySymbol || "$";
    decimalChar = decimalChar || isc.NumberUtil.decimalSymbol || ".";
    if (padDecimal == null) padDecimal = true;

    // output sign
    
    if (negative) output.append(isc.NumberUtil.negativeSymbol || "-");

    // output currency symbol first by default
    if (currencyCharLast != true) output.append(currencyChar);

    // output whole number
    output.append(wholeNumber.stringify(1));

    // output decimal symbol and decimal number
    // (unless padding is off and decimal portion is 0)
    if (padDecimal) {
        output.append(decimalChar);
        output.append(decimalNumber.stringify(2));
    } else if (decimalNumber != 0) {
        output.append(decimalChar);
        if (decimalNumber % 10 == 0) output.append(decimalNumber/10);
        else output.append(decimalNumber.stringify(2));
    }

    // output currency symbol last if specified
    if (currencyCharLast == true) output.append(currencyChar);

    return output.toString();
},

//> @classMethod NumberUtil.toLocalizedString()
//  Format the passed number for readability, with:
//  <ul>
//      <li>separators between three-digit groups</li>
//      <li>optional fixed decimal precision (so decimal points align on right-aligned numbers)</li>
//      <li>localized decimal, grouping, and negative symbols</li>
//  </ul>
//  +link{NumberUtil.decimalSymbol, Decimal symbol}, 
//  +link{NumberUtil.groupingSymbol, grouping symbol}, and 
//  +link{NumberUtil.negativeSymbol, negative symbol} will normally come from
//  SmartClient locale settings (which may come from either client OS or application locale
//  settings), but they are also supported as arguments for mixed-format applications
//  (eg normalize all currency to +link{NumberUtil.toUSCurrencyString, US format}, but use the 
// current locale format for other numbers).
//
//  @param number (Number) the number object to convert
//  @param [decimalPrecision] (number) decimal-precision for the formatted value
//  @param [decimalSymbol] (string) the symbol that appears before the decimal part of the number
//  @param [groupingSymbol] (string) the symbol shown between groups of 3 non-decimal digits
//  @param [negativeSymbol] (string) the symbol that indicate a negative number
//  @return (string) formatted number or empty string if not passed a number.
//  @visibility external
//<

toLocalizedString : function (number, decimalPrecision, decimalSymbol, groupingSymbol, negativeSymbol) {
    if (!isc.isA.Number(number)) return "";

    var roundedValue = !decimalPrecision ? number : 
            Math.round(number * Math.pow(10, decimalPrecision)) / Math.pow(10, decimalPrecision);
    var absNum = Math.abs(roundedValue), // remove sign for now; deal with it at the very end of this method
        wholeNum = Math.floor(absNum), // whole part of the number (no decimal)
        wholeString, // string representation of whole part, before formatting
        decimalString, // string representation of decimal part, after formatting (padding)
        wholeChunks = []; // chunks of the whole number, based on 3-digit groupings
    
    // decimal part - doing this first because this code may round the whole part
    if (decimalPrecision) {
        // decimalPrecision specified and > 0, so
        // round/pad the decimal part to the specified number of digits
        var decimalNum = Math.round( (absNum-wholeNum) * Math.pow(10,decimalPrecision) );
        decimalString = isc.NumberUtil._stringify(decimalPrecision, null, decimalNum); // NOTE: stringify() could use a better name
    } else if (decimalPrecision == 0) {
        // decimalPrecision of 0 explicitly specified, so
        // round the whole number and drop the decimal part entirely
        wholeNum = Math.round(absNum);
    } else {
        // decimalPrecision not specified, so show the decimal part if there is one
        if (absNum-wholeNum > 0) {
            //  PRECISION ERROR - the next line of code introduces noise that makes a very long decimal part,
            //  e.g. 1.1 becomes 1.10000000000000009 - what causes this? some int to float conversion?
            //            decimalString = (absNum-wholeNum).toString().substring(2); // drops the leading "0."
            //  So using this alternate approach - just split the toString() on the decimal point
            //  and take the decimal part
            var absString = absNum.toString();
            decimalString = absString.substring(absString.indexOf(isc.NumberUtil._jsDecimalSymbol)+1);
        }
    }

    // whole part - slice it into chunks to be joined with grouping symbols
    wholeString = wholeNum.toString();
    var wholeLength = wholeString.length;
    var tripletCount = Math.floor(wholeLength/3); // number of complete chunks of 3 digits
    if (wholeLength%3) {
        // start with the incomplete chunk (first 1 or 2 digits) if any
        wholeChunks[0] = wholeString.substr(0, wholeLength%3);
    }
    for (var i=0; i<tripletCount; i++) {
        // then slice out each chunk of 3 digits
        wholeChunks[wholeChunks.length] = wholeString.substr(wholeLength%3 + i*3, 3);
    }
    
    // assembly - join the chunks of the whole part with grouping symbols, and glue together
    // the whole part, decimal symbol, decimal part, and negative sign as appropriate
    var outputString = wholeChunks.join(groupingSymbol || isc.NumberUtil.groupingSymbol);
    if (decimalString) outputString = outputString + (decimalSymbol || isc.NumberUtil.decimalSymbol) + decimalString;
    if (roundedValue < 0) outputString = (negativeSymbol || isc.NumberUtil.negativeSymbol) + outputString;
    return outputString;
},

// same as toLocalizedString but handles extra zeroes using decimalPrecision and decimalPad values 
floatValueToLocalizedString : function (number, decimalPrecision, decimalPad) {
    if (!decimalPad) decimalPad = 0;
    var res = isc.NumberUtil.toLocalizedString(number, decimalPrecision);
    var decIndx = res.indexOf(isc.NumberUtil.decimalSymbol);
    var zeroesToAdd = 0;
    if (decIndx < 0) {
        if (decimalPad == 0) return res;
        zeroesToAdd = decimalPad;
        // no decimalSymbol were found, so we adding one
        res += isc.NumberUtil.decimalSymbol;
    } else {
        zeroesToAdd = decimalPad - (res.length - decIndx - 1);    
    }
    if (zeroesToAdd > 0) {
        // add zeroes to the end according decimalPad value
        res += new Array(zeroesToAdd + 1).join('0');
    } else if (zeroesToAdd < 0) {
        // all extra zeroes should be removed
        for (var i = (res.length - 1); i>(decIndx + decimalPad); i--) {
            if (res[i] != '0' && res[i] != isc.NumberUtil.decimalSymbol) break;
        }
        // remove decimalSymbol if is the last one
        if (res[i] == isc.NumberUtil.decimalSymbol) i--;
        res = res.substr(0, i + 1);        
    }
    return res;
},

//> @classMethod NumberUtil.toUSString()
//  Format the passed number as a US string.  Returns empty string if not passed a number.
//
//  @param number (Number) the number object to format
//  @param [decimalPrecision] (number)
//  @return (string) formatted number or empty string if not passed a number
//  @visibility external
//<
toUSString : function(number, decimalPrecision) {
    if (!isc.isA.Number(number)) return "";
    return isc.NumberUtil.toLocalizedString(number, decimalPrecision, ".", ",", "-");
},

//> @classMethod NumberUtil.toUSCurrencyString()
//  Format the passed number as a US Dollar currency string. Returns empty string if not passed 
// a number.
//
//  @param number (Number) the number object to format
//  @param [decimalPrecision] (number)
//  @return (string) formatted number
//  @visibility external
//<
toUSCurrencyString : function(number, decimalPrecision) {
    if (!isc.isA.Number(number)) return "";
    var util = isc.NumberUtil;
    return "$" + util.toLocalizedString(number, decimalPrecision, ".", ",", "-");
},

//> @method NumberUtil.iscToLocaleString()
// Customizeable version of the <code>toLocaleString()</code> method for numbers.
// Called by <code>isc.iscToLocaleString()</code>.
// Uses the formatter set by NumberUtil.setStandardLocaleStringFormatter(), or at the instance 
// level by NumberUtil.setLocaleStringFormatter()
//
// @param number (Number) the number to format
// @return (string) formatted number as a string
//
// @group stringProcessing
//<
iscToLocaleString : function (number) {
    var f = isc.NumberUtil.localeStringFormatter;
    //var method = Number[f] || isc.NumberUtil[f];
    var method = isc.isA.Function(f) ? f : isc.NumberUtil[f];
    return method ? method(number) : number.toString();
},

//> @method NumberUtil.toFormattedString()
// Allow use of a custom number formatter - can be passed in as a parameter, or set by
// NumberUtil.setStandardFormatter()
//
// @param number (Number) the number to format
// @param [formatter] (string) name of a Number function to use
// @return (string) formatted number as a string
//
// @group stringProcessing
//<

toFormattedString : function (number, formatter) {
    var f = formatter || isc.NumberUtil.formatter;
    var method = isc.isA.Function(f) ? f : isc.NumberUtil[f];
    return method ? method(number) : number.toString();
},

toString : function (number) {
    if (isc.isA.Class(number)) return number.valueOf().toString();
    return number.toString();
},

//> @method NumberUtil.parseInt()
// Parse string that contains integer number. This method correctly handles locale based
// separators and currency symbol.
//
// @param string (string) the string to parse
// @return (Number) parsed number as a Number
// @visibility external
//
//<

parseInt : function (string) {
  string = string.replace(new RegExp("[" + this.groupingSymbol + "|"  + this.currencySymbol
      + "]", "g"),"");
  return parseInt(string);
},

//> @method NumberUtil.parseFloat()
// Parse string that contains float number. This method correctly handles locale based
// separators, decimal points and currency symbol.
//
// @param string (string) the string to parse
// @return (float) parsed number as a Number
// @visibility external
//
//<
parseFloat : function (string) {
    string = string.replace(new RegExp("[" + this.groupingSymbol + "|"  + this.currencySymbol
        + "]", "g"), "");
    if (this.decimalSymbol != ".") {
        string = string.replace(new RegExp("[" + this.decimalSymbol + "]", "g"), ".");
    }
    return parseFloat(string);
},

parseLocaleFloat : function (string, decimalSymbol, groupingSymbol) {
    if (string == null) return Number.NaN;
    if (!decimalSymbol) decimalSymbol = isc.NumberUtil.decimalSymbol;
    if (!groupingSymbol) groupingSymbol = isc.NumberUtil.groupingSymbol;
    string = string.replace(new RegExp("[" + groupingSymbol + "]", "g"), "");
    if (decimalSymbol != ".") {
        string = string.replace(new RegExp("[" + decimalSymbol + "]", "g"), ".");
    }
    return parseFloat(string);
},

parseLocaleInt : function (string, groupingSymbol) {
    if (string == null) return Number.NaN;
    if (!groupingSymbol) groupingSymbol = isc.NumberUtil.groupingSymbol;
    string = string.replace(new RegExp("[" + groupingSymbol + "]", "g"), "");
    return parseInt(string);
},

parseLocaleCurrency : function (string, currencySymbol, decimalSymbol, groupingSymbol) {
    if (string == null) return Number.NaN;
    if (!currencySymbol) currencySymbol = isc.NumberUtil.currencySymbol;
    string = string.replace(new RegExp("[" + currencySymbol + "]", "g"), "");
    return this.parseLocaleFloat(string);
},

//> @classMethod NumberUtil.parseIfNumeric()
//
// If given a numeric string (that is, a non-empty string which converts to a
// number), will return the equivalent integer. Otherwise, returns the
// parameter unchanged. Useful for dealing with values that can be numbers or
// strings, but which you want to coerce to a numeric type if possible.
//
// @param numberOrString (any) the string or number to parse
// @return (any) an integer, if possible, otherwise the input unchanged 
// @visibility internal
//<
// Used for dealing with heights and widths. They can be numbers or strings
// (e.g. "50%" or "*"), and thus are deserialized as strings. But we
// sometimes want to know whether it's "really" a string, or instead a
// "numeric string" like "100". 

parseIfNumeric : function (numberOrString) {
    if (isc.isA.Number(numberOrString)) {
        return numberOrString;
    } else if (isc.isA.nonemptyString(numberOrString)) {
        // Note that we want to return strings with trailing characters (like
        // "100%") unchanged, even though parseInt would produce an integer
        // from them. To check for that, isNaN is probably faster than a
        // regexp.
        if (isNaN(numberOrString)) {
            return numberOrString;
        } else {
            return parseInt(numberOrString, 10);
        }
    } else {
        // If it's neither Number nor String, or an empty String, just return
        // it. An empty string could be parsed to 0, but that's not necessarily
        // what was meant.
        return numberOrString;
    }
}
});

// set the standard formatter for the date prototype to the native browser string
// so 'toFormattedString()' defaults to returning the standard number format string
if (!isc.NumberUtil.formatter) isc.NumberUtil.formatter = "toString";


if (!isc.NumberUtil.localeStringFormatter) 
    isc.NumberUtil.localeStringFormatter = "toString";

