text:
/**
 * @fileoverview
 *
 * JsWorld
 *
 *
Javascript library for localised formatting and parsing of:
 *

 *
Numbers
 *
Dates and times
 *
Currency
 *

 *
 *
The library classes are configured with standard POSIX locale definitions
 * derived from Unicode's Common Locale Data Repository (CLDR).
 *
 *
Website: JsWorld
 *
 * @author Vladimir Dzhuvinov
 * @version 2.5 (2011-12-23)
 */

/**
 * @namespace Namespace container for the JsWorld library objects.
 */
jsworld = {};

/**
 * @function
 *
 * @description Formats a JavaScript Date object as an ISO-8601 date/time
 * string.
 *
 * @param {Date} [d] A valid JavaScript Date object. If undefined the
 *        current date/time will be used.
 * @param {Boolean} [withTZ] Include timezone offset, default false.
 *
 * @returns {String} The date/time formatted as YYYY-MM-DD HH:MM:SS.
 */
jsworld.formatIsoDateTime = function (d, withTZ) {

    if (typeof d === "undefined")
        d = new Date(); // now

    if (typeof withTZ === "undefined")
        withTZ = false;

    var s = jsworld.formatIsoDate(d) + " " + jsworld.formatIsoTime(d);

    if (withTZ) {

        var diff = d.getHours() - d.getUTCHours();
        var hourDiff = Math.abs(diff);

        var minuteUTC = d.getUTCMinutes();
        var minute = d.getMinutes();

        if (minute != minuteUTC && minuteUTC < 30 && diff < 0)
            hourDiff--;

        if (minute != minuteUTC && minuteUTC > 30 && diff > 0)
            hourDiff--;

        var minuteDiff;
        if (minute != minuteUTC)
            minuteDiff = ":30";
        else
            minuteDiff = ":00";

        var timezone;
        if (hourDiff < 10)
            timezone = "0" + hourDiff + minuteDiff;

        else
            timezone = "" + hourDiff + minuteDiff;

        if (diff < 0)
            timezone = "-" + timezone;

        else
            timezone = "+" + timezone;

        s = s + timezone;
    }

    return s;
};

/**
 * @function
 *
 * @description Formats a JavaScript Date object as an ISO-8601 date string.
 *
 * @param {Date} [d] A valid JavaScript Date object. If undefined the current
 *        date will be used.
 *
 * @returns {String} The date formatted as YYYY-MM-DD.
 */
jsworld.formatIsoDate = function (d) {

    if (typeof d === "undefined")
        d = new Date(); // now

    var year = d.getFullYear();
    var month = d.getMonth() + 1;
    var day = d.getDate();

    return year + "-" + jsworld._zeroPad(month, 2) + "-" + jsworld._zeroPad(day, 2);
};

/**
 * @function
 *
 * @description Formats a JavaScript Date object as an ISO-8601 time string.
 *
 * @param {Date} [d] A valid JavaScript Date object. If undefined the current
 *        time will be used.
 *
 * @returns {String} The time formatted as HH:MM:SS.
 */
jsworld.formatIsoTime = function (d) {

    if (typeof d === "undefined")
        d = new Date(); // now

    var hour = d.getHours();
    var minute = d.getMinutes();
    var second = d.getSeconds();

    return jsworld._zeroPad(hour, 2) + ":" + jsworld._zeroPad(minute, 2) + ":" + jsworld._zeroPad(second, 2);
};

/**
 * @function
 *
 * @description Parses an ISO-8601 formatted date/time string to a JavaScript
 * Date object.
 *
 * @param {String} isoDateTimeVal An ISO-8601 formatted date/time string.
 *
 *
Accepted formats:
 *
 *

 *
YYYY-MM-DD HH:MM:SS
 *
YYYYMMDD HHMMSS
 *
YYYY-MM-DD HHMMSS
 *
YYYYMMDD HH:MM:SS
 *

 *
 * @returns {Date} The corresponding Date object.
 *
 * @throws Error on a badly formatted date/time string or on a invalid date.
 */
jsworld.parseIsoDateTime = function (isoDateTimeVal) {

    if (typeof isoDateTimeVal != "string")
        throw "Error: The parameter must be a string";

    // First, try to match "YYYY-MM-DD HH:MM:SS" format
    var matches = isoDateTimeVal.match(/^(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)/);

    // If unsuccessful, try to match "YYYYMMDD HHMMSS" format
    if (matches === null)
        matches = isoDateTimeVal.match(/^(\d\d\d\d)(\d\d)(\d\d)[T ](\d\d)(\d\d)(\d\d)/);

    // ... try to match "YYYY-MM-DD HHMMSS" format
    if (matches === null)
        matches = isoDateTimeVal.match(/^(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d)(\d\d)(\d\d)/);

    // ... try to match "YYYYMMDD HH:MM:SS" format
    if (matches === null)
        matches = isoDateTimeVal.match(/^(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)/);

    // Report bad date/time string
    if (matches === null)
        throw "Error: Invalid ISO-8601 date/time string";

    // Force base 10 parse int as some values may have leading zeros!
    // (to avoid implicit octal base conversion)
    var year = parseInt(matches[1], 10);
    var month = parseInt(matches[2], 10);
    var day = parseInt(matches[3], 10);

    var hour = parseInt(matches[4], 10);
    var mins = parseInt(matches[5], 10);
    var secs = parseInt(matches[6], 10);

    // Simple value range check, leap years not checked
    // Note: the originial ISO time spec for leap hours (24:00:00) and seconds (00:00:60) is not supported
    if (month < 1 || month > 12 ||
        day < 1 || day > 31 ||
        hour < 0 || hour > 23 ||
        mins < 0 || mins > 59 ||
        secs < 0 || secs > 59)

        throw "Error: Invalid ISO-8601 date/time value";

    var d = new Date(year, month - 1, day, hour, mins, secs);

    // Check if the input date was valid
    // (JS Date does automatic forward correction)
    if (d.getDate() != day || d.getMonth() + 1 != month)
        throw "Error: Invalid date";

    return d;
};

/**
 * @function
 *
 * @description Parses an ISO-8601 formatted date string to a JavaScript
 * Date object.
 *
 * @param {String} isoDateVal An ISO-8601 formatted date string.
 *
 *
Accepted formats:
 *
 *

 *
YYYY-MM-DD
 *
YYYYMMDD
 *

 *
 * @returns {Date} The corresponding Date object.
 *
 * @throws Error on a badly formatted date string or on a invalid date.
 */
jsworld.parseIsoDate = function (isoDateVal) {

    if (typeof isoDateVal != "string")
        throw "Error: The parameter must be a string";

    // First, try to match "YYYY-MM-DD" format
    var matches = isoDateVal.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/);

    // If unsuccessful, try to match "YYYYMMDD" format
    if (matches === null)
        matches = isoDateVal.match(/^(\d\d\d\d)(\d\d)(\d\d)/);

    // Report bad date/time string
    if (matches === null)
        throw "Error: Invalid ISO-8601 date string";

    // Force base 10 parse int as some values may have leading zeros!
    // (to avoid implicit octal base conversion)
    var year = parseInt(matches[1], 10);
    var month = parseInt(matches[2], 10);
    var day = parseInt(matches[3], 10);

    // Simple value range check, leap years not checked
    if (month < 1 || month > 12 ||
        day < 1 || day > 31)

        throw "Error: Invalid ISO-8601 date value";

    var d = new Date(year, month - 1, day);

    // Check if the input date was valid
    // (JS Date does automatic forward correction)
    if (d.getDate() != day || d.getMonth() + 1 != month)
        throw "Error: Invalid date";

    return d;
};

/**
 * @function
 *
 * @description Parses an ISO-8601 formatted time string to a JavaScript
 * Date object.
 *
 * @param {String} isoTimeVal An ISO-8601 formatted time string.
 *
 *
Accepted formats:
 *
 *

 *
HH:MM:SS
 *
HHMMSS
 *

 *
 * @returns {Date} The corresponding Date object, with year, month and day set
 *          to zero.
 *
 * @throws Error on a badly formatted time string.
 */
jsworld.parseIsoTime = function (isoTimeVal) {

    if (typeof isoTimeVal != "string")
        throw "Error: The parameter must be a string";

    // First, try to match "HH:MM:SS" format
    var matches = isoTimeVal.match(/^(\d\d):(\d\d):(\d\d)/);

    // If unsuccessful, try to match "HHMMSS" format
    if (matches === null)
        matches = isoTimeVal.match(/^(\d\d)(\d\d)(\d\d)/);

    // Report bad date/time string
    if (matches === null)
        throw "Error: Invalid ISO-8601 date/time string";

    // Force base 10 parse int as some values may have leading zeros!
    // (to avoid implicit octal base conversion)
    var hour = parseInt(matches[1], 10);
    var mins = parseInt(matches[2], 10);
    var secs = parseInt(matches[3], 10);

    // Simple value range check, leap years not checked
    if (hour < 0 || hour > 23 ||
        mins < 0 || mins > 59 ||
        secs < 0 || secs > 59)

        throw "Error: Invalid ISO-8601 time value";

    return new Date(0, 0, 0, hour, mins, secs);
};

/**
 * @private
 *
 * @description Trims leading and trailing whitespace from a string.
 *
 *
Used non-regexp the method from http://blog.stevenlevithan.com/archives/faster-trim-javascript
 *
 * @param {String} str The string to trim.
 *
 * @returns {String} The trimmed string.
 */
jsworld._trim = function (str) {

    var whitespace = ' \n\r\t\f\x0b\xa0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000';

    for (var i = 0; i < str.length; i++) {

        if (whitespace.indexOf(str.charAt(i)) === -1) {
            str = str.substring(i);
            break;
        }
    }

    for (i = str.length - 1; i >= 0; i--) {
        if (whitespace.indexOf(str.charAt(i)) === -1) {
            str = str.substring(0, i + 1);
            break;
        }
    }

    return whitespace.indexOf(str.charAt(0)) === -1 ? str : '';
};

/**
 * @private
 *
 * @description Returns true if the argument represents a decimal number.
 *
 * @param {Number|String} arg The argument to test.
 *
 * @returns {Boolean} true if the argument represents a decimal number,
 *          otherwise false.
 */
jsworld._isNumber = function (arg) {

    if (typeof arg == "number")
        return true;

    if (typeof arg != "string")
        return false;

    // ensure string
    var s = arg + "";

    return (/^-?(\d+|\d*\.\d+)$/).test(s);
};

/**
 * @private
 *
 * @description Returns true if the argument represents a decimal integer.
 *
 * @param {Number|String} arg The argument to test.
 *
 * @returns {Boolean} true if the argument represents an integer, otherwise
 *          false.
 */
jsworld._isInteger = function (arg) {

    if (typeof arg != "number" && typeof arg != "string")
        return false;

    // convert to string
    var s = arg + "";

    return (/^-?\d+$/).test(s);
};

/**
 * @private
 *
 * @description Returns true if the argument represents a decimal float.
 *
 * @param {Number|String} arg The argument to test.
 *
 * @returns {Boolean} true if the argument represents a float, otherwise false.
 */
jsworld._isFloat = function (arg) {

    if (typeof arg != "number" && typeof arg != "string")
        return false;

    // convert to string
    var s = arg + "";

    return (/^-?\.\d+?$/).test(s);
};

/**
 * @private
 *
 * @description Checks if the specified formatting option is contained
 * within the options string.
 *
 * @param {String} option The option to search for.
 * @param {String} optionsString The options string.
 *
 * @returns {Boolean} true if the flag is found, else false
 */
jsworld._hasOption = function (option, optionsString) {

    if (typeof option != "string" || typeof optionsString != "string")
        return false;

    if (optionsString.indexOf(option) != -1)
        return true;
    else
        return false;
};

/**
 * @private
 *
 * @description String replacement function.
 *
 * @param {String} s The string to work on.
 * @param {String} target The string to search for.
 * @param {String} replacement The replacement.
 *
 * @returns {String} The new string.
 */
jsworld._stringReplaceAll = function (s, target, replacement) {

    var out;

    if (target.length == 1 && replacement.length == 1) {
        // simple char/char case somewhat faster
        out = "";

        for (var i = 0; i < s.length; i++) {

            if (s.charAt(i) == target.charAt(0))
                out = out + replacement.charAt(0);
            else
                out = out + s.charAt(i);
        }

        return out;
    }
    else {
        // longer target and replacement strings
        out = s;

        var index = out.indexOf(target);

        while (index != -1) {

            out = out.replace(target, replacement);

            index = out.indexOf(target);
        }

        return out;
    }
};

