funcunit
Version:
<!-- @hide title
1,828 lines (1,479 loc) • 126 kB
JavaScript
/**
* @fileoverview
*
* JsWorld
*
* <p>Javascript library for localised formatting and parsing of:
* <ul>
* <li>Numbers
* <li>Dates and times
* <li>Currency
* </ul>
*
* <p>The library classes are configured with standard POSIX locale definitions
* derived from Unicode's Common Locale Data Repository (CLDR).
*
* <p>Website: <a href="http://software.dzhuvinov.com/jsworld.html">JsWorld</a>
*
* @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.
*
* <p>Accepted formats:
*
* <ul>
* <li>YYYY-MM-DD HH:MM:SS
* <li>YYYYMMDD HHMMSS
* <li>YYYY-MM-DD HHMMSS
* <li>YYYYMMDD HH:MM:SS
* </ul>
*
* @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.
*
* <p>Accepted formats:
*
* <ul>
* <li>YYYY-MM-DD
* <li>YYYYMMDD
* </ul>
*
* @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.
*
* <p>Accepted formats:
*
* <ul>
* <li>HH:MM:SS
* <li>HHMMSS
* </ul>
*
* @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.
*
* <p>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.
*
* <p>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.
*
* <p>The locale properties follow the POSIX standards:
*
* <ul>
* <li><a href="http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap07.html#tag_07_03_04">POSIX LC_NUMERIC</a>
* <li><a href="http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap07.html#tag_07_03_03">POSIX LC_MONETARY</a>
* <li><a href="http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap07.html#tag_07_03_05">POSIX LC_TIME</a>
* </ul>
*
* @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:
*
* <p>0 No space separates the currency symbol and value.</p>
*
* <p>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.</p>
*
* <p>2 If the currency symbol and sign string are adjacent, a space
* separates them; otherwise, a space separates the sign string
* from the value.</p>
*
*
* @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:
*
* <p>0 Parentheses enclose the quantity and the currency_symbol.</p>
*
* <p>1 The sign string precedes the quantity and the currency_symbol.</p>
*
* <p>2 The sign string succeeds the quantity and the currency_symbol.</p>
*
* <p>3 The sign string precedes the currency_symbol.</p>
*
* <p>4 The sign string succeeds the currency_symbol.</p>
*
*
* @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.
*
* <p>See: <a href="http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap07.html#tag_07_03_04">
* POSIX LC_NUMERIC</a>.
*
*
* @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:
* <ul>
* <li>"^" suppress grouping
* <li>"+" force positive sign for positive amounts
* <li>"~" suppress positive/negative sign
* <li>".n" specify decimal precision 'n'
* </ul>
*
* @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.
*
* <p>See: <a href="http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap07.html#tag_07_03_05">
* POSIX LC_TIME</a>.
*
* @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.
* @para