index.js (178 lines of code) (raw):

/* * decimalformat 0.1.0 * Copyright (c) 2018 * https://github.com/DeloitteDigitalAPAC/DecimalFormat/ * Licensed under the BSD 3-Clause license. */ 'use strict'; const parser = require('./dist/parser'); class DecimalFormat { /** @constructor @param {string} pattern - Passed directly to the parse() method */ constructor(pattern) { this.parse(pattern); } /* Pads a string to the provided length @param {string} str @param {number} length @param {string} pad */ static padStart(str, length, pad = ' ') { if (str.length > length) { return str } else { length = length - str.length; if (length > pad.length) { pad += pad.repeat(length / pad.length); } return pad.slice(0, length) + str; } }; /** Counts the number of occurances of a string within another string. @param {string} str - String to search within @param {string} search - String to search for */ static count(str, search) { return str.split(search).length - 1; } /** Reverses a string @param {string} str */ static reverseString(str) { return str.split('').reverse().join(''); } /** Returns whether a number is negative; specifically testing for negative zero. @param {number} n */ static isNegative(n) { return n === 0 ? (1 / n < 0) : (n < 0); } /** Parses a pattern. @throws Will throw when the pattern cannot be parsed. @param {string} pattern */ parse(pattern) { this.pattern = pattern; this.parsed = parser.parse(pattern); const percentageInPrefix = DecimalFormat.count(this.parsed.positive.prefix, '%'); const percentageInSuffix = DecimalFormat.count(this.parsed.positive.suffix, '%'); if ((percentageInPrefix + percentageInSuffix) > 1) { throw new Error('Too many percent/per mille characters in pattern'); } // Explicitly check for the pattern '.E0' if (this.minimumIntegerDigits === 0 && this.minimumFractionDigits === 0 && this.hasMantissa) { throw new Error('At least one integer or fraction digit is required for exponential patterns'); } // If a negative pattern is not explicitly provided, use a copy of the // positive pattern. if (this.parsed.negative === null) { this.parsed.negative = Object.assign({}, this.parsed.positive); } // Java adds a '-' character to the negative prefix, when the prefix // is identical to the positive prefix. if (this.parsed.negative.prefix === this.parsed.positive.prefix) { this.parsed.negative.prefix = '-' + this.parsed.negative.prefix; } } /** Applies grouping separators to a string. @param {string} str - String @param {number} size - Number of characters between each separator @param {string} separator - Separator */ static applyGrouping(str, size, separator) { const groupingMatch = new RegExp('(.{' + size + '})(?=.)', 'g'); str = DecimalFormat.reverseString(str).replace(groupingMatch, (match) => { return match + separator; }); return DecimalFormat.reverseString(str); } /** Formats a number with the pattern. @param {number} number - Number to format @param {object} Locale - Locale */ format(number, locale = {}) { const formatLocale = Object.assign({}, this.defaultLocale, locale); if (typeof number !== 'number') { throw new TypeError(`Argument must be a number, received: ${typeof number}`); } else if (this.hasMantissa) { return number.toExponential(); } else { const isNegative = DecimalFormat.isNegative(number); let [integerString, fractionString] = Math.abs(number * this.multiplier).toFixed(this.maximumFractionDigits).split('.'); if (this.isDecimalSeparatorAlwaysShown) { const optionalFractionalDigits = this.maximumFractionDigits - this.minimumFractionDigits; // Remove optional zeroes in fraction if (optionalFractionalDigits > 0) { const re = new RegExp('0{0,' + optionalFractionalDigits + '}$'); fractionString = fractionString.replace(re, ''); } // Remove the zero for matters like '#.0' with values like 0.5 if (this.minimumFractionDigits > 0) { if (this.minimumIntegerDigits === 0 && integerString === '0') { integerString = ''; } } // Add decimal separator if necessary if (fractionString && fractionString.length > 0) { fractionString = formatLocale.decimalSeparator + fractionString; } else if (optionalFractionalDigits === 0 && this.minimumFractionDigits === 0) { fractionString = formatLocale.decimalSeparator; } else { fractionString = ''; } } else { fractionString = ''; } // Minimum integer digits integerString = DecimalFormat.padStart(integerString, this.minimumIntegerDigits, '0'); // Grouping if (this.hasMantissa === false && this.groupingSize > 0) { integerString = DecimalFormat.applyGrouping(integerString, this.groupingSize, formatLocale.groupingSeparator); } // Prefix and suffix for positive and negative numbers, respectively. let prefix, suffix; // This function is used to specifically check for negative zero (-0), // which returns the negative pattern in Java. if (isNegative) { prefix = this.negativePrefix; suffix = this.negativeSuffix; } else { prefix = this.positivePrefix; suffix = this.positiveSuffix; } return prefix + integerString + fractionString + suffix; } } get defaultLocale() { return { currencySymbol: '', decimalSeparator: '', digit: '', exponentSeparator: '', groupingSeparator: '', minusSign: '', percent: '', perMill: '' }; } /** Whether the pattern forces a decimal separator to always be shown. */ get isDecimalSeparatorAlwaysShown() { return this.parsed.positive.decimalSeparatorAlwaysShown; } /** Whether the pattern includes an mantissa and exponent. */ get hasMantissa() { return this.parsed.positive.mantissa; } /** Grouping size, or zero if no grouping was specified in the pattern. */ get groupingSize() { return this.parsed.positive.groupingSize; } /** Minimum number of fractional digits specified in the pattern. */ get minimumFractionDigits() { return this.parsed.positive.minimumFractionDigits; } /** Maximum number of fractional digits specified in the pattern. */ get maximumFractionDigits() { return this.parsed.positive.maximumFractionDigits; } /** Minimum number of integer digits specified in the pattern. */ get minimumIntegerDigits() { return this.parsed.positive.minimumIntegerDigits; } /** Maximum number of integer digits specified in the pattern. */ get maximumIntegerDigits() { return this.parsed.positive.maximumIntegerDigits; } /** Symbol used in the pattern which affects the multiplier, or null if a modifier was not specified in the pattern. */ get multiplierSymbol() { const p = this.parsed.positive; if (p.prefix.indexOf('%') > -1) { return '%'; } else if (p.suffix.indexOf('%') > -1) { return '%'; } else { return null; } return null; } /** Multiplier (as an number) specified in the pattern, or 1 if no multiplier was specified. */ get multiplier() { const multiplierSymbol = this.multiplierSymbol; if (multiplierSymbol === '%') { return 100; } else { return 1; } } /** Text preceeding the negative pattern. */ get negativePrefix() { return this.parsed.negative.prefix; } /** Text proceeding the negative pattern. */ get negativeSuffix() { return this.parsed.negative.suffix; } /** Text preceeding the positive pattern. */ get positivePrefix() { return this.parsed.positive.prefix; } /** Text proceeding the positive pattern. */ get positiveSuffix() { return this.parsed.positive.suffix; } } module.exports = { DecimalFormat, parser };