/**
 * @private
 *
 * @description Tests if a string starts with the specified substring.
 *
 * @param {String} testedString The string to test.
 * @param {String} sub The string to match.
 *
 * @returns {Boolean} true if the test succeeds.
 */
jsworld._stringStartsWith = function (testedString, sub) {

    if (testedString.length < sub.length)
        return false;

    for (var i = 0; i < sub.length; i++) {
        if (testedString.charAt(i) != sub.charAt(i))
            return false;
    }

    return true;
};

/**
 * @private
 *
 * @description Gets the requested precision from an options string.
 *
 *
Example: ".3" returns 3 decimal places precision.
 *
 * @param {String} optionsString The options string.
 *
 * @returns {integer Number} The requested precision, -1 if not specified.
 */
jsworld._getPrecision = function (optionsString) {

    if (typeof optionsString != "string")
        return -1;

    var m = optionsString.match(/\.(\d)/);
    if (m)
        return parseInt(m[1], 10);
    else
        return -1;
};

/**
 * @private
 *
 * @description Takes a decimal numeric amount (optionally as string) and
 * returns its integer and fractional parts packed into an object.
 *
 * @param {Number|String} amount The amount, e.g. "123.45" or "-56.78"
 *
 * @returns {object} Parsed amount object with properties:
 *         {String} integer  : the integer part
 *         {String} fraction : the fraction part
 */
jsworld._splitNumber = function (amount) {

    if (typeof amount == "number")
        amount = amount + "";

    var obj = {};

    // remove negative sign
    if (amount.charAt(0) == "-")
        amount = amount.substring(1);

    // split amount into integer and decimal parts
    var amountParts = amount.split(".");
    if (!amountParts[1])
        amountParts[1] = ""; // we need "" instead of null

    obj.integer = amountParts[0];
    obj.fraction = amountParts[1];

    return obj;
};

/**
 * @private
 *
 * @description Formats the integer part using the specified grouping
 * and thousands separator.
 *
 * @param {String} intPart The integer part of the amount, as string.
 * @param {String} grouping The grouping definition.
 * @param {String} thousandsSep The thousands separator.
 *
 * @returns {String} The formatted integer part.
 */
jsworld._formatIntegerPart = function (intPart, grouping, thousandsSep) {

    // empty separator string? no grouping?
    // -> return immediately with no formatting!
    if (thousandsSep == "" || grouping == "-1")
        return intPart;

    // turn the semicolon-separated string of integers into an array
    var groupSizes = grouping.split(";");

    // the formatted output string
    var out = "";

    // the intPart string position to process next,
    // start at string end, e.g. "10000000<starts here"
    var pos = intPart.length;

    // process the intPart string backwards
    //     "1000000000"
    //            <---\ direction
    var size;

    while (pos > 0) {

        // get next group size (if any, otherwise keep last)
        if (groupSizes.length > 0)
            size = parseInt(groupSizes.shift(), 10);

        // int parse error?
        if (isNaN(size))
            throw "Error: Invalid grouping";

        // size is -1? -> no more grouping, so just copy string remainder
        if (size == -1) {
            out = intPart.substring(0, pos) + out;
            break;
        }

        pos -= size; // move to next sep. char. position

        // position underrun? -> just copy string remainder
        if (pos < 1) {
            out = intPart.substring(0, pos + size) + out;
            break;
        }

        // extract group and apply sep. char.
        out = thousandsSep + intPart.substring(pos, pos + size) + out;
    }

    return out;
};

/**
 * @private
 *
 * @description Formats the fractional part to the specified decimal
 * precision.
 *
 * @param {String} fracPart The fractional part of the amount
 * @param {integer Number} precision The desired decimal precision
 *
 * @returns {String} The formatted fractional part.
 */
jsworld._formatFractionPart = function (fracPart, precision) {

    // append zeroes up to precision if necessary
    for (var i = 0; fracPart.length < precision; i++)
        fracPart = fracPart + "0";

    return fracPart;
};

/**
 * @private
 *
 * @desription Converts a number to string and pad it with leading zeroes if the
 * string is shorter than length.
 *
 * @param {integer Number} number The number value subjected to selective padding.
 * @param {integer Number} length If the number has fewer digits than this length
 *        apply padding.
 *
 * @returns {String} The formatted string.
 */
jsworld._zeroPad = function (number, length) {

    // ensure string
    var s = number + "";

    while (s.length < length)
        s = "0" + s;

    return s;
};

/**
 * @private
 * @description Converts a number to string and pads it with leading spaces if
 * the string is shorter than length.
 *
 * @param {integer Number} number The number value subjected to selective padding.
 * @param {integer Number} length If the number has fewer digits than this length
 *        apply padding.
 *
 * @returns {String} The formatted string.
 */
jsworld._spacePad = function (number, length) {

    // ensure string
    var s = number + "";

    while (s.length < length)
        s = " " + s;

    return s;
};

/**
 * @class
 * Represents a POSIX-style locale with its numeric, monetary and date/time
 * properties. Also provides a set of locale helper methods.
 *
 *
The locale properties follow the POSIX standards:
 *
 *

 *
POSIX LC_NUMERIC
 *
POSIX LC_MONETARY
 *
POSIX LC_TIME
 *

 *
 * @public
 * @constructor
 * @description Creates a new locale object (POSIX-style) with the specified
 * properties.
 *
 * @param {object} properties An object containing the raw locale properties:
 *
 *        @param {String} properties.decimal_point
 *
 *        A string containing the symbol that shall be used as the decimal
 *        delimiter (radix character) in numeric, non-monetary formatted
 *        quantities. This property cannot be omitted and cannot be set to the
 *        empty string.
 *
 *
 *        @param {String} properties.thousands_sep
 *
 *        A string containing the symbol that shall be used as a separator for
 *        groups of digits to the left of the decimal delimiter in numeric,
 *        non-monetary formatted monetary quantities.
 *
 *
 *        @param {String} properties.grouping
 *
 *        Defines the size of each group of digits in formatted non-monetary
 *        quantities. The operand is a sequence of integers separated by
 *        semicolons. Each integer specifies the number of digits in each group,
 *        with the initial integer defining the size of the group immediately
 *        preceding the decimal delimiter, and the following integers defining
 *        the preceding groups. If the last integer is not -1, then the size of
 *        the previous group (if any) shall be repeatedly used for the
 *        remainder of the digits. If the last integer is -1, then no further
 *        grouping shall be performed.
 *
 *
 *        @param {String} properties.int_curr_symbol
 *
 *        The first three letters signify the ISO-4217 currency code,
 *        the fourth letter is the international symbol separation character
 *        (normally a space).
 *
 *
 *        @param {String} properties.currency_symbol
 *
 *        The local shorthand currency symbol, e.g. "$" for the en_US locale
 *
 *
 *        @param {String} properties.mon_decimal_point
 *
 *        The symbol to be used as the decimal delimiter (radix character)
 *
 *
 *        @param {String} properties.mon_thousands_sep
 *
 *        The symbol to be used as a separator for groups of digits to the
 *        left of the decimal delimiter.
 *
 *
 *        @param {String} properties.mon_grouping
 *
 *        A string that defines the size of each group of digits. The
 *        operand is a sequence of integers separated by semicolons (";").
 *        Each integer specifies the number of digits in each group, with the
 *        initial integer defining the size of the group preceding the
 *        decimal delimiter, and the following integers defining the
 *        preceding groups. If the last integer is not -1, then the size of
 *        the previous group (if any) must be repeatedly used for the
 *        remainder of the digits. If the last integer is -1, then no
 *        further grouping is to be performed.
 *
 *
 *        @param {String} properties.positive_sign
 *
 *        The string to indicate a non-negative monetary amount.
 *
 *
 *        @param {String} properties.negative_sign
 *
 *        The string to indicate a negative monetary amount.
 *
 *
 *        @param {integer Number} properties.frac_digits
 *
 *        An integer representing the number of fractional digits (those to
 *        the right of the decimal delimiter) to be written in a formatted
 *        monetary quantity using currency_symbol.
 *
 *
 *        @param {integer Number} properties.int_frac_digits
 *
 *        An integer representing the number of fractional digits (those to
 *        the right of the decimal delimiter) to be written in a formatted
 *        monetary quantity using int_curr_symbol.
 *
 *
 *        @param {integer Number} properties.p_cs_precedes
 *
 *        An integer set to 1 if the currency_symbol precedes the value for a
 *        monetary quantity with a non-negative value, and set to 0 if the
 *        symbol succeeds the value.
 *
 *
 *        @param {integer Number} properties.n_cs_precedes
 *
 *        An integer set to 1 if the currency_symbol precedes the value for a
 *        monetary quantity with a negative value, and set to 0 if the symbol
 *        succeeds the value.
 *
 *
 *        @param {integer Number} properties.p_sep_by_space
 *
 *        Set to a value indicating the separation of the currency_symbol,
 *        the sign string, and the value for a non-negative formatted monetary
 *        quantity:
 *
 *
0 No space separates the currency symbol and value.

 *
 *
1 If the currency symbol and sign string are adjacent, a space
 *                  separates them from the value; otherwise, a space separates
 *                  the currency symbol from the value.

 *
 *
2 If the currency symbol and sign string are adjacent, a space
 *                  separates them; otherwise, a space separates the sign string
 *                  from the value.

 *
 *
 *        @param {integer Number} properties.n_sep_by_space
 *
 *        Set to a value indicating the separation of the currency_symbol,
 *        the sign string, and the value for a negative formatted monetary
 *        quantity. Rules same as for p_sep_by_space.
 *
 *
 *        @param {integer Number} properties.p_sign_posn
 *
 *        An integer set to a value indicating the positioning of the
 *        positive_sign for a monetary quantity with a non-negative value:
 *
 *
0 Parentheses enclose the quantity and the currency_symbol.

 *
 *
1 The sign string precedes the quantity and the currency_symbol.

 *
 *
2 The sign string succeeds the quantity and the currency_symbol.

 *
 *
3 The sign string precedes the currency_symbol.

 *
 *
4 The sign string succeeds the currency_symbol.

 *
 *
 *      @param {integer Number} properties.n_sign_posn
 *
 *      An integer set to a value indicating the positioning of the
 *      negative_sign for a negative formatted monetary quantity. Rules same
 *      as for p_sign_posn.
 *
 *
 *      @param {integer Number} properties.int_p_cs_precedes
 *
 *      An integer set to 1 if the int_curr_symbol precedes the value for a
 *      monetary quantity with a non-negative value, and set to 0 if the
 *      symbol succeeds the value.
 *
 *
 *      @param {integer Number} properties.int_n_cs_precedes
 *
 *      An integer set to 1 if the int_curr_symbol precedes the value for a
 *      monetary quantity with a negative value, and set to 0 if the symbol
 *      succeeds the value.
 *
 *
 *      @param {integer Number} properties.int_p_sep_by_space
 *
 *      Set to a value indicating the separation of the int_curr_symbol,
 *      the sign string, and the value for a non-negative internationally
 *      formatted monetary quantity. Rules same as for p_sep_by_space.
 *
 *
 *      @param {integer Number} properties.int_n_sep_by_space
 *
 *      Set to a value indicating the separation of the int_curr_symbol,
 *      the sign string, and the value for a negative internationally
 *      formatted monetary quantity. Rules same as for p_sep_by_space.
 *
 *
 *      @param {integer Number} properties.int_p_sign_posn
 *
 *      An integer set to a value indicating the positioning of the
 *      positive_sign for a positive monetary quantity formatted with the
 *      international format. Rules same as for p_sign_posn.
 *
 *
 *      @param {integer Number} properties.int_n_sign_posn
 *
 *      An integer set to a value indicating the positioning of the
 *      negative_sign for a negative monetary quantity formatted with the
 *      international format. Rules same as for p_sign_posn.
 *
 *
 *        @param {String[] | String} properties.abday
 *
 *        The abbreviated weekday names, corresponding to the %a conversion
 *        specification. The property must be either an array of 7 strings or
 *        a string consisting of 7 semicolon-separated substrings, each
 *        surrounded by double-quotes. The first must be the abbreviated name
 *        of the day corresponding to Sunday, the second the abbreviated name
 *        of the day corresponding to Monday, and so on.
 *
 *
 *        @param {String[] | String} properties.day
 *
 *        The full weekday names, corresponding to the %A conversion
 *        specification. The property must be either an array of 7 strings or
 *        a string consisting of 7 semicolon-separated substrings, each
 *        surrounded by double-quotes. The first must be the full name of the
 *        day corresponding to Sunday, the second the full name of the day
 *        corresponding to Monday, and so on.
 *
 *
 *        @param {String[] | String} properties.abmon
 *
 *        The abbreviated month names, corresponding to the %b conversion
 *        specification. The property must be either an array of 12 strings or
 *        a string consisting of 12 semicolon-separated substrings, each
 *        surrounded by double-quotes. The first must be the abbreviated name
 *        of the first month of the year (January), the second the abbreviated
 *        name of the second month, and so on.
 *
 *
 *        @param {String[] | String} properties.mon
 *
 *        The full month names, corresponding to the %B conversion
 *        specification. The property must be either an array of 12 strings or
 *        a string consisting of 12 semicolon-separated substrings, each
 *        surrounded by double-quotes. The first must be the full name of the
 *        first month of the year (January), the second the full name of the second
 *        month, and so on.
 *
 *
 *        @param {String} properties.d_fmt
 *
 *        The appropriate date representation. The string may contain any
 *        combination of characters and conversion specifications (%<char>).
 *
 *
 *        @param {String} properties.t_fmt
 *
 *        The appropriate time representation. The string may contain any
 *        combination of characters and conversion specifications (%<char>).
 *
 *
 *        @param {String} properties.d_t_fmt
 *
 *        The appropriate date and time representation. The string may contain
 *        any combination of characters and conversion specifications (%<char>).
 *
 *
 *        @param {String[] | String} properties.am_pm
 *
 *        The appropriate representation of the ante-meridiem and post-meridiem
 *        strings, corresponding to the %p conversion specification. The property
 *        must be either an array of 2 strings or a string consisting of 2
 *        semicolon-separated substrings, each surrounded by double-quotes.
 *        The first string must represent the ante-meridiem designation, the
 *        last string the post-meridiem designation.
 *
 *
 * @throws @throws Error on a undefined or invalid locale property.
 */
