blob: 0d515a0812ab38ae31e1f99e4b1ef3495f084c21 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014-2015 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
/**
* Provides formatting of numbers using java format pattern.
* <p>
* Compared to the java DecimalFormat the following pattern characters are not considered:
* <ul>
* <li>prefix and suffix</li>
* <li>E</li>
* <li>%</li>
* </ul>
*/
scout.DecimalFormat = function(locale, decimalFormatConfiguration) {
// format function will use these (defaults)
this.positivePrefix;
this.positiveSuffix;
this.negativePrefix;
this.negativeSuffix;
this.minusSign = locale.decimalFormatSymbols.minusSign;
this.groupingChar = locale.decimalFormatSymbols.groupingSeparator;
this.groupLength = 0;
this.decimalSeparatorChar = locale.decimalFormatSymbols.decimalSeparator;
this.zeroBefore = 1;
this.zeroAfter = 0;
this.allAfter = 0;
decimalFormatConfiguration = decimalFormatConfiguration || {};
this.pattern = decimalFormatConfiguration.pattern || locale.decimalFormatPatternDefault;
this.multiplier = decimalFormatConfiguration.multiplier || 1;
this.roundingMode = decimalFormatConfiguration.roundingMode || scout.numbers.RoundingMode.HALF_UP;
var SYMBOLS = scout.DecimalFormat.PATTERN_SYMBOLS;
// Check if there are separate subpatterns for positive and negative numbers ("PositivePattern;NegativePattern")
var split = this.pattern.split(SYMBOLS.patternSeparator);
// Use the first subpattern as positive prefix/suffix
var positivePatternParts = scout.DecimalFormat.findPatternParts(split[0]);
this.positivePrefix = positivePatternParts.prefix;
this.positiveSuffix = positivePatternParts.suffix;
this.numberPattern = positivePatternParts.number;
if (split.length > 1) {
// Yes, there is a negative subpattern
var negativePatternParts = scout.DecimalFormat.findPatternParts(split[1]);
this.negativePrefix = negativePatternParts.prefix;
this.negativeSuffix = negativePatternParts.suffix;
if (negativePatternParts.prefixMinusMask.indexOf(SYMBOLS.minusSign) !== -1) {
this.minusSignInNegativePrefix = true;
}
if (negativePatternParts.suffixMinusMask.indexOf(SYMBOLS.minusSign) !== -1) {
this.minusSignInNegativeSuffix = true;
}
// "number" part is ignored, positive and negative number pattern are the same
} else {
// No, there is no negative subpattern, so the positive prefix/suffix are used for both positive and negative numbers.
this.negativePrefix = this.positivePrefix;
this.negativeSuffix = this.positiveSuffix;
// Check if there is a minus sign in the prefix/suffix.
var prefixMinusSignIndex = positivePatternParts.prefixMinusMask.indexOf(SYMBOLS.minusSign);
if (prefixMinusSignIndex !== -1) {
// Yes, there is a minus sign in the prefix. Use this a negativePrefix and remove the minus sign from the posistivePrefix.
while (prefixMinusSignIndex !== -1) {
this.positivePrefix = replaceCharAt(this.positivePrefix, prefixMinusSignIndex, '');
this.negativePrefix = replaceCharAt(this.negativePrefix, prefixMinusSignIndex, this.minusSign);
prefixMinusSignIndex = positivePatternParts.prefixMinusMask.indexOf(SYMBOLS.minusSign, prefixMinusSignIndex + 1);
}
this.minusSignInNegativePrefix = true;
}
var suffixMinusSignIndex = positivePatternParts.suffixMinusMask.indexOf(SYMBOLS.minusSign);
if (suffixMinusSignIndex !== -1) {
// Yes, there is a minus sign in the suffix. Use this a negativeSuffix and remove the minus sign from the posistiveSuffix.
while (suffixMinusSignIndex !== -1) {
this.positiveSuffix = replaceCharAt(this.positiveSuffix, suffixMinusSignIndex, '');
this.negativeSuffix = replaceCharAt(this.negativePrefix, suffixMinusSignIndex, this.minusSign);
suffixMinusSignIndex = positivePatternParts.suffixMinusMask.indexOf(SYMBOLS.minusSign, suffixMinusSignIndex + 1);
}
this.minusSignInNegativeSuffix = true;
}
if (!this.minusSignInNegativePrefix && !this.minusSignInNegativeSuffix) {
// No, there is no minus sign in the prefix/suffix. Therefore, automatically prepend the minus sign to the prefix.
this.negativePrefix = this.minusSign + this.positivePrefix;
this.minusSignInNegativePrefix = true;
}
}
// find group length
var posDecimalSeparator = this.numberPattern.indexOf(SYMBOLS.decimalSeparator);
if (posDecimalSeparator === -1) {
posDecimalSeparator = this.numberPattern.length; // assume decimal separator at end
}
var posGroupingSeparator = this.numberPattern.lastIndexOf(SYMBOLS.groupingSeparator, posDecimalSeparator); // only search before decimal separator
if (posGroupingSeparator > 0) {
this.groupLength = posDecimalSeparator - posGroupingSeparator - 1;
}
this.numberPattern = this.numberPattern.replace(new RegExp('[' + SYMBOLS.groupingSeparator + ']', 'g'), '');
// split on decimal point
split = this.numberPattern.split(SYMBOLS.decimalSeparator);
// find digits before and after decimal point
this.zeroBefore = scout.strings.count(split[0], SYMBOLS.zeroDigit);
if (split.length > 1) { // has decimal point?
this.zeroAfter = scout.strings.count(split[1], SYMBOLS.zeroDigit);
this.allAfter = this.zeroAfter + scout.strings.count(split[1], SYMBOLS.digit);
}
// ----- Helper functions -----
function replaceCharAt(s, pos, replacement) {
return s.substring(0, pos) + replacement + s.substring(pos + 1);
}
};
/**
* Returns a number for the given numberString, if the string can be converted into a number.
* Throws an Error otherwise
*/
scout.DecimalFormat.prototype.parse = function(numberString) {
if (scout.strings.empty(numberString)) {
return null;
}
var pureNumber = this.normalizeString(numberString);
var number = Number(pureNumber);
if (isNaN(number)) {
throw new Error(numberString + ' is not a number (NaN)');
}
return number;
};
scout.DecimalFormat.prototype.normalizeString = function(numberString) {
if (scout.strings.empty(numberString)) {
return '';
}
var negativePrefixRegEx = new RegExp('^' + scout.strings.quote(this.negativePrefix));
var negativeSuffixRegEx = new RegExp(scout.strings.quote(this.negativeSuffix) + '$');
var minus = '';
if ((this.minusSignInNegativePrefix && negativePrefixRegEx.test(numberString)) ||
(this.minusSignInNegativeSuffix && negativeSuffixRegEx.test(numberString))) {
minus = '-';
}
return minus + numberString
.replace(new RegExp('^' + scout.strings.quote(this.positivePrefix)), '')
.replace(new RegExp(scout.strings.quote(this.positiveSuffix) + '$'), '')
.replace(negativePrefixRegEx, '')
.replace(negativeSuffixRegEx, '')
.replace(new RegExp('[' + this.groupingChar + ']', 'g'), '')
.replace(new RegExp('[' + this.decimalSeparatorChar + ']', 'g'), '.')
.replace(/\s/g, '');
};
scout.DecimalFormat.prototype.format = function(number, applyMultiplier) {
applyMultiplier = scout.nvl(applyMultiplier, true);
if (number === null || number === undefined) {
return null;
}
var prefix = this.positivePrefix;
var suffix = this.positiveSuffix;
// apply multiplier
if (applyMultiplier && this.multiplier !== 1) {
number *= this.multiplier;
}
// round
number = this.round(number);
// after decimal point
var after = '';
if (this.allAfter) {
after = number.toFixed(this.allAfter).split('.')[1];
for (var j = after.length - 1; j > this.zeroAfter - 1; j--) {
if (after[j] !== '0') {
break;
}
after = after.slice(0, -1);
}
if (after) { // did we find any non-zero characters?
after = this.decimalSeparatorChar + after;
}
}
// absolute value
if (number < 0) {
prefix = this.negativePrefix;
suffix = this.negativeSuffix;
number = -number;
}
// before decimal point
var before = Math.floor(number);
before = (before === 0) ? '' : String(before);
before = scout.strings.padZeroLeft(before, this.zeroBefore);
// group digits
if (this.groupLength) {
for (var i = before.length - this.groupLength; i > 0; i -= this.groupLength) {
before = before.substr(0, i) + this.groupingChar + before.substr(i);
}
}
// put together and return
return prefix + before + after + suffix;
};
/**
* Rounds a number according to the properties of the DecimalFormat.
*/
scout.DecimalFormat.prototype.round = function(number) {
return scout.numbers.round(number, this.roundingMode, this.allAfter);
};
/* --- STATIC HELPERS ------------------------------------------------------------- */
/**
* Literal (not localized!) pattern symbols as defined in http://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html
*/
scout.DecimalFormat.PATTERN_SYMBOLS = {
digit: '#',
zeroDigit: '0',
decimalSeparator: '.',
groupingSeparator: ',',
minusSign: '-',
patternSeparator: ';'
};
scout.DecimalFormat.ensure = function(locale, format) {
if (!format) {
return format;
}
if (format instanceof scout.DecimalFormat) {
return format;
}
return new scout.DecimalFormat(locale, format);
};
/**
* Returns an object with the properties 'prefix', 'number' and 'suffix'. Number contains
* the part of the pattern that consists of 'digit-like' characters. Prefix and suffix
* contain the literal part before and after the number part, respectively. Single quotes
* that can be used to escape characters are removed from the result.
*/
scout.DecimalFormat.findPatternParts = function(pattern) {
var result = {
prefix: '',
prefixMinusMask: '',
number: '',
suffix: '',
suffixMinusMask: ''
};
var SYMBOLS = scout.DecimalFormat.PATTERN_SYMBOLS;
// Pattern that matches digit-like characters
var r = new RegExp('[' + SYMBOLS.digit + SYMBOLS.zeroDigit + SYMBOLS.decimalSeparator + SYMBOLS.groupingSeparator + ']');
var escape = false;
var scope = 'PREFIX';
for (var i = 0; i < pattern.length; i++) {
var ch = pattern.charAt(i);
if (scope === 'PREFIX') {
// prefix
if (ch === '\'') { // toggle escape
if (escape && pattern.charAt(i - 1) === '\'') { // two consecutive ' are equal to one literal '
result.prefix += '\'';
result.prefixMinusMask += ' ';
}
escape = !escape;
continue;
} else if (!escape && r.test(ch)) { // digit-like character, belongs to 'number' part
scope = 'NUMBER';
} else { // part of prefix
result.prefix += ch;
result.prefixMinusMask += (ch === SYMBOLS.minusSign && !escape ? '-' : ' ');
continue;
}
}
if (scope === 'NUMBER') {
// number
if (r.test(ch)) { // digit-like character
result.number += ch;
continue;
} else { // number is finished, belongs to suffix
scope = 'SUFFIX';
}
}
if (scope === 'SUFFIX') {
// suffix
if (ch === '\'') { // toggle escape
if (escape && pattern.charAt(i - 1) === '\'') { // two consecutive ' are equal to one literal '
result.suffix += '\'';
result.suffixMinusMask += ' ';
}
escape = !escape;
continue;
} else { // part of suffix
result.suffix += ch;
result.suffixMinusMask += (ch === SYMBOLS.minusSign && !escape ? '-' : ' ');
}
}
}
return result;
};