//
// sprintf.js - Version 1.0
//
// See the accompanying file "sprintf.txt" for release notes.
//
// Copyright (C) 2007, Michael W. Hayes.  This software is distributed
// under the terms of the GNU General Public License.
//
// This file is part of sprintf.js.
//
// sprintf.js is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// sprintf.js is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with sprintf.js; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
//

function sprintf()
{
    // This function fetches the next argument from the argument list and
    // returns it, after making sure things are kosher.  The variable "currArg"
    // tracks our current position within the argument list.

    var currArg = 0;

    function getNextArg()
    {
        if (currArg == sprintf.arguments.length)
            throw "sprintf: Too few arguments";

        var nextArg = sprintf.arguments[currArg++];

        if (nextArg == null)
            throw "sprintf: Null argument supplied";

        return nextArg;
    }

    // This function adds padding to a value and returns it.

    function addPadding(conv, value)
    {
        // Pick the character to use for padding.  It's a space, unless the
        // caller specified zero padding, in which case it's a zero, unless
        // the user also specified left alignment, in which case we use a
        // space anyway.

        var padChar = " ";

        if (conv.zeroPadding) {
            padChar = "0";
            if (conv.leftAlign)
                padChar = " ";
        }

        // Build up the padding string.

        var padding = "";

        for (var i = 0; i < conv.width - value.length; i++)
            padding += padChar;

        // Now join the padding onto the existing value and return it.

        if (conv.leftAlign)
            return value + padding;
        else
            return padding + value;
    }

    // This function performs thousands separation on the string that is
    // passed in, which must contain a formatted number.

    function separateThousands(value)
    {
        // Look for the decimal point, if present; we only want to separate
        // digits that come before it.

        var dotPos = value.indexOf(".");
        if (dotPos == -1)
            dotPos = value.length;

        // Look for a group of 4 digits or more -- it's the only thing that
        // could conceivably need separating.

        var match = /\d{4,}/.exec(value);
        if (match == null)
            return value;

        // We found 4 digits or more, but we're only interested if they come
        // before the decimal point.

        var endPos = match.index + match[0].length;
        if (endPos > dotPos)
            return value;

        // Looks good.  Insert the thousands separator before every group of
        // 3 digits, starting from the end and working backward.

        for (var i = endPos - 3; i > match.index; i -= 3)
            value = value.slice(0, i) + sprintf.thousandSeparator +
                    value.slice(i);

        return value;
    }

    // This function tries to make a number out of the value given to it.
    // A numeric value is returned as-is, while a string value is converted
    // to a number if possible.  An exception is thrown if the value is not
    // a number or a convertible string.

    function forceToNumber(value)
    {
        switch (typeof value) {
            case "number":
                return value;
            case "string":
                value = Number(value);
                if (isNaN(value))
                    throw "sprintf: Numeric argument required";
                return value;
            default:
                throw "sprintf: Numeric argument required";
        }
    }

    // Same as forceToNumber(), but throws an exception if the value is not
    // an integer.

    function forceToInteger(value)
    {
        value = forceToNumber(value);

        if (value != Math.floor(value))
            throw "sprintf: Integer argument required";

        return value;
    }

    // This function does the conversion required by the %f specifier.

    function fConvert(conv)
    {
        // Figure out the precision to use.  If the caller didn't specify it
        // then default to 6.  If the specifier is %g or %G then choose a
        // precision that gives the correct number of significant digits.

        var precision = 6;

        if (conv.havePrecision)
            precision = conv.precision;

        if (conv.specifier == "g" || conv.specifier == "G") {
            if (conv.arg == 0)
                precision = 0;
            else {
                var log10 = Math.floor(Math.log(conv.arg) * Math.LOG10E);
                precision = Math.max(precision - log10 - 1, 0);
           }
        }

        return conv.arg.toFixed(precision);
    }

    // This function does the conversion required by the %e and %E specifiers.

    function eConvert(conv)
    {
        // Figure out the precision to use.  If the caller didn't specify it
        // then default to 6.  If the specifier is %g or %G then knock one
        // off the precision, but don't let it go below zero.

        var precision = 6;

        if (conv.havePrecision)
            precision = conv.precision;

        if (conv.specifier == "g" || conv.specifier == "G")
            precision = Math.max(precision - 1, 0);

        // Do the conversion, and force the "e" to upper case if required.

        var result = conv.arg.toExponential(precision);

        if (conv.specifier == "E" || conv.specifier == "G")
            result = result.toUpperCase();

        // The exponent has to have at least two digits, but toExponential()
        // will use just one when it can, so we have to put in the extra zero
        // by ourselves.

        result = result.replace(/(\D)(\d)$/, "$10$2");

        return result;
    }

    // This function does the conversion required by the %g and %G specifiers.

    function gConvert(conv)
    {
        // Start by getting the result of the %e conversion.

        var result = eConvert(conv);

        // Now grab the exponent from the end.

        var match    = /([+-]\d+)$/.exec(result);
        var exponent = Number(match[1]);

        // If the exponent is below -4 or at least as big as the precision
        // (without allowing the precision to be zero), then we'll stick with
        // %e, otherwise we'll switch to %f.

        if (exponent < -4 || exponent >= Math.max(conv.precision, 1)) {

            // We're sticking with %e.  Remove any trailing zeroes, and remove
            // the decimal point if there are no digits following it.

            result = result.replace(/0+([eE])/, "$1");
            result = result.replace(/\.([eE])/, "$1");

        } else {

            // We're switching to %f.

            result = fConvert(conv);

            // Remove any trailing zeroes, and remove the decimal point if
            // there are no digits following it.

            if (result.indexOf(".") != -1) {
                result = result.replace(/0+$/, "");
                result = result.replace(/\.$/, "");
            }
        }

        return result;
    }

    // This monstrous regular expression matches the conversions, capturing
    // each conversion in six parts, as laid out in the comment below.

    var re =
/%(\d+\$|)([-+ \'#0]*)(\d*|\*)(\.\d*|\.\*|)([lLhJqtzZ]*)([bcCdeEfgGiosSuxX%])/g;
//(Argpos)(Flags     )(Width )(Precision  )(Size       )(Conversion         )

    re.lastIndex = 0;

    // Chunks of the formatted result are accumulated here, and later joined up
    // into a single string that gets returned.

    var chunks = [];

    // These flag tracks whether or not positioned arguments are being used.
    // It starts off as "null" since we won't know until we look at the first
    // conversion.

    var usingPositions = null;

    // The argument position to use, if any, goes here.

    var position;

    // Our previous and current positions within the format string.

    var currIndex = 0;
    var prevIndex = 0;

    // Get the format string from the argument list.

    var format = getNextArg();

    // Loop through the format string looking for conversions, perform them,
    // and accumulate the converted results in the "chunks" array, along with
    // any blocks of literal text found between the conversions.

    for (;;) {

        // Find the next conversion, if there is one.

        var capture = re.exec(format);

        if (capture == null)
            currIndex = format.length;
        else
            currIndex = capture.index;

        // If we have walked past the end of the previous conversion then there
        // is some literal text between the two conversions.  Slice out the
        // text and save it.

        if (currIndex > prevIndex) {
            var literal = format.slice(prevIndex, currIndex);
            chunks.push(literal);
        }

        // If we didn't find a conversion then we're done.

        if (capture == null)
            break;

        // Remember where this conversion ends.

        prevIndex = currIndex + capture[0].length;

        // Create an object named "conv" that will hold a description of the
        // conversion.

        var conv = new Object();

        // The first capture contains the optional argument position.  If the
        // argument is positioned, make sure the position is valid.  The first
        // conversion sets the tone for the rest of them -- if it's positioned,
        // then the rest must be, too; if not, then none of the others are
        // allowed to be, either.

        if (capture[1] == "") {
            // No position given.
            if (usingPositions == null)
                usingPositions = false;
            else if (usingPositions)
                throw "sprintf: Argument position required";
        } else {
            // Position given.
            if (usingPositions == null)
                usingPositions = true;
            else if (!usingPositions)
                throw "sprintf: Argument position prohibited";

            position = Number(capture[1].slice(0, -1));
            if (position < 1 || position >= sprintf.arguments.length)
                throw "sprintf: Illegal argument position";
        }

        // The second capture contains the conversion flags.  Figure out
        // which flags were supplied.

        conv.leftAlign         = capture[2].indexOf("-") != -1;
        conv.showPlus          = capture[2].indexOf("+") != -1;
        conv.showPlusAsBlank   = capture[2].indexOf(" ") != -1;
        conv.separateThousands = capture[2].indexOf("'") != -1;
        conv.leadingZero       = capture[2].indexOf("#") != -1;
        conv.trailingDecimal   = capture[2].indexOf("#") != -1;
        conv.zeroPadding       = capture[2].indexOf("0") != -1;

        // The third capture contains the field width.  If it contains "*"
        // then the width comes from the argument list, and, if it's negative,
        // it is forced to zero and implies left alignment.

        conv.width = 0;

        if (capture[3] == "*") {
            conv.width = forceToInteger(getNextArg());
            if (conv.width < 0) {
                conv.width = 0;
                conv.leftAlign = true;
            }
        } else if (capture[3] != "")
            conv.width = Number(capture[3]);

        // The fourth capture contains the precision.  If it contains "*" then
        // the precision comes from the argument list, and is ignored if it's
        // negative.

        conv.havePrecision = capture[4].length > 0;
        conv.precision = 0;

        if (capture[4] == ".*") {
            conv.precision = forceToInteger(getNextArg());
            if (conv.precision < 0) {
                conv.precision = 0;
                conv.havePrecision = false;
            }
        } else
            conv.precision = Number(capture[4].slice(1));

        // The fifth capture contains size flags, which we ignore.

        // The sixth capture cointains the conversion specifier.  Classify it.

        conv.specifier = capture[6];

        conv.isNumeric = "bdeEfgGiouxX".indexOf(conv.specifier) != -1;
        conv.isFloat   = "f"           .indexOf(conv.specifier) != -1;
        conv.isInteger = "bdiouxX"     .indexOf(conv.specifier) != -1;
        conv.isOctal   = "o"           .indexOf(conv.specifier) != -1;
        conv.isDecimal = "diof"        .indexOf(conv.specifier) != -1;
        conv.isHex     = "xX"          .indexOf(conv.specifier) != -1;

        // Get the argument to be converted.  If the specifier is % then we
        // don't need one.

        if (conv.specifier != "%")
            if (usingPositions)
                conv.arg = sprintf.arguments[position];
            else
                conv.arg = getNextArg();

        // If the conversion is numeric, make sure the argument is, too.

        if (conv.isNumeric)
            conv.arg = forceToNumber(conv.arg);

        // For a numeric argument, make a note of whether it's negative, and,
        // if so, make it positive -- we'll deal with the sign later.

        if (conv.isNumeric && conv.arg < 0) {
            conv.isNegative = true;
            conv.arg = -conv.arg;
        } else
            conv.isNegative = false;

        // Everything looks good, so do the conversion.  The result of the
        // conversion goes into "chunk".

        var chunk;

        switch (conv.specifier) {

            case "d":
            case "i":
            case "u":
                chunk = conv.arg.toFixed(0);
                break;

            case "b":
                chunk = conv.arg.toString(2);
                break;

            case "o":
                chunk = conv.arg.toString(8);
                break;

            case "x":
                chunk = conv.arg.toString(16);
                break;

            case "X":
                chunk = conv.arg.toString(16).toUpperCase();
                break;

            case "c":
            case "C":
                chunk = conv.arg.charAt(0);
                break;

            case "s":
            case "S":
                if (conv.havePrecision)
                    chunk = conv.arg.slice(0, conv.precision);
                else
                    chunk = conv.arg;
                break;

            case "f":
                chunk = fConvert(conv);
                break;

            case "e":
            case "E":
                chunk = eConvert(conv);
                break;

            case "g":
            case "G":
                chunk = gConvert(conv);
                break;

            case "%":
                chunk = "%";
                break;

            default:
                throw "sprintf: Bug in sprintf";
                break;
        }

        // Handle the precision for an integer.

        if (conv.isInteger && conv.havePrecision) {
            if (conv.precision == 0 && chunk == "0")
                chunk = "";
            while (chunk.length < conv.precision)
                chunk = "0" + chunk;
        }

        // Add a decimal point to the end of a fractionless number,
        // if requested and appropriate.

        if (conv.isFloat && conv.trailingDecimal && chunk.indexOf(".") == -1)
            chunk += ".";

        // Determine how much padding is required, if any.

        conv.paddingRequired = conv.width > chunk.length;

        // Add zero padding now if requested.

        if (conv.paddingRequired && conv.zeroPadding)
            chunk = addPadding(conv, chunk);

        // If the conversion is octal and a leading zero is requested, make
        // sure it's there.

        if (conv.isOctal && conv.leadingZero && chunk.charAt(0) != "0")
            chunk = "0" + chunk;

        // If the conversion is hex and a leading "0x" or "0X" is requested,
        // make sure it's there.

        if (conv.isHex && conv.leadingZero && Number(chunk) != 0)
            if (conv.specifier == "x")
                chunk = "0x" + chunk;
            else
                chunk = "0X" + chunk;

        // Prepend the sign, if required.

        if (conv.isNumeric) {

            // Figure out what the sign character should be, if anything.

            var sign = "";

            if (conv.isNegative)
                sign = "-";
            else if (conv.showPlus)
                sign = "+";
            else if (conv.showPlusAsBlank)
                sign = " ";

            if (sign != "") {

                // There is a sign, and it has to be prepended to the number.
                // If the leading digit is zero, then we might replace it with
                // the sign.

                if (chunk.charAt(0) == "0") {

                    // The leading digit is zero.  Replace it with the sign,
                    // unless the entire number is just zero, or unless the
                    // the number is an integer and replacing the leading zero
                    // would drop the precision too low.

                    var replaceZero = true;

                    if (chunk == "0")
                        replaceZero = false;

                    if (conv.isInteger && conv.havePrecision &&
                            chunk.length == conv.precision)
                        replaceZero = false;

                    // If we've decided to replace the zero, slice it off.

                    if (replaceZero)
                        chunk = chunk.slice(1);
                }

                // Stick the sign in front of the number.

                chunk = sign + chunk;
            }
        }

        // Now apply thousands separation, if requested and appropriate.

        if (conv.separateThousands && conv.isDecimal)
            chunk = separateThousands(chunk);

        // Add blank padding now if requested.

        if (conv.paddingRequired && !conv.zeroPadding)
            chunk = addPadding(conv, chunk);

        // We're finished with this chunk.

        chunks.push(chunk);
    }

    // Make sure we consumed all of the arguments.

    if (!usingPositions && currArg < sprintf.arguments.length)
        throw "sprintf: Too many arguments";

    // The final result is created by joining the chunks together.

    var result = chunks.join("");

    return result;
}

//
// The character to use for thousands separation is stored here.  If you need
// a character other than the default, which is a comma, just assign it to this
// property before calling sprintf().
//
sprintf.thousandSeparator = ",";

//
// This function works the same as sprintf() but shows the resulting formatted
// string using alert() before returning it.
//
function alertf()
{
    var result = sprintf.apply(null, arguments);
    alert(result);
    return result;
}