jsworld.Locale = function (properties) {

    /**
     * @private
     *
     * @description Identifies the class for internal library purposes.
     */
    this._className = "jsworld.Locale";

    /**
     * @private
     *
     * @description Parses a day or month name definition list, which
     * could be a ready JS array, e.g. ["Mon", "Tue", "Wed"...] or
     * it could be a string formatted according to the classic POSIX
     * definition e.g. "Mon";"Tue";"Wed";...
     *
     * @param {String[] | String} namesAn array or string defining
     *        the week/month names.
     * @param {integer Number} expectedItems The number of expected list
     *        items, e.g. 7 for weekdays, 12 for months.
     *
     * @returns {String[]} The parsed (and checked) items.
     *
     * @throws Error on missing definition, unexpected item count or
     *         missing double-quotes.
     */
    this._parseList = function (names, expectedItems) {

        var array = [];

        if (names == null) {
            throw "Names not defined";
        }
        else if (typeof names == "object") {
            // we got a ready array
            array = names;
        }
        else if (typeof names == "string") {
            // we got the names in the classic POSIX form, do parse
            array = names.split(";", expectedItems);

            for (var i = 0; i < array.length; i++) {
                // check for and strip double quotes
                if (array[i][0] == "\"" && array[i][array[i].length - 1] == "\"")
                    array[i] = array[i].slice(1, -1);
                else
                    throw "Missing double quotes";
            }
        }
        else {
            throw "Names must be an array or a string";
        }

        if (array.length != expectedItems)
            throw "Expected " + expectedItems + " items, got " + array.length;

        return array;
    };

    /**
     * @private
     *
     * @description Validates a date/time format string, such as "H:%M:%S".
     * Checks that the argument is of type "string" and is not empty.
     *
     * @param {String} formatString The format string.
     *
     * @returns {String} The validated string.
     *
     * @throws Error on null or empty string.
     */
    this._validateFormatString = function (formatString) {

        if (typeof formatString == "string" && formatString.length > 0)
            return formatString;
        else
            throw "Empty or no string";
    };

    // LC_NUMERIC

    if (properties == null || typeof properties != "object")
        throw "Error: Invalid/missing locale properties";

    if (typeof properties.decimal_point != "string")
        throw "Error: Invalid/missing decimal_point property";

    this.decimal_point = properties.decimal_point;

    if (typeof properties.thousands_sep != "string")
        throw "Error: Invalid/missing thousands_sep property";

    this.thousands_sep = properties.thousands_sep;

    if (typeof properties.grouping != "string")
        throw "Error: Invalid/missing grouping property";

    this.grouping = properties.grouping;

    // LC_MONETARY

    if (typeof properties.int_curr_symbol != "string")
        throw "Error: Invalid/missing int_curr_symbol property";

    if (!/[A-Za-z]{3}.?/.test(properties.int_curr_symbol))
        throw "Error: Invalid int_curr_symbol property";

    this.int_curr_symbol = properties.int_curr_symbol;

    if (typeof properties.currency_symbol != "string")
        throw "Error: Invalid/missing currency_symbol property";

    this.currency_symbol = properties.currency_symbol;

    if (typeof properties.frac_digits != "number" && properties.frac_digits < 0)
        throw "Error: Invalid/missing frac_digits property";

    this.frac_digits = properties.frac_digits;

    // may be empty string/null for currencies with no fractional part
    if (properties.mon_decimal_point === null || properties.mon_decimal_point == "") {

        if (this.frac_digits > 0)
            throw "Error: Undefined mon_decimal_point property";
        else
            properties.mon_decimal_point = "";
    }

    if (typeof properties.mon_decimal_point != "string")
        throw "Error: Invalid/missing mon_decimal_point property";

    this.mon_decimal_point = properties.mon_decimal_point;

    if (typeof properties.mon_thousands_sep != "string")
        throw "Error: Invalid/missing mon_thousands_sep property";

    this.mon_thousands_sep = properties.mon_thousands_sep;

    if (typeof properties.mon_grouping != "string")
        throw "Error: Invalid/missing mon_grouping property";

    this.mon_grouping = properties.mon_grouping;

    if (typeof properties.positive_sign != "string")
        throw "Error: Invalid/missing positive_sign property";

    this.positive_sign = properties.positive_sign;

    if (typeof properties.negative_sign != "string")
        throw "Error: Invalid/missing negative_sign property";

    this.negative_sign = properties.negative_sign;

    if (properties.p_cs_precedes !== 0 && properties.p_cs_precedes !== 1)
        throw "Error: Invalid/missing p_cs_precedes property, must be 0 or 1";

    this.p_cs_precedes = properties.p_cs_precedes;

    if (properties.n_cs_precedes !== 0 && properties.n_cs_precedes !== 1)
        throw "Error: Invalid/missing n_cs_precedes, must be 0 or 1";

    this.n_cs_precedes = properties.n_cs_precedes;

    if (properties.p_sep_by_space !== 0 &&
        properties.p_sep_by_space !== 1 &&
        properties.p_sep_by_space !== 2)
        throw "Error: Invalid/missing p_sep_by_space property, must be 0, 1 or 2";

    this.p_sep_by_space = properties.p_sep_by_space;

    if (properties.n_sep_by_space !== 0 &&
        properties.n_sep_by_space !== 1 &&
        properties.n_sep_by_space !== 2)
        throw "Error: Invalid/missing n_sep_by_space property, must be 0, 1, or 2";

    this.n_sep_by_space = properties.n_sep_by_space;

    if (properties.p_sign_posn !== 0 &&
        properties.p_sign_posn !== 1 &&
        properties.p_sign_posn !== 2 &&
        properties.p_sign_posn !== 3 &&
        properties.p_sign_posn !== 4)
        throw "Error: Invalid/missing p_sign_posn property, must be 0, 1, 2, 3 or 4";

    this.p_sign_posn = properties.p_sign_posn;

    if (properties.n_sign_posn !== 0 &&
        properties.n_sign_posn !== 1 &&
        properties.n_sign_posn !== 2 &&
        properties.n_sign_posn !== 3 &&
        properties.n_sign_posn !== 4)
        throw "Error: Invalid/missing n_sign_posn property, must be 0, 1, 2, 3 or 4";

    this.n_sign_posn = properties.n_sign_posn;

    if (typeof properties.int_frac_digits != "number" && properties.int_frac_digits < 0)
        throw "Error: Invalid/missing int_frac_digits property";

    this.int_frac_digits = properties.int_frac_digits;

    if (properties.int_p_cs_precedes !== 0 && properties.int_p_cs_precedes !== 1)
        throw "Error: Invalid/missing int_p_cs_precedes property, must be 0 or 1";

    this.int_p_cs_precedes = properties.int_p_cs_precedes;

    if (properties.int_n_cs_precedes !== 0 && properties.int_n_cs_precedes !== 1)
        throw "Error: Invalid/missing int_n_cs_precedes property, must be 0 or 1";

    this.int_n_cs_precedes = properties.int_n_cs_precedes;

    if (properties.int_p_sep_by_space !== 0 &&
        properties.int_p_sep_by_space !== 1 &&
        properties.int_p_sep_by_space !== 2)
        throw "Error: Invalid/missing int_p_sep_by_spacev, must be 0, 1 or 2";

    this.int_p_sep_by_space = properties.int_p_sep_by_space;

    if (properties.int_n_sep_by_space !== 0 &&
        properties.int_n_sep_by_space !== 1 &&
        properties.int_n_sep_by_space !== 2)
        throw "Error: Invalid/missing int_n_sep_by_space property, must be 0, 1, or 2";

    this.int_n_sep_by_space = properties.int_n_sep_by_space;

    if (properties.int_p_sign_posn !== 0 &&
        properties.int_p_sign_posn !== 1 &&
        properties.int_p_sign_posn !== 2 &&
        properties.int_p_sign_posn !== 3 &&
        properties.int_p_sign_posn !== 4)
        throw "Error: Invalid/missing int_p_sign_posn property, must be 0, 1, 2, 3 or 4";

    this.int_p_sign_posn = properties.int_p_sign_posn;

    if (properties.int_n_sign_posn !== 0 &&
        properties.int_n_sign_posn !== 1 &&
        properties.int_n_sign_posn !== 2 &&
        properties.int_n_sign_posn !== 3 &&
        properties.int_n_sign_posn !== 4)
        throw "Error: Invalid/missing int_n_sign_posn property, must be 0, 1, 2, 3 or 4";

    this.int_n_sign_posn = properties.int_n_sign_posn;

    // LC_TIME

    if (properties == null || typeof properties != "object")
        throw "Error: Invalid/missing time locale properties";

    // parse the supported POSIX LC_TIME properties

    // abday
    try {
        this.abday = this._parseList(properties.abday, 7);
    }
    catch (error) {
        throw "Error: Invalid abday property: " + error;
    }

    // day
    try {
        this.day = this._parseList(properties.day, 7);
    }
    catch (error) {
        throw "Error: Invalid day property: " + error;
    }

    // abmon
    try {
        this.abmon = this._parseList(properties.abmon, 12);
    } catch (error) {
        throw "Error: Invalid abmon property: " + error;
    }

    // mon
    try {
        this.mon = this._parseList(properties.mon, 12);
    } catch (error) {
        throw "Error: Invalid mon property: " + error;
    }

    // d_fmt
    try {
        this.d_fmt = this._validateFormatString(properties.d_fmt);
    } catch (error) {
        throw "Error: Invalid d_fmt property: " + error;
    }

    // t_fmt
    try {
        this.t_fmt = this._validateFormatString(properties.t_fmt);
    } catch (error) {
        throw "Error: Invalid t_fmt property: " + error;
    }

    // d_t_fmt
    try {
        this.d_t_fmt = this._validateFormatString(properties.d_t_fmt);
    } catch (error) {
        throw "Error: Invalid d_t_fmt property: " + error;
    }

    // am_pm
    try {
        var am_pm_strings = this._parseList(properties.am_pm, 2);
        this.am = am_pm_strings[0];
        this.pm = am_pm_strings[1];
    } catch (error) {
        // ignore empty/null string errors
        this.am = "";
        this.pm = "";
    }

    /**
     * @public
     *
     * @description Returns the abbreviated name of the specified weekday.
     *
     * @param {integer Number} [weekdayNum] An integer between 0 and 6. Zero
     *        corresponds to Sunday, one to Monday, etc. If omitted the
     *        method will return an array of all abbreviated weekday
     *        names.
     *
     * @returns {String | String[]} The abbreviated name of the specified weekday
     *          or an array of all abbreviated weekday names.
     *
     * @throws Error on invalid argument.
     */
    this.getAbbreviatedWeekdayName = function (weekdayNum) {

        if (typeof weekdayNum == "undefined" || weekdayNum === null)
            return this.abday;

        if (!jsworld._isInteger(weekdayNum) || weekdayNum < 0 || weekdayNum > 6)
            throw "Error: Invalid weekday argument, must be an integer [0..6]";

        return this.abday[weekdayNum];
    };

    /**
     * @public
     *
     * @description Returns the name of the specified weekday.
     *
     * @param {integer Number} [weekdayNum] An integer between 0 and 6. Zero
     *        corresponds to Sunday, one to Monday, etc. If omitted the
     *        method will return an array of all weekday names.
     *
     * @returns {String | String[]} The name of the specified weekday or an
     *          array of all weekday names.
     *
     * @throws Error on invalid argument.
     */
    this.getWeekdayName = function (weekdayNum) {

        if (typeof weekdayNum == "undefined" || weekdayNum === null)
            return this.day;

        if (!jsworld._isInteger(weekdayNum) || weekdayNum < 0 || weekdayNum > 6)
            throw "Error: Invalid weekday argument, must be an integer [0..6]";

        return this.day[weekdayNum];
    };

    /**
     * @public
     *
     * @description Returns the abbreviated name of the specified month.
     *
     * @param {integer Number} [monthNum] An integer between 0 and 11. Zero
     *        corresponds to January, one to February, etc. If omitted the
     *        method will return an array of all abbreviated month names.
     *
     * @returns {String | String[]} The abbreviated name of the specified month
     *          or an array of all abbreviated month names.
     *
     * @throws Error on invalid argument.
     */
    this.getAbbreviatedMonthName = function (monthNum) {

        if (typeof monthNum == "undefined" || monthNum === null)
            return this.abmon;

        if (!jsworld._isInteger(monthNum) || monthNum < 0 || monthNum > 11)
            throw "Error: Invalid month argument, must be an integer [0..11]";

        return this.abmon[monthNum];
    };

    /**
     * @public
     *
     * @description Returns the name of the specified month.
     *
     * @param {integer Number} [monthNum] An integer between 0 and 11. Zero
     *        corresponds to January, one to February, etc. If omitted the
     *        method will return an array of all month names.
     *
     * @returns {String | String[]} The name of the specified month or an array
     *          of all month names.
     *
     * @throws Error on invalid argument.
     */
    this.getMonthName = function (monthNum) {

        if (typeof monthNum == "undefined" || monthNum === null)
            return this.mon;

        if (!jsworld._isInteger(monthNum) || monthNum < 0 || monthNum > 11)
            throw "Error: Invalid month argument, must be an integer [0..11]";

        return this.mon[monthNum];
    };

    /**
     * @public
     *
     * @description Gets the decimal delimiter (radix) character for
     * numeric quantities.
     *
     * @returns {String} The radix character.
     */
    this.getDecimalPoint = function () {

        return this.decimal_point;
    };

    /**
     * @public
     *
     * @description Gets the local shorthand currency symbol.
     *
     * @returns {String} The currency symbol.
     */
    this.getCurrencySymbol = function () {

        return this.currency_symbol;
    };

    /**
     * @public
     *
     * @description Gets the internaltion currency symbol (ISO-4217 code).
     *
     * @returns {String} The international currency symbol.
     */
    this.getIntCurrencySymbol = function () {

        return this.int_curr_symbol.substring(0, 3);
    };

    /**
     * @public
     *
     * @description Gets the position of the local (shorthand) currency
     * symbol relative to the amount. Assumes a non-negative amount.
     *
     * @returns {Boolean} True if the symbol precedes the amount, false if
     * the symbol succeeds the amount.
     */
    this.currencySymbolPrecedes = function () {

        if (this.p_cs_precedes == 1)
            return true;
        else
            return false;
    };

    /**
     * @public
     *
     * @description Gets the position of the international (ISO-4217 code)
     * currency symbol relative to the amount. Assumes a non-negative
     * amount.
     *
     * @returns {Boolean} True if the symbol precedes the amount, false if
     * the symbol succeeds the amount.
     */
    this.intCurrencySymbolPrecedes = function () {

        if (this.int_p_cs_precedes == 1)
            return true;
        else
            return false;

    };

    /**
     * @public
     *
     * @description Gets the decimal delimiter (radix) for monetary
     * quantities.
     *
     * @returns {String} The radix character.
     */
    this.getMonetaryDecimalPoint = function () {

        return this.mon_decimal_point;
    };

    /**
     * @public
     *
     * @description Gets the number of fractional digits for local
     * (shorthand) symbol formatting.
     *
     * @returns {integer Number} The number of fractional digits.
     */
    this.getFractionalDigits = function () {

        return this.frac_digits;
    };

    /**
     * @public
     *
     * @description Gets the number of fractional digits for
     * international (ISO-4217 code) formatting.
     *
     * @returns {integer Number} The number of fractional digits.
     */
    this.getIntFractionalDigits = function () {

        return this.int_frac_digits;
    };
};

