UNPKG

@eclipse-scout/core

Version:
313 lines (281 loc) 11.5 kB
/* * Copyright (c) 2010, 2023 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {Locale, numbers, RoundingMode, scout, strings} from '../index'; /** * Provides formatting of numbers using java format pattern. * * Compared to the java DecimalFormat the following pattern characters are not considered: * - E * - % */ export class DecimalFormat { positivePrefix: string; positiveSuffix: string; negativePrefix: string; negativeSuffix: string; groupingChar: string; lenientGroupingChars: string; groupLength: number; decimalSeparatorChar: string; zeroBefore: number; zeroAfter: number; allAfter: number; pattern: string; multiplier: number; roundingMode: RoundingMode; constructor(locale: Locale, options?: string | DecimalFormatOptions) { // format function will use these (defaults) this.positivePrefix = ''; this.positiveSuffix = ''; this.negativePrefix = locale.decimalFormatSymbols.minusSign; this.negativeSuffix = ''; this.groupingChar = locale.decimalFormatSymbols.groupingSeparator; // we want to be lenient when it comes to grouping separators, try the locale default plus a few others this.lenientGroupingChars = '\'´`’' + // apostrophe and variations '\u00B7' + // middle dot '\u0020' + // space '\u00A0' + // no-break space '\u2009' + // thin space '\u202F'; // narrow no-break space this.groupLength = 0; this.decimalSeparatorChar = locale.decimalFormatSymbols.decimalSeparator; this.zeroBefore = 1; this.zeroAfter = 0; this.allAfter = 0; if (typeof options === 'string') { this.pattern = options; this.multiplier = 1; this.roundingMode = RoundingMode.HALF_UP; } else { options = options || {pattern: null}; this.pattern = this.pattern || options.pattern || locale.decimalFormatPatternDefault; this.multiplier = options.multiplier || 1; this.roundingMode = options.roundingMode || RoundingMode.HALF_UP; } let SYMBOLS = DecimalFormat.PATTERN_SYMBOLS; // Check if there are separate subpatterns for positive and negative numbers ("PositivePattern;NegativePattern") let split = this.pattern.split(SYMBOLS.patternSeparator); // Use the first subpattern as positive prefix/suffix let positivePrefixAndSuffix = findPrefixAndSuffix(split[0]); this.positivePrefix = positivePrefixAndSuffix.prefix; this.positiveSuffix = positivePrefixAndSuffix.suffix; if (split.length > 1) { // Yes, there is a negative subpattern let negativePrefixAndSuffix = findPrefixAndSuffix(split[1]); this.negativePrefix = negativePrefixAndSuffix.prefix; this.negativeSuffix = negativePrefixAndSuffix.suffix; // from now on, only look at the positive subpattern this.pattern = split[0]; } else { // No, there is no negative subpattern, so the positive prefix/suffix are used for both positive and negative numbers. // Check if there is a minus sign in the prefix/suffix. if (this.positivePrefix.indexOf(SYMBOLS.minusSign) !== -1 || this.positiveSuffix.indexOf(SYMBOLS.minusSign) !== -1) { // Yes, there is a minus sign in the prefix/suffix. Use this a negativePrefix/Suffix and remove the minus sign from the posistivePrefix/Suffix. this.negativePrefix = this.positivePrefix.replace(SYMBOLS.minusSign, locale.decimalFormatSymbols.minusSign); this.negativeSuffix = this.positiveSuffix.replace(SYMBOLS.minusSign, locale.decimalFormatSymbols.minusSign); this.positivePrefix = this.positivePrefix.replace(SYMBOLS.minusSign, ''); this.positiveSuffix = this.positiveSuffix.replace(SYMBOLS.minusSign, ''); } else { // No, there is no minus sign in the prefix/suffix. Therefore, use the default negativePrefix/Suffix, but append the positivePrefix/Suffix this.negativePrefix = this.positivePrefix + this.negativePrefix; this.negativeSuffix = this.negativeSuffix + this.positiveSuffix; } } // find group length let posDecimalSeparator = this.pattern.indexOf(SYMBOLS.decimalSeparator); if (posDecimalSeparator === -1) { posDecimalSeparator = this.pattern.length; // assume decimal separator at end } let posGroupingSeparator = this.pattern.lastIndexOf(SYMBOLS.groupingSeparator, posDecimalSeparator); // only search before decimal separator if (posGroupingSeparator > 0) { this.groupLength = posDecimalSeparator - posGroupingSeparator - 1; } this.pattern = this.pattern.replace(new RegExp('[' + SYMBOLS.groupingSeparator + ']', 'g'), ''); // split on decimal point split = this.pattern.split(SYMBOLS.decimalSeparator); // find digits before and after decimal point this.zeroBefore = strings.count(split[0], SYMBOLS.zeroDigit); if (split.length > 1) { // has decimal point? this.zeroAfter = strings.count(split[1], SYMBOLS.zeroDigit); this.allAfter = this.zeroAfter + strings.count(split[1], SYMBOLS.digit); } // Returns an object with the properties 'prefix' and 'suffix', which contain all characters // before or after any 'digit-like' character in the given pattern string. function findPrefixAndSuffix(pattern) { let result = { prefix: '', suffix: '' }; // Find prefix (anything before the first 'digit-like' character) let digitLikeCharacters = SYMBOLS.digit + SYMBOLS.zeroDigit + SYMBOLS.decimalSeparator + SYMBOLS.groupingSeparator; let r = new RegExp('^(.*?)[' + digitLikeCharacters + '].*$'); let matches = r.exec(pattern); if (matches !== null) { // Ignore single quotes (for special, quoted characters - e.g. Java quotes percentage sign like '%') result.prefix = matches[1].replace(new RegExp('\'([^\']+)\'', 'g'), '$1'); } // Find suffix (anything before the first 'digit-like' character) r = new RegExp('^.*[' + digitLikeCharacters + '](.*?)$'); matches = r.exec(pattern); if (matches !== null) { // Ignore single quotes (for special, quoted characters - e.g. Java quotes percentage sign like '%') result.suffix = matches[1].replace(new RegExp('\'([^\']+)\'', 'g'), '$1'); } return result; } } /** * Converts the numberString into a number and applies the multiplier. * @param evaluateNumberFunction optional function for custom evaluation. The function gets a normalized string and has to return a Number * @returns A number for the given numberString, if the string can be converted into a number. Throws an Error otherwise */ parse(numberString: string, evaluateNumberFunction?: (normalizedNumberString: string) => number): number { if (strings.empty(numberString)) { return null; } let normalizedNumberString = this.normalize(numberString); evaluateNumberFunction = evaluateNumberFunction || Number; let number = evaluateNumberFunction(normalizedNumberString); if (isNaN(number)) { throw new Error(numberString + ' is not a number (NaN)'); } if (this.multiplier !== 1) { number /= this.multiplier; } return number; } format(number: number, applyMultiplier?: boolean): string { if (number === null || number === undefined) { return null; } let prefix = this.positivePrefix; let suffix = this.positiveSuffix; // apply multiplier applyMultiplier = scout.nvl(applyMultiplier, true); if (applyMultiplier && this.multiplier !== 1) { number *= this.multiplier; } // round number = this.round(number); // after decimal point let after = ''; if (this.allAfter) { after = number.toFixed(this.allAfter).split('.')[1]; for (let 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 let b = Math.floor(number); let before = (b === 0) ? '' : String(b); before = strings.padZeroLeft(before, this.zeroBefore); // group digits if (this.groupLength) { for (let 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. */ round(number: number, applyMultiplier?: boolean): number { applyMultiplier = scout.nvl(applyMultiplier, true); if (number === null || number === undefined) { return null; } // apply multiplier if (applyMultiplier && this.multiplier !== 1) { number *= this.multiplier; } // round number = numbers.round(number, this.roundingMode, this.allAfter); // un-apply multiplier if (applyMultiplier && this.multiplier !== 1) { number /= this.multiplier; } return number; } /** * Convert to JS number format: * - remove groupingChar and lenientGroupingChars * - replace decimalSeparatorChar with '.' * - remove positiveSuffix and negativeSuffix * - replace positivePrefix with '+' * - replace negativePrefix with '-' */ normalize(numberString: string): string { if (!numberString) { return numberString; } let result = numberString .replace(new RegExp('[' + this.groupingChar + this.lenientGroupingChars + ']', 'g'), '') .replace(new RegExp('[' + this.decimalSeparatorChar + ']', 'g'), '.'); if (strings.hasText(this.positivePrefix)) { result = result.replace(new RegExp(this.positivePrefix, 'g'), '+'); } if (strings.hasText(this.positiveSuffix)) { result = result.replace(new RegExp(this.positiveSuffix, 'g'), ''); } if (strings.hasText(this.negativePrefix)) { result = result.replace(new RegExp(this.negativePrefix, 'g'), '-'); } if (strings.hasText(this.negativeSuffix)) { result = result.replace(new RegExp(this.negativeSuffix, 'g'), ''); } return result.replace(/\s/g, ''); } /* --- STATIC HELPERS ------------------------------------------------------------- */ /** * Literal (not localized!) pattern symbols as defined in http://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html */ static PATTERN_SYMBOLS = { digit: '#', zeroDigit: '0', decimalSeparator: '.', groupingSeparator: ',', minusSign: '-', patternSeparator: ';' } as const; static ensure(locale: Locale, format: DecimalFormat | string | DecimalFormatOptions): DecimalFormat { if (!format) { return format as DecimalFormat; } if (format instanceof DecimalFormat) { return format; } return new DecimalFormat(locale, format); } } export interface DecimalFormatOptions { pattern: string; /** * default is 1 */ multiplier?: number; /** * default is {@link RoundingMode.HALF_UP} */ roundingMode?: RoundingMode; }