/**
 * @class
 * Class for localised formatting of numbers.
 *
 *
See:
 * POSIX LC_NUMERIC.
 *
 *
 * @public
 * @constructor
 * @description Creates a new numeric formatter for the specified locale.
 *
 * @param {jsworld.Locale} locale A locale object specifying the required
 *        POSIX LC_NUMERIC formatting properties.
 *
 * @throws Error on constructor failure.
 */
jsworld.NumericFormatter = function (locale) {

    if (typeof locale != "object" || locale._className != "jsworld.Locale")
        throw "Constructor error: You must provide a valid jsworld.Locale instance";

    this.lc = locale;

    /**
     * @public
     *
     * @description Formats a decimal numeric value according to the preset
     * locale.
     *
     * @param {Number|String} number The number to format.
     * @param {String} [options] Options to modify the formatted output:
     *

     *
"^"  suppress grouping
     *
"+"  force positive sign for positive amounts
     *
"~"  suppress positive/negative sign
     *
".n" specify decimal precision 'n'
     *

     *
     * @returns {String} The formatted number.
     *
     * @throws "Error: Invalid input" on bad input.
     */
    this.format = function (number, options) {

        if (typeof number == "string")
            number = jsworld._trim(number);

        if (!jsworld._isNumber(number))
            throw "Error: The input is not a number";

        var floatAmount = parseFloat(number, 10);

        // get the required precision
        var reqPrecision = jsworld._getPrecision(options);

        // round to required precision
        if (reqPrecision != -1)
            floatAmount = Math.round(floatAmount * Math.pow(10, reqPrecision)) / Math.pow(10, reqPrecision);

        // convert the float number to string and parse into
        // object with properties integer and fraction
        var parsedAmount = jsworld._splitNumber(String(floatAmount));

        // format integer part with grouping chars
        var formattedIntegerPart;

        if (floatAmount === 0)
            formattedIntegerPart = "0";
        else
            formattedIntegerPart = jsworld._hasOption("^", options) ?
                parsedAmount.integer :
                jsworld._formatIntegerPart(parsedAmount.integer,
                    this.lc.grouping,
                    this.lc.thousands_sep);

        // format the fractional part
        var formattedFractionPart =
            reqPrecision != -1 ?
                jsworld._formatFractionPart(parsedAmount.fraction, reqPrecision) :
                parsedAmount.fraction;

        // join the integer and fraction parts using the decimal_point property
        var formattedAmount =
            formattedFractionPart.length ?
                formattedIntegerPart + this.lc.decimal_point + formattedFractionPart :
                formattedIntegerPart;

        // prepend sign?
        if (jsworld._hasOption("~", options) || floatAmount === 0) {
            // suppress both '+' and '-' signs, i.e. return abs value
            return formattedAmount;
        }
        else {
            if (jsworld._hasOption("+", options) || floatAmount < 0) {
                if (floatAmount > 0)
                // force '+' sign for positive amounts
                    return "+" + formattedAmount;
                else if (floatAmount < 0)
                // prepend '-' sign
                    return "-" + formattedAmount;
                else
                // zero case
                    return formattedAmount;
            }
            else {
                // positive amount with no '+' sign
                return formattedAmount;
            }
        }
    };
};

/**
 * @class
 * Class for localised formatting of dates and times.
 *
 *
See:
 * POSIX LC_TIME.
 *
 * @public
 * @constructor
 * @description Creates a new date/time formatter for the specified locale.
 *
 * @param {jsworld.Locale} locale A locale object specifying the required
 *        POSIX LC_TIME formatting properties.
 *
 * @throws Error on constructor failure.
 */
jsworld.DateTimeFormatter = function (locale) {

    if (typeof locale != "object" || locale._className != "jsworld.Locale")
        throw "Constructor error: You must provide a valid jsworld.Locale instance.";

    this.lc = locale;

    /**
     * @public
     *
     * @description Formats a date according to the preset locale.
     *
     * @param {Date|String} date A valid Date object instance or a string
     *        containing a valid ISO-8601 formatted date, e.g. "2010-31-03"
     *        or "2010-03-31 23:59:59".
     *
     * @returns {String} The formatted date
     *
     * @throws Error on invalid date argument
     */
    this.formatDate = function (date) {

        var d = null;

        if (typeof date == "string") {
            // assume ISO-8601 date string
            try {
                d = jsworld.parseIsoDate(date);
            } catch (error) {
                // try full ISO-8601 date/time string
                d = jsworld.parseIsoDateTime(date);
            }
        }
        else if (date !== null && typeof date == "object") {
            // assume ready Date object
            d = date;
        }
        else {
            throw "Error: Invalid date argument, must be a Date object or an ISO-8601 date/time string";
        }

        return this._applyFormatting(d, this.lc.d_fmt);
    };

    /**
     * @public
     *
     * @description Formats a time according to the preset locale.
     *
     * @param {Date|String} date A valid Date object instance or a string
     *        containing a valid ISO-8601 formatted time, e.g. "23:59:59"
     *        or "2010-03-31 23:59:59".
     *
     * @returns {String} The formatted time.
     *
     * @throws Error on invalid date argument.
     */
    this.formatTime = function (date) {

        var d = null;

        if (typeof date == "string") {
            // assume ISO-8601 time string
            try {
                d = jsworld.parseIsoTime(date);
            } catch (error) {
                // try full ISO-8601 date/time string
                d = jsworld.parseIsoDateTime(date);
            }
        }
        else if (date !== null && typeof date == "object") {
            // assume ready Date object
            d = date;
        }
        else {
            throw "Error: Invalid date argument, must be a Date object or an ISO-8601 date/time string";
        }

        return this._applyFormatting(d, this.lc.t_fmt);
    };

    /**
     * @public
     *
     * @description Formats a date/time value according to the preset
     * locale.
     *
     * @param {Date|String} date A valid Date object instance or a string
     *        containing a valid ISO-8601 formatted date/time, e.g.
     *        "2010-03-31 23:59:59".
     *
     * @returns {String} The formatted time.
     *
     * @throws Error on invalid argument.
     */
    this.formatDateTime = function (date) {

        var d = null;

        if (typeof date == "string") {
            // assume ISO-8601 format
            d = jsworld.parseIsoDateTime(date);
        }
        else if (date !== null && typeof date == "object") {
            // assume ready Date object
            d = date;
        }
        else {
            throw "Error: Invalid date argument, must be a Date object or an ISO-8601 date/time string";
        }

        return this._applyFormatting(d, this.lc.d_t_fmt);
    };

    /**
     * @private
     *
     * @description Apples formatting to the Date object according to the
     * format string.
     *
     * @param {Date} d A valid Date instance.
     * @param {String} s The formatting string with '%' placeholders.
     *
     * @returns {String} The formatted string.
     */
    this._applyFormatting = function (d, s) {

        s = s.replace(/%%/g, '%');
        s = s.replace(/%a/g, this.lc.abday[d.getDay()]);
        s = s.replace(/%A/g, this.lc.day[d.getDay()]);
        s = s.replace(/%b/g, this.lc.abmon[d.getMonth()]);
        s = s.replace(/%B/g, this.lc.mon[d.getMonth()]);
        s = s.replace(/%d/g, jsworld._zeroPad(d.getDate(), 2));
        s = s.replace(/%e/g, jsworld._spacePad(d.getDate(), 2));
        s = s.replace(/%F/g, d.getFullYear() +
            "-" +
            jsworld._zeroPad(d.getMonth() + 1, 2) +
            "-" +
            jsworld._zeroPad(d.getDate(), 2));
        s = s.replace(/%h/g, this.lc.abmon[d.getMonth()]); // same as %b
        s = s.replace(/%H/g, jsworld._zeroPad(d.getHours(), 2));
        s = s.replace(/%I/g, jsworld._zeroPad(this._hours12(d.getHours()), 2));
        s = s.replace(/%k/g, d.getHours());
        s = s.replace(/%l/g, this._hours12(d.getHours()));
        s = s.replace(/%m/g, jsworld._zeroPad(d.getMonth() + 1, 2));
        s = s.replace(/%n/g, "\n");
        s = s.replace(/%M/g, jsworld._zeroPad(d.getMinutes(), 2));
        s = s.replace(/%p/g, this._getAmPm(d.getHours()));
        s = s.replace(/%P/g, this._getAmPm(d.getHours()).toLocaleLowerCase()); // safe?
        s = s.replace(/%R/g, jsworld._zeroPad(d.getHours(), 2) +
            ":" +
            jsworld._zeroPad(d.getMinutes(), 2));
        s = s.replace(/%S/g, jsworld._zeroPad(d.getSeconds(), 2));
        s = s.replace(/%T/g, jsworld._zeroPad(d.getHours(), 2) +
            ":" +
            jsworld._zeroPad(d.getMinutes(), 2) +
            ":" +
            jsworld._zeroPad(d.getSeconds(), 2));
        s = s.replace(/%w/g, this.lc.day[d.getDay()]);
        s = s.replace(/%y/g, new String(d.getFullYear()).substring(2));
        s = s.replace(/%Y/g, d.getFullYear());

        s = s.replace(/%Z/g, ""); // to do: ignored until a reliable TMZ method found

        s = s.replace(/%[a-zA-Z]/g, ""); // ignore all other % sequences

        return s;
    };

    /**
     * @private
     *
     * @description Does 24 to 12 hour conversion.
     *
     * @param {integer Number} hour24 Hour [0..23].
     *
     * @returns {integer Number} Corresponding hour [1..12].
     */
    this._hours12 = function (hour24) {

        if (hour24 === 0)
            return 12; // 00h is 12AM

        else if (hour24 > 12)
            return hour24 - 12; // 1PM to 11PM

        else
            return hour24; // 1AM to 12PM
    };

    /**
     * @private
     *
     * @description Gets the appropriate localised AM or PM string depending
     * on the day hour. Special cases: midnight is 12AM, noon is 12PM.
     *
     * @param {integer Number} hour24 Hour [0..23].
     *
     * @returns {String} The corresponding localised AM or PM string.
     */
    this._getAmPm = function (hour24) {

        if (hour24 < 12)
            return this.lc.am;
        else
            return this.lc.pm;
    };
};

/**
 * @class Class for localised formatting of currency amounts.
 *
 *
See:
 * POSIX LC_MONETARY.
 *
 * @public
 * @constructor
 * @description Creates a new monetary formatter for the specified locale.
 *
 * @param {jsworld.Locale} locale A locale object specifying the required
 *        POSIX LC_MONETARY formatting properties.
 * @param {String} [currencyCode] Set the currency explicitly by
 *        passing its international ISO-4217 code, e.g. "USD", "EUR", "GBP".
 *        Use this optional parameter to override the default local currency
 * @param {String} [altIntSymbol] Non-local currencies are formatted
 *        with their international ISO-4217 code to prevent ambiguity.
 *        Use this optional argument to force a different symbol, such as the
 *        currency's shorthand sign. This is mostly useful when the shorthand
 *        sign is both internationally recognised and identifies the currency
 *        uniquely (e.g. the Euro sign).
 *
 * @throws Error on constructor failure.
 */
jsworld.MonetaryFormatter = function (locale, currencyCode, altIntSymbol) {

    if (typeof locale != "object" || locale._className != "jsworld.Locale")
        throw "Constructor error: You must provide a valid jsworld.Locale instance";

    this.lc = locale;

    /**
     * @private
     * @description Lookup table to determine the fraction digits for a
     * specific currency; most currencies subdivide at 1/100 (2 fractional
     * digits), so we store only those that deviate from the default.
     *
     *
The data is from Unicode's CLDR version 1.7.0. The two currencies
     * with non-decimal subunits (MGA and MRO) are marked as having no
     * fractional digits as well as all currencies that have no subunits
     * in circulation.
     *
     *
It is "hard-wired" for referential convenience and is only looked
     * up when an overriding currencyCode parameter is supplied.
     */
    this.currencyFractionDigits = {
        "AFN": 0, "ALL": 0, "AMD": 0, "BHD": 3, "BIF": 0,
        "BYR": 0, "CLF": 0, "CLP": 0, "COP": 0, "CRC": 0,
        "DJF": 0, "GNF": 0, "GYD": 0, "HUF": 0, "IDR": 0,
        "IQD": 0, "IRR": 0, "ISK": 0, "JOD": 3, "JPY": 0,
        "KMF": 0, "KRW": 0, "KWD": 3, "LAK": 0, "LBP": 0,
        "LYD": 3, "MGA": 0, "MMK": 0, "MNT": 0, "MRO": 0,
        "MUR": 0, "OMR": 3, "PKR": 0, "PYG": 0, "RSD": 0,
        "RWF": 0, "SLL": 0, "SOS": 0, "STD": 0, "SYP": 0,
        "TND": 3, "TWD": 0, "TZS": 0, "UGX": 0, "UZS": 0,
        "VND": 0, "VUV": 0, "XAF": 0, "XOF": 0, "XPF": 0,
        "YER": 0, "ZMK": 0
    };

    // optional currencyCode argument?
    if (typeof currencyCode == "string") {
        // user wanted to override the local currency
        this.currencyCode = currencyCode.toUpperCase();

        // must override the frac digits too, for some
        // currencies have 0, 2 or 3!
        var numDigits = this.currencyFractionDigits[this.currencyCode];
        if (typeof numDigits != "number")
            numDigits = 2; // default for most currencies
        this.lc.frac_digits = numDigits;
        this.lc.int_frac_digits = numDigits;
    }
    else {
        // use local currency
        this.currencyCode = this.lc.int_curr_symbol.substring(0, 3).toUpperCase();
    }

    // extract intl. currency separator
    this.intSep = this.lc.int_curr_symbol.charAt(3);

    // flag local or intl. sign formatting?
    if (this.currencyCode == this.lc.int_curr_symbol.substring(0, 3)) {
        // currency matches the local one? ->
        // formatting with local symbol and parameters
        this.internationalFormatting = false;
        this.curSym = this.lc.currency_symbol;
    }
    else {
        // currency doesn't match the local ->

        // do we have an overriding currency symbol?
        if (typeof altIntSymbol == "string") {
            // -> force formatting with local parameters, using alt symbol
            this.curSym = altIntSymbol;
            this.internationalFormatting = false;
        }
        else {
            // -> force formatting with intl. sign and parameters
            this.internationalFormatting = true;
        }
    }

    /**
     * @public
     *
     * @description Gets the currency symbol used in formatting.
     *
     * @returns {String} The currency symbol.
     */
    this.getCurrencySymbol = function () {

        return this.curSym;
    };

    /**
     * @public
     *
     * @description Gets the position of the currency symbol relative to
     * the amount. Assumes a non-negative amount and local formatting.
     *
     * @param {String} intFlag Optional flag to force international
     * formatting by passing the string "i".
     *
     * @returns {Boolean} True if the symbol precedes the amount, false if
     * the symbol succeeds the amount.
     */
    this.currencySymbolPrecedes = function (intFlag) {

        if (typeof intFlag == "string" && intFlag == "i") {
            // international formatting was forced
            if (this.lc.int_p_cs_precedes == 1)
                return true;
            else
                return false;

        }
        else {
            // check whether local formatting is on or off
            if (this.internationalFormatting) {
                if (this.lc.int_p_cs_precedes == 1)
                    return true;
                else
                    return false;
            }
            else {
                if (this.lc.p_cs_precedes == 1)
                    return true;
                else
                    return false;
            }
        }
    };

    /**
     * @public
     *
     * @description Gets the decimal delimiter (radix) used in formatting.
     *
     * @returns {String} The radix character.
     */
    this.getDecimalPoint = function () {

        return this.lc.mon_decimal_point;
    };

    /**
     * @public
     *
     * @description Gets the number of fractional digits. Assumes local
     * formatting.
     *
     * @param {String} intFlag Optional flag to force international
     *        formatting by passing the string "i".
     *
     * @returns {integer Number} The number of fractional digits.
     */
    this.getFractionalDigits = function (intFlag) {

        if (typeof intFlag == "string" && intFlag == "i") {
            // international formatting was forced
            return this.lc.int_frac_digits;
        }
        else {
            // check whether local formatting is on or off
            if (this.internationalFormatting)
                return this.lc.int_frac_digits;
            else
                return this.lc.frac_digits;
        }
    };

    /**
     * @public
     *
     * @description Formats a monetary amount according to the preset
     * locale.
     *
     *

     * For local currencies the native shorthand symbol will be used for
     * formatting.
     * Example:
     *        locale is en_US
     *        currency is USD
     *        -> the "$" symbol will be used, e.g. $123.45
     *
     * For non-local currencies the international ISO-4217 code will be
     * used for formatting.
     * Example:
     *       locale is en_US (which has USD as currency)
     *       currency is EUR
     *       -> the ISO three-letter code will be used, e.g. EUR 123.45
     *
     * If the currency is non-local, but an alternative currency symbol was
     * provided, this will be used instead.
     * Example
     *       locale is en_US (which has USD as currency)
     *       currency is EUR
     *       an alternative symbol is provided - "€"
     *       -> the alternative symbol will be used, e.g. €123.45
     *

     *
     * @param {Number|String} amount The amount to format as currency.
     * @param {String} [options] Options to modify the formatted output:
     *

     *
"^"  suppress grouping
     *
"!"  suppress the currency symbol
     *
"~"  suppress the currency symbol and the sign (positive or negative)
     *
"i"  force international sign (ISO-4217 code) formatting
     *
".n" specify decimal precision
     *
     * @returns The formatted currency amount as string.
     *
     * @throws "Error: Invalid amount" on bad amount.
     */
    this.format = function (amount, options) {

        // if the amount is passed as string, check that it parses to a float
        var floatAmount;

        if (typeof amount == "string") {
            amount = jsworld._trim(amount);
            floatAmount = parseFloat(amount);

            if (typeof floatAmount != "number" || isNaN(floatAmount))
                throw "Error: Amount string not a number";
        }
        else if (typeof amount == "number") {
            floatAmount = amount;
        }
        else {
            throw "Error: Amount not a number";
        }

        // get the required precision, ".n" option arg overrides default locale config
        var reqPrecision = jsworld._getPrecision(options);

        if (reqPrecision == -1) {
            if (this.internationalFormatting || jsworld._hasOption("i", options))
                reqPrecision = this.lc.int_frac_digits;
            else
                reqPrecision = this.lc.frac_digits;
        }

        // round
        floatAmount = Math.round(floatAmount * Math.pow(10, reqPrecision)) / Math.pow(10, reqPrecision);

        // convert the float amount to string and parse into
        // object with properties integer and fraction
        var parsedAmount = jsworld._splitNumber(String(floatAmount));

        // format integer part with grouping chars
        var formattedIntegerPart;

        if (floatAmount === 0)
            formattedIntegerPart = "0";
        else
            formattedIntegerPart = jsworld._hasOption("^", options) ?
                parsedAmount.integer :
                jsworld._formatIntegerPart(parsedAmount.integer,
                    this.lc.mon_grouping,
                    this.lc.mon_thousands_sep);

        // format the fractional part
        var formattedFractionPart;

        if (reqPrecision == -1) {
            // pad fraction with trailing zeros accoring to default locale [int_]frac_digits
            if (this.internationalFormatting || jsworld._hasOption("i", options))
                formattedFractionPart =
                    jsworld._formatFractionPart(parsedAmount.fraction, this.lc.int_frac_digits);
            else
                formattedFractionPart =
                    jsworld._formatFractionPart(parsedAmount.fraction, this.lc.frac_digits);
        }
        else {
            // pad fraction with trailing zeros according to optional format parameter
            formattedFractionPart =
                jsworld._formatFractionPart(parsedAmount.fraction, reqPrecision);
        }

        // join integer and decimal parts using the mon_decimal_point property
        var quantity;

        if (this.lc.frac_digits > 0 || formattedFractionPart.length)
            quantity = formattedIntegerPart + this.lc.mon_decimal_point + formattedFractionPart;
        else
            quantity = formattedIntegerPart;

        // do final formatting with sign and symbol
        if (jsworld._hasOption("~", options)) {
            return quantity;
        }
        else {
            var suppressSymbol = jsworld._hasOption("!", options) ? true : false;

            var sign = floatAmount < 0 ? "-" : "+";

            if (this.internationalFormatting || jsworld._hasOption("i", options)) {

                // format with ISO-4217 code (suppressed or not)
                if (suppressSymbol)
                    return this._formatAsInternationalCurrencyWithNoSym(sign, quantity);
                else
                    return this._formatAsInternationalCurrency(sign, quantity);
            }
            else {
                // format with local currency code (suppressed or not)
                if (suppressSymbol)
                    return this._formatAsLocalCurrencyWithNoSym(sign, quantity);
                else
                    return this._formatAsLocalCurrency(sign, quantity);
            }
        }
    };

    /**
     * @private
     *
     * @description Assembles the final string with sign, separator and symbol as local
     * currency.
     *
     * @param {String} sign The amount sign: "+" or "-".
     * @param {String} q The formatted quantity (unsigned).
     *
     * @returns {String} The final formatted string.
     */
    this._formatAsLocalCurrency = function (sign, q) {

        // assemble final formatted amount by going over all possible value combinations of:
        // sign {+,-} , sign position {0,1,2,3,4} , separator {0,1,2} , symbol position {0,1}
        if (sign == "+") {

            // parentheses
            if (this.lc.p_sign_posn === 0 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return "(" + q + this.curSym + ")";
            }
            else if (this.lc.p_sign_posn === 0 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return "(" + this.curSym + q + ")";
            }
            else if (this.lc.p_sign_posn === 0 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return "(" + q + " " + this.curSym + ")";
            }
            else if (this.lc.p_sign_posn === 0 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return "(" + this.curSym + " " + q + ")";
            }

            // sign before q + sym
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return this.lc.positive_sign + q + this.curSym;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + this.curSym + q;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return this.lc.positive_sign + q + " " + this.curSym;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + this.curSym + " " + q;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return this.lc.positive_sign + " " + q + this.curSym;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + " " + this.curSym + q;
            }

            // sign after q + sym
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return q + this.curSym + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return this.curSym + q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return  q + " " + this.curSym + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return this.curSym + " " + q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return q + this.curSym + " " + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return this.curSym + q + " " + this.lc.positive_sign;
            }

            // sign before sym
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return q + this.lc.positive_sign + this.curSym;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + this.curSym + q;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return q + " " + this.lc.positive_sign + this.curSym;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + this.curSym + " " + q;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return q + this.lc.positive_sign + " " + this.curSym;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + " " + this.curSym + q;
            }

            // sign after symbol
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return q + this.curSym + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return this.curSym + this.lc.positive_sign + q;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return  q + " " + this.curSym + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return this.curSym + this.lc.positive_sign + " " + q;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return q + this.curSym + " " + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return this.curSym + " " + this.lc.positive_sign + q;
            }

        }
        else if (sign == "-") {

            // parentheses enclose q + sym
            if (this.lc.n_sign_posn === 0 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return "(" + q + this.curSym + ")";
            }
            else if (this.lc.n_sign_posn === 0 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return "(" + this.curSym + q + ")";
            }
            else if (this.lc.n_sign_posn === 0 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return "(" + q + " " + this.curSym + ")";
            }
            else if (this.lc.n_sign_posn === 0 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return "(" + this.curSym + " " + q + ")";
            }

            // sign before q + sym
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return this.lc.negative_sign + q + this.curSym;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + this.curSym + q;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return this.lc.negative_sign + q + " " + this.curSym;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + this.curSym + " " + q;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return this.lc.negative_sign + " " + q + this.curSym;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + " " + this.curSym + q;
            }

            // sign after q + sym
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return q + this.curSym + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return this.curSym + q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return  q + " " + this.curSym + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return this.curSym + " " + q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return q + this.curSym + " " + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return this.curSym + q + " " + this.lc.negative_sign;
            }

            // sign before sym
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return q + this.lc.negative_sign + this.curSym;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + this.curSym + q;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return q + " " + this.lc.negative_sign + this.curSym;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + this.curSym + " " + q;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return q + this.lc.negative_sign + " " + this.curSym;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + " " + this.curSym + q;
            }

            // sign after symbol
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return q + this.curSym + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return this.curSym + this.lc.negative_sign + q;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return  q + " " + this.curSym + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return this.curSym + this.lc.negative_sign + " " + q;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return q + this.curSym + " " + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return this.curSym + " " + this.lc.negative_sign + q;
            }
        }

        // throw error if we fall through
        throw "Error: Invalid POSIX LC MONETARY definition";
    };

    /**
     * @private
     *
     * @description Assembles the final string with sign, separator and ISO-4217
     * currency code.
     *
     * @param {String} sign The amount sign: "+" or "-".
     * @param {String} q The formatted quantity (unsigned).
     *
     * @returns {String} The final formatted string.
     */
    this._formatAsInternationalCurrency = function (sign, q) {

        // assemble the final formatted amount by going over all possible value combinations of:
        // sign {+,-} , sign position {0,1,2,3,4} , separator {0,1,2} , symbol position {0,1}

        if (sign == "+") {

            // parentheses
            if (this.lc.int_p_sign_posn === 0 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return "(" + q + this.currencyCode + ")";
            }
            else if (this.lc.int_p_sign_posn === 0 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return "(" + this.currencyCode + q + ")";
            }
            else if (this.lc.int_p_sign_posn === 0 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return "(" + q + this.intSep + this.currencyCode + ")";
            }
            else if (this.lc.int_p_sign_posn === 0 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return "(" + this.currencyCode + this.intSep + q + ")";
            }

            // sign before q + sym
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return this.lc.positive_sign + q + this.currencyCode;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.currencyCode + q;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return this.lc.positive_sign + q + this.intSep + this.currencyCode;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.currencyCode + this.intSep + q;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return this.lc.positive_sign + this.intSep + q + this.currencyCode;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.intSep + this.currencyCode + q;
            }

            // sign after q + sym
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return q + this.currencyCode + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return this.currencyCode + q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return  q + this.intSep + this.currencyCode + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return this.currencyCode + this.intSep + q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return q + this.currencyCode + this.intSep + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return this.currencyCode + q + this.intSep + this.lc.positive_sign;
            }

            // sign before sym
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return q + this.lc.positive_sign + this.currencyCode;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.currencyCode + q;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return q + this.intSep + this.lc.positive_sign + this.currencyCode;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.currencyCode + this.intSep + q;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return q + this.lc.positive_sign + this.intSep + this.currencyCode;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.intSep + this.currencyCode + q;
            }

            // sign after symbol
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return q + this.currencyCode + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return this.currencyCode + this.lc.positive_sign + q;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return  q + this.intSep + this.currencyCode + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return this.currencyCode + this.lc.positive_sign + this.intSep + q;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return q + this.currencyCode + this.intSep + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return this.currencyCode + this.intSep + this.lc.positive_sign + q;
            }

        }
        else if (sign == "-") {

            // parentheses enclose q + sym
            if (this.lc.int_n_sign_posn === 0 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return "(" + q + this.currencyCode + ")";
            }
            else if (this.lc.int_n_sign_posn === 0 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return "(" + this.currencyCode + q + ")";
            }
            else if (this.lc.int_n_sign_posn === 0 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return "(" + q + this.intSep + this.currencyCode + ")";
            }
            else if (this.lc.int_n_sign_posn === 0 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return "(" + this.currencyCode + this.intSep + q + ")";
            }

            // sign before q + sym
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return this.lc.negative_sign + q + this.currencyCode;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.currencyCode + q;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return this.lc.negative_sign + q + this.intSep + this.currencyCode;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.currencyCode + this.intSep + q;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return this.lc.negative_sign + this.intSep + q + this.currencyCode;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.intSep + this.currencyCode + q;
            }

            // sign after q + sym
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return q + this.currencyCode + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return this.currencyCode + q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return  q + this.intSep + this.currencyCode + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return this.currencyCode + this.intSep + q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return q + this.currencyCode + this.intSep + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return this.currencyCode + q + this.intSep + this.lc.negative_sign;
            }

            // sign before sym
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return q + this.lc.negative_sign + this.currencyCode;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.currencyCode + q;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return q + this.intSep + this.lc.negative_sign + this.currencyCode;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.currencyCode + this.intSep + q;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return q + this.lc.negative_sign + this.intSep + this.currencyCode;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.intSep + this.currencyCode + q;
            }

            // sign after symbol
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return q + this.currencyCode + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return this.currencyCode + this.lc.negative_sign + q;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return  q + this.intSep + this.currencyCode + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return this.currencyCode + this.lc.negative_sign + this.intSep + q;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return q + this.currencyCode + this.intSep + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return this.currencyCode + this.intSep + this.lc.negative_sign + q;
            }
        }

        // throw error if we fall through
        throw "Error: Invalid POSIX LC MONETARY definition";
    };

    /**
     * @private
     *
     * @description Assembles the final string with sign and separator, but suppress the
     * local currency symbol.
     *
     * @param {String} sign The amount sign: "+" or "-".
     * @param {String} q The formatted quantity (unsigned).
     *
     * @returns {String} The final formatted string
     */
    this._formatAsLocalCurrencyWithNoSym = function (sign, q) {

        // assemble the final formatted amount by going over all possible value combinations of:
        // sign {+,-} , sign position {0,1,2,3,4} , separator {0,1,2} , symbol position {0,1}

        if (sign == "+") {

            // parentheses
            if (this.lc.p_sign_posn === 0) {
                return "(" + q + ")";
            }

            // sign before q + sym
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return this.lc.positive_sign + " " + q;
            }
            else if (this.lc.p_sign_posn === 1 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + " " + q;
            }

            // sign after q + sym
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return  q + " " + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 2 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return q + " " + this.lc.positive_sign;
            }

            // sign before sym
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return q + " " + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + " " + q;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 3 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + " " + q;
            }

            // sign after symbol
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 0 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 0) {
                return  q + " " + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 1 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + " " + q;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 0) {
                return q + " " + this.lc.positive_sign;
            }
            else if (this.lc.p_sign_posn === 4 && this.lc.p_sep_by_space === 2 && this.lc.p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }

        }
        else if (sign == "-") {

            // parentheses enclose q + sym
            if (this.lc.n_sign_posn === 0) {
                return "(" + q + ")";
            }

            // sign before q + sym
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + " " + q;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return this.lc.negative_sign + " " + q;
            }
            else if (this.lc.n_sign_posn === 1 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + " " + q;
            }

            // sign after q + sym
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return  q + " " + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return q + " " + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 2 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return q + " " + this.lc.negative_sign;
            }

            // sign before sym
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return q + " " + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + " " + q;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 3 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + " " + q;
            }

            // sign after symbol
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 0 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 0) {
                return  q + " " + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 1 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + " " + q;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 0) {
                return q + " " + this.lc.negative_sign;
            }
            else if (this.lc.n_sign_posn === 4 && this.lc.n_sep_by_space === 2 && this.lc.n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
        }

        // throw error if we fall through
        throw "Error: Invalid POSIX LC MONETARY definition";
    };

    /**
     * @private
     *
     * @description Assembles the final string with sign and separator, but suppress
     * the ISO-4217 currency code.
     *
     * @param {String} sign The amount sign: "+" or "-".
     * @param {String} q The formatted quantity (unsigned).
     *
     * @returns {String} The final formatted string.
     */
    this._formatAsInternationalCurrencyWithNoSym = function (sign, q) {

        // assemble the final formatted amount by going over all possible value combinations of:
        // sign {+,-} , sign position {0,1,2,3,4} , separator {0,1,2} , symbol position {0,1}

        if (sign == "+") {

            // parentheses
            if (this.lc.int_p_sign_posn === 0) {
                return "(" + q + ")";
            }

            // sign before q + sym
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.intSep + q;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return this.lc.positive_sign + this.intSep + q;
            }
            else if (this.lc.int_p_sign_posn === 1 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.intSep + q;
            }

            // sign after q + sym
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return  q + this.intSep + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return q + this.intSep + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 2 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return q + this.intSep + this.lc.positive_sign;
            }

            // sign before sym
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return q + this.intSep + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.intSep + q;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 3 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.intSep + q;
            }

            // sign after symbol
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 0) {
                return q + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 0 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 0) {
                return  q + this.intSep + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 1 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + this.intSep + q;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 0) {
                return q + this.intSep + this.lc.positive_sign;
            }
            else if (this.lc.int_p_sign_posn === 4 && this.lc.int_p_sep_by_space === 2 && this.lc.int_p_cs_precedes === 1) {
                return this.lc.positive_sign + q;
            }

        }
        else if (sign == "-") {

            // parentheses enclose q + sym
            if (this.lc.int_n_sign_posn === 0) {
                return "(" + q + ")";
            }

            // sign before q + sym
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.intSep + q;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return this.lc.negative_sign + this.intSep + q;
            }
            else if (this.lc.int_n_sign_posn === 1 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.intSep + q;
            }

            // sign after q + sym
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return  q + this.intSep + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return q + this.intSep + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 2 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return q + this.intSep + this.lc.negative_sign;
            }

            // sign before sym
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return q + this.intSep + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.intSep + q;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 3 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.intSep + q;
            }

            // sign after symbol
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 0) {
                return q + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 0 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 0) {
                return  q + this.intSep + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 1 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + this.intSep + q;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 0) {
                return q + this.intSep + this.lc.negative_sign;
            }
            else if (this.lc.int_n_sign_posn === 4 && this.lc.int_n_sep_by_space === 2 && this.lc.int_n_cs_precedes === 1) {
                return this.lc.negative_sign + q;
            }
        }

        // throw error if we fall through
        throw "Error: Invalid POSIX LC_MONETARY definition";
    };
};

/**
 * @class
 * Class for parsing localised number strings.
 *
 * @public
 * @constructor
 * @description Creates a new numeric parser for the specified locale.
 *
 * @param {jsworld.Locale} locale A locale object specifying the required
 *        POSIX LC_NUMERIC formatting properties.
 *
 * @throws Error on constructor failure.
 */
jsworld.NumericParser = function (locale) {

    if (typeof locale != "object" || locale._className != "jsworld.Locale")
        throw "Constructor error: You must provide a valid jsworld.Locale instance";

    this.lc = locale;

    /**
     * @public
     *
     * @description Parses a numeric string formatted according to the
     * preset locale. Leading and trailing whitespace is ignored; the number
     * may also be formatted without thousands separators.
     *
     * @param {String} formattedNumber The formatted number.
     *
     * @returns {Number} The parsed number.
     *
     * @throws Error on a parse exception.
     */
    this.parse = function (formattedNumber) {

        if (typeof formattedNumber != "string")
            throw "Parse error: Argument must be a string";

        // trim whitespace
        var s = jsworld._trim(formattedNumber);

        // remove any thousand separator symbols
        s = jsworld._stringReplaceAll(formattedNumber, this.lc.thousands_sep, "");

        // replace any local decimal point symbols with the symbol used
        // in JavaScript "."
        s = jsworld._stringReplaceAll(s, this.lc.decimal_point, ".");

        // test if the string represents a number
        if (jsworld._isNumber(s))
            return parseFloat(s, 10);
        else
            throw "Parse error: Invalid number string";
    };
};

/**
 * @class
 * Class for parsing localised date and time strings.
 *
 * @public
 * @constructor
 * @description Creates a new date/time parser for the specified locale.
 *
 * @param {jsworld.Locale} locale A locale object specifying the required
 *        POSIX LC_TIME formatting properties.
 *
 * @throws Error on constructor failure.
 */
jsworld.DateTimeParser = function (locale) {

    if (typeof locale != "object" || locale._className != "jsworld.Locale")
        throw "Constructor error: You must provide a valid jsworld.Locale instance.";

    this.lc = locale;

    /**
     * @public
     *
     * @description Parses a time string formatted according to the
     * POSIX LC_TIME t_fmt property of the preset locale.
     *
     * @param {String} formattedTime The formatted time.
     *
     * @returns {String} The parsed time in ISO-8601 format (HH:MM:SS), e.g.
     *          "23:59:59".
     *
     * @throws Error on a parse exception.
     */
    this.parseTime = function (formattedTime) {

        if (typeof formattedTime != "string")
            throw "Parse error: Argument must be a string";

        var dt = this._extractTokens(this.lc.t_fmt, formattedTime);

        var timeDefined = false;

        if (dt.hour !== null && dt.minute !== null && dt.second !== null) {
            timeDefined = true;
        }
        else if (dt.hourAmPm !== null && dt.am !== null && dt.minute !== null && dt.second !== null) {
            if (dt.am) {
                // AM [12(midnight), 1 .. 11]
                if (dt.hourAmPm == 12)
                    dt.hour = 0;
                else
                    dt.hour = parseInt(dt.hourAmPm, 10);
            }
            else {
                // PM [12(noon), 1 .. 11]
                if (dt.hourAmPm == 12)
                    dt.hour = 12;
                else
                    dt.hour = parseInt(dt.hourAmPm, 10) + 12;
            }
            timeDefined = true;
        }

        if (timeDefined)
            return jsworld._zeroPad(dt.hour, 2) +
                ":" +
                jsworld._zeroPad(dt.minute, 2) +
                ":" +
                jsworld._zeroPad(dt.second, 2);
        else
            throw "Parse error: Invalid/ambiguous time string";
    };

    /**
     * @public
     *
     * @description Parses a date string formatted according to the
     * POSIX LC_TIME d_fmt property of the preset locale.
     *
     * @param {String} formattedDate The formatted date, must be valid.
     *
     * @returns {String} The parsed date in ISO-8601 format (YYYY-MM-DD),
     *          e.g. "2010-03-31".
     *
     * @throws Error on a parse exception.
     */
    this.parseDate = function (formattedDate) {

        if (typeof formattedDate != "string")
            throw "Parse error: Argument must be a string";

        var dt = this._extractTokens(this.lc.d_fmt, formattedDate);

        var dateDefined = false;

        if (dt.year !== null && dt.month !== null && dt.day !== null) {
            dateDefined = true;
        }

        if (dateDefined)
            return jsworld._zeroPad(dt.year, 4) +
                "-" +
                jsworld._zeroPad(dt.month, 2) +
                "-" +
                jsworld._zeroPad(dt.day, 2);
        else
            throw "Parse error: Invalid date string";
    };

    /**
     * @public
     *
     * @description Parses a date/time string formatted according to the
     * POSIX LC_TIME d_t_fmt property of the preset locale.
     *
     * @param {String} formattedDateTime The formatted date/time, must be
     *        valid.
     *
     * @returns {String} The parsed date/time in ISO-8601 format
     *          (YYYY-MM-DD HH:MM:SS), e.g. "2010-03-31 23:59:59".
     *
     * @throws Error on a parse exception.
     */
    this.parseDateTime = function (formattedDateTime) {

        if (typeof formattedDateTime != "string")
            throw "Parse error: Argument must be a string";

        var dt = this._extractTokens(this.lc.d_t_fmt, formattedDateTime);

        var timeDefined = false;
        var dateDefined = false;

        if (dt.hour !== null && dt.minute !== null && dt.second !== null) {
            timeDefined = true;
        }
        else if (dt.hourAmPm !== null && dt.am !== null && dt.minute !== null && dt.second !== null) {
            if (dt.am) {
                // AM [12(midnight), 1 .. 11]
                if (dt.hourAmPm == 12)
                    dt.hour = 0;
                else
                    dt.hour = parseInt(dt.hourAmPm, 10);
            }
            else {
                // PM [12(noon), 1 .. 11]
                if (dt.hourAmPm == 12)
                    dt.hour = 12;
                else
                    dt.hour = parseInt(dt.hourAmPm, 10) + 12;
            }
            timeDefined = true;
        }

        if (dt.year !== null && dt.month !== null && dt.day !== null) {
            dateDefined = true;
        }

        if (dateDefined && timeDefined)
            return jsworld._zeroPad(dt.year, 4) +
                "-" +
                jsworld._zeroPad(dt.month, 2) +
                "-" +
                jsworld._zeroPad(dt.day, 2) +
                " " +
                jsworld._zeroPad(dt.hour, 2) +
                ":" +
                jsworld._zeroPad(dt.minute, 2) +
                ":" +
                jsworld._zeroPad(dt.second, 2);
        else
            throw "Parse error: Invalid/ambiguous date/time string";
    };

    /**
     * @private
     *
     * @description Parses a string according to the specified format
     * specification.
     *
     * @param {String} fmtSpec The format specification, e.g. "%I:%M:%S %p".
     * @param {String} s The string to parse.
     *
     * @returns {object} An object with set properties year, month, day,
     *          hour, minute and second if the corresponding values are
     *          found in the parsed string.
     *
     * @throws Error on a parse exception.
     */
    this._extractTokens = function (fmtSpec, s) {

        // the return object containing the parsed date/time properties
        var dt = {
            // for date and date/time strings
            "year": null,
            "month": null,
            "day": null,

            // for time and date/time strings
            "hour": null,
            "hourAmPm": null,
            "am": null,
            "minute": null,
            "second": null,

            // used internally only
            "weekday": null
        };

        // extract and process each token in the date/time spec
        while (fmtSpec.length > 0) {

            // Do we have a valid "%\w" placeholder in stream?
            if (fmtSpec.charAt(0) == "%" && fmtSpec.charAt(1) != "") {

                // get placeholder
                var placeholder = fmtSpec.substring(0, 2);

                if (placeholder == "%%") {
                    // escaped '%''
                    s = s.substring(1);
                }
                else if (placeholder == "%a") {
                    // abbreviated weekday name
                    for (var i = 0; i < this.lc.abday.length; i++) {

                        if (jsworld._stringStartsWith(s, this.lc.abday[i])) {
                            dt.weekday = i;
                            s = s.substring(this.lc.abday[i].length);
                            break;
                        }
                    }

                    if (dt.weekday === null)
                        throw "Parse error: Unrecognised abbreviated weekday name (%a)";
                }
                else if (placeholder == "%A") {
                    // weekday name
                    for (var i = 0; i < this.lc.day.length; i++) {

                        if (jsworld._stringStartsWith(s, this.lc.day[i])) {
                            dt.weekday = i;
                            s = s.substring(this.lc.day[i].length);
                            break;
                        }
                    }

                    if (dt.weekday === null)
                        throw "Parse error: Unrecognised weekday name (%A)";
                }
                else if (placeholder == "%b" || placeholder == "%h") {
                    // abbreviated month name
                    for (var i = 0; i < this.lc.abmon.length; i++) {

                        if (jsworld._stringStartsWith(s, this.lc.abmon[i])) {
                            dt.month = i + 1;
                            s = s.substring(this.lc.abmon[i].length);
                            break;
                        }
                    }

                    if (dt.month === null)
                        throw "Parse error: Unrecognised abbreviated month name (%b)";
                }
                else if (placeholder == "%B") {
                    // month name
                    for (var i = 0; i < this.lc.mon.length; i++) {

                        if (jsworld._stringStartsWith(s, this.lc.mon[i])) {
                            dt.month = i + 1;
                            s = s.substring(this.lc.mon[i].length);
                            break;
                        }
                    }

                    if (dt.month === null)
                        throw "Parse error: Unrecognised month name (%B)";
                }
                else if (placeholder == "%d") {
                    // day of the month [01..31]
                    if (/^0[1-9]|[1-2][0-9]|3[0-1]/.test(s)) {
                        dt.day = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised day of the month (%d)";
                }
                else if (placeholder == "%e") {
                    // day of the month [1..31]

                    // Note: if %e is leading in fmt string -> space padded!

                    var day = s.match(/^\s?(\d{1,2})/);
                    dt.day = parseInt(day, 10);

                    if (isNaN(dt.day) || dt.day < 1 || dt.day > 31)
                        throw "Parse error: Unrecognised day of the month (%e)";

                    s = s.substring(day.length);
                }
                else if (placeholder == "%F") {
                    // equivalent to %Y-%m-%d (ISO-8601 date format)

                    // year [nnnn]
                    if (/^\d\d\d\d/.test(s)) {
                        dt.year = parseInt(s.substring(0, 4), 10);
                        s = s.substring(4);
                    }
                    else {
                        throw "Parse error: Unrecognised date (%F)";
                    }

                    // -
                    if (jsworld._stringStartsWith(s, "-"))
                        s = s.substring(1);
                    else
                        throw "Parse error: Unrecognised date (%F)";

                    // month [01..12]
                    if (/^0[1-9]|1[0-2]/.test(s)) {
                        dt.month = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised date (%F)";

                    // -
                    if (jsworld._stringStartsWith(s, "-"))
                        s = s.substring(1);
                    else
                        throw "Parse error: Unrecognised date (%F)";

                    // day of the month [01..31]
                    if (/^0[1-9]|[1-2][0-9]|3[0-1]/.test(s)) {
                        dt.day = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised date (%F)";
                }
                else if (placeholder == "%H") {
                    // hour [00..23]
                    if (/^[0-1][0-9]|2[0-3]/.test(s)) {
                        dt.hour = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised hour (%H)";
                }
                else if (placeholder == "%I") {
                    // hour [01..12]
                    if (/^0[1-9]|1[0-2]/.test(s)) {
                        dt.hourAmPm = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised hour (%I)";
                }
                else if (placeholder == "%k") {
                    // hour [0..23]
                    var h = s.match(/^(\d{1,2})/);
                    dt.hour = parseInt(h, 10);

                    if (isNaN(dt.hour) || dt.hour < 0 || dt.hour > 23)
                        throw "Parse error: Unrecognised hour (%k)";

                    s = s.substring(h.length);
                }
                else if (placeholder == "%l") {
                    // hour AM/PM [1..12]
                    var h = s.match(/^(\d{1,2})/);
                    dt.hourAmPm = parseInt(h, 10);

                    if (isNaN(dt.hourAmPm) || dt.hourAmPm < 1 || dt.hourAmPm > 12)
                        throw "Parse error: Unrecognised hour (%l)";

                    s = s.substring(h.length);
                }
                else if (placeholder == "%m") {
                    // month [01..12]
                    if (/^0[1-9]|1[0-2]/.test(s)) {
                        dt.month = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised month (%m)";
                }
                else if (placeholder == "%M") {
                    // minute [00..59]
                    if (/^[0-5][0-9]/.test(s)) {
                        dt.minute = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised minute (%M)";
                }
                else if (placeholder == "%n") {
                    // new line

                    if (s.charAt(0) == "\n")
                        s = s.substring(1);
                    else
                        throw "Parse error: Unrecognised new line (%n)";
                }
                else if (placeholder == "%p") {
                    // locale's equivalent of AM/PM
                    if (jsworld._stringStartsWith(s, this.lc.am)) {
                        dt.am = true;
                        s = s.substring(this.lc.am.length);
                    }
                    else if (jsworld._stringStartsWith(s, this.lc.pm)) {
                        dt.am = false;
                        s = s.substring(this.lc.pm.length);
                    }
                    else
                        throw "Parse error: Unrecognised AM/PM value (%p)";
                }
                else if (placeholder == "%P") {
                    // same as %p but forced lower case
                    if (jsworld._stringStartsWith(s, this.lc.am.toLowerCase())) {
                        dt.am = true;
                        s = s.substring(this.lc.am.length);
                    }
                    else if (jsworld._stringStartsWith(s, this.lc.pm.toLowerCase())) {
                        dt.am = false;
                        s = s.substring(this.lc.pm.length);
                    }
                    else
                        throw "Parse error: Unrecognised AM/PM value (%P)";
                }
                else if (placeholder == "%R") {
                    // same as %H:%M

                    // hour [00..23]
                    if (/^[0-1][0-9]|2[0-3]/.test(s)) {
                        dt.hour = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised time (%R)";

                    // :
                    if (jsworld._stringStartsWith(s, ":"))
                        s = s.substring(1);
                    else
                        throw "Parse error: Unrecognised time (%R)";

                    // minute [00..59]
                    if (/^[0-5][0-9]/.test(s)) {
                        dt.minute = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised time (%R)";

                }
                else if (placeholder == "%S") {
                    // second [00..59]
                    if (/^[0-5][0-9]/.test(s)) {
                        dt.second = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised second (%S)";
                }
                else if (placeholder == "%T") {
                    // same as %H:%M:%S

                    // hour [00..23]
                    if (/^[0-1][0-9]|2[0-3]/.test(s)) {
                        dt.hour = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised time (%T)";

                    // :
                    if (jsworld._stringStartsWith(s, ":"))
                        s = s.substring(1);
                    else
                        throw "Parse error: Unrecognised time (%T)";

                    // minute [00..59]
                    if (/^[0-5][0-9]/.test(s)) {
                        dt.minute = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised time (%T)";

                    // :
                    if (jsworld._stringStartsWith(s, ":"))
                        s = s.substring(1);
                    else
                        throw "Parse error: Unrecognised time (%T)";

                    // second [00..59]
                    if (/^[0-5][0-9]/.test(s)) {
                        dt.second = parseInt(s.substring(0, 2), 10);
                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised time (%T)";
                }
                else if (placeholder == "%w") {
                    // weekday [0..6]
                    if (/^\d/.test(s)) {
                        dt.weekday = parseInt(s.substring(0, 1), 10);
                        s = s.substring(1);
                    }
                    else
                        throw "Parse error: Unrecognised weekday number (%w)";
                }
                else if (placeholder == "%y") {
                    // year [00..99]
                    if (/^\d\d/.test(s)) {
                        var year2digits = parseInt(s.substring(0, 2), 10);

                        // this conversion to year[nnnn] is arbitrary!!!
                        if (year2digits > 50)
                            dt.year = 1900 + year2digits;
                        else
                            dt.year = 2000 + year2digits;

                        s = s.substring(2);
                    }
                    else
                        throw "Parse error: Unrecognised year (%y)";
                }
                else if (placeholder == "%Y") {
                    // year [nnnn]
                    if (/^\d\d\d\d/.test(s)) {
                        dt.year = parseInt(s.substring(0, 4), 10);
                        s = s.substring(4);
                    }
                    else
                        throw "Parse error: Unrecognised year (%Y)";
                }

                else if (placeholder == "%Z") {
                    // time-zone place holder is not supported

                    if (fmtSpec.length === 0)
                        break; // ignore rest of fmt spec
                }

                // remove the spec placeholder that was just parsed
                fmtSpec = fmtSpec.substring(2);
            }
            else {
                // If we don't have a placeholder, the chars
                // at pos. 0 of format spec and parsed string must match

                // Note: Space chars treated 1:1 !

                if (fmtSpec.charAt(0) != s.charAt(0))
                    throw "Parse error: Unexpected symbol \"" + s.charAt(0) + "\" in date/time string";

                fmtSpec = fmtSpec.substring(1);
                s = s.substring(1);
            }
        }

        // parsing finished, return composite date/time object
        return dt;
    };
};

/**
 * @class
 * Class for parsing localised currency amount strings.
 *
 * @public
 * @constructor
 * @description Creates a new monetary parser for the specified locale.
 *
 * @param {jsworld.Locale} locale A locale object specifying the required
 *        POSIX LC_MONETARY formatting properties.
 *
 * @throws Error on constructor failure.
 */
jsworld.MonetaryParser = function (locale) {

    if (typeof locale != "object" || locale._className != "jsworld.Locale")
        throw "Constructor error: You must provide a valid jsworld.Locale instance";

    this.lc = locale;

    /**
     * @public
     *
     * @description Parses a currency amount string formatted according to
     * the preset locale. Leading and trailing whitespace is ignored; the
     * amount may also be formatted without thousands separators. Both
     * the local (shorthand) symbol and the ISO 4217 code are accepted to
     * designate the currency in the formatted amount.
     *
     * @param {String} formattedCurrency The formatted currency amount.
     *
     * @returns {Number} The parsed amount.
     *
     * @throws Error on a parse exception.
     */
    this.parse = function (formattedCurrency) {

        if (typeof formattedCurrency != "string")
            throw "Parse error: Argument must be a string";

        // Detect the format type and remove the currency symbol
        var symbolType = this._detectCurrencySymbolType(formattedCurrency);

        var formatType, s;

        if (symbolType == "local") {
            formatType = "local";
            s = formattedCurrency.replace(this.lc.getCurrencySymbol(), "");
        }
        else if (symbolType == "int") {
            formatType = "int";
            s = formattedCurrency.replace(this.lc.getIntCurrencySymbol(), "");
        }
        else if (symbolType == "none") {
            formatType = "local"; // assume local
            s = formattedCurrency;
        }
        else
            throw "Parse error: Internal assert failure";

        // Remove any thousands separators
        s = jsworld._stringReplaceAll(s, this.lc.mon_thousands_sep, "");

        // Replace any local radix char with JavaScript's "."
        s = s.replace(this.lc.mon_decimal_point, ".");

        // Remove all whitespaces
        s = s.replace(/\s*/g, "");

        // Remove any local non-negative sign
        s = this._removeLocalNonNegativeSign(s, formatType);

        // Replace any local minus sign with JavaScript's "-" and put
        // it in front of the amount if necessary
        // (special parentheses rule checked too)
        s = this._normaliseNegativeSign(s, formatType);

        // Finally, we should be left with a bare parsable decimal number
        if (jsworld._isNumber(s))
            return parseFloat(s, 10);
        else
            throw "Parse error: Invalid currency amount string";
    };

    /**
     * @private
     *
     * @description Tries to detect the symbol type used in the specified
     *              formatted currency string: local(shorthand),
     *              international (ISO-4217 code) or none.
     *
     * @param {String} formattedCurrency The the formatted currency string.
     *
     * @return {String} With possible values "local", "int" or "none".
     */
    this._detectCurrencySymbolType = function (formattedCurrency) {

        // Check for whichever sign (int/local) is longer first
        // to cover cases such as MOP/MOP$ and ZAR/R

        if (this.lc.getCurrencySymbol().length > this.lc.getIntCurrencySymbol().length) {

            if (formattedCurrency.indexOf(this.lc.getCurrencySymbol()) != -1)
                return "local";
            else if (formattedCurrency.indexOf(this.lc.getIntCurrencySymbol()) != -1)
                return "int";
            else
                return "none";
        }
        else {
            if (formattedCurrency.indexOf(this.lc.getIntCurrencySymbol()) != -1)
                return "int";
            else if (formattedCurrency.indexOf(this.lc.getCurrencySymbol()) != -1)
                return "local";
            else
                return "none";
        }
    };

    /**
     * @private
     *
     * @description Removes a local non-negative sign in a formatted
     * currency string if it is found. This is done according to the
     * locale properties p_sign_posn and int_p_sign_posn.
     *
     * @param {String} s The input string.
     * @param {String} formatType With possible values "local" or "int".
     *
     * @returns {String} The processed string.
     */
    this._removeLocalNonNegativeSign = function (s, formatType) {

        s = s.replace(this.lc.positive_sign, "");

        // check for enclosing parentheses rule
        if (((formatType == "local" && this.lc.p_sign_posn === 0) ||
            (formatType == "int" && this.lc.int_p_sign_posn === 0)    ) &&
            /\(\d+\.?\d*\)/.test(s)) {
            s = s.replace("(", "");
            s = s.replace(")", "");
        }

        return s;
    };

    /**
     * @private
     *
     * @description Replaces a local negative sign with the standard
     * JavaScript minus ("-") sign placed in the correct position
     * (preceding the amount). This is done according to the locale
     * properties for negative sign symbol and relative position.
     *
     * @param {String} s The input string.
     * @param {String} formatType With possible values "local" or "int".
     *
     * @returns {String} The processed string.
     */
    this._normaliseNegativeSign = function (s, formatType) {

        // replace local negative symbol with JavaScript's "-"
        s = s.replace(this.lc.negative_sign, "-");

        // check for enclosing parentheses rule and replace them
        // with negative sign before the amount
        if ((formatType == "local" && this.lc.n_sign_posn === 0) ||
            (formatType == "int" && this.lc.int_n_sign_posn === 0)) {

            if (/^\(\d+\.?\d*\)$/.test(s)) {

                s = s.replace("(", "");
                s = s.replace(")", "");
                return "-" + s;
            }
        }

        // check for rule negative sign succeeding the amount
        if (formatType == "local" && this.lc.n_sign_posn == 2 ||
            formatType == "int" && this.lc.int_n_sign_posn == 2) {

            if (/^\d+\.?\d*-$/.test(s)) {
                s = s.replace("-", "");
                return "-" + s;
            }
        }

        // check for rule cur. sym. succeeds and sign adjacent
        if (formatType == "local" && this.lc.n_cs_precedes === 0 && this.lc.n_sign_posn == 3 ||
            formatType == "local" && this.lc.n_cs_precedes === 0 && this.lc.n_sign_posn == 4 ||
            formatType == "int" && this.lc.int_n_cs_precedes === 0 && this.lc.int_n_sign_posn == 3 ||
            formatType == "int" && this.lc.int_n_cs_precedes === 0 && this.lc.int_n_sign_posn == 4) {

            if (/^\d+\.?\d*-$/.test(s)) {
                s = s.replace("-", "");
                return "-" + s;
            }
        }

        return s;
    };
};

// end-of-file