Skip to content

Commit

Permalink
API changes
Browse files Browse the repository at this point in the history
===
- Added new converter @opuscapita/i18n/NumberConverter
- Added method for `I18nManager.formatBigNumber(<string>) <string>`
- Added method for `I18nManager.parseBigNumber(<string>) <string>`
- Added method for `I18nManager.formatBigDecimalNumber(<string>) <string>`
- Added method for `I18nManager.parseBigDecimalNumber(<string>) <string>`
- Added method for `I18nManager.formatBigDecimalNumberWithPattern(<string>) <string>`
  • Loading branch information
ddivin-sc committed Apr 9, 2020
1 parent ec57dbd commit 8787baa
Show file tree
Hide file tree
Showing 8 changed files with 453 additions and 16 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ i18n.formatDate(new Date(2001, 0, 10)) // returns '10/01/2001'
i18n.parseDate('10/01/2001').toISOString() // returns new Date(2001, 0, 10).toISOString()
i18n.formatDateTime(new Date(2001, 0, 10)) // returns '10/01/2001 00:00:00'

//format and parse numbers
i18n.formatNumber(10000) // returns '10,000'
i18n.parseNumber('10,000')// returns 10000

Expand All @@ -205,6 +206,15 @@ i18n.parseDecimalNumber('10,000.00') // returns 10000
// Wraps decimal number converter but allows the use of custom patterns
i18n.formatDecimalNumberWithPattern(10000, '#,##0.000000') // returns 10,000.000000

//format and parse big numbers
i18n.formatBigNumber('10000') // returns '10,000'
i18n.parseBigNumber('10,000')// returns '10000'

i18n.formatBigDecimalNumber('10000') // returns '10,000.00'
i18n.parseBigDecimalNumber('10,000.00') // returns '10000'

i18n.formatBigDecimalNumberWithPattern('10000', '#,##0.000000') // returns '10,000.000000'

// getting date format
i18n.dateFormat // returns 'dd/MM/yyyy'
```
Expand Down Expand Up @@ -235,6 +245,19 @@ nc.valueToString(10000000) // returns '10,000,000.00'
nc.stringToValue('10,000.00') // returns 10000
```

**BigNumberConverter**

The converter format from and parse to only string values, because number package big numbers in JavaScript is not safety operation.
Format definition is similar to Java's [_DecimalFormat_](https://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html) class, but **exponent is not supported**

```javascript
import BigNumberConverter from '@opuscapita/i18n/BigNumberConverter';

let nc = new BigNumberConverter('#,##0.00', ',', '.'); // format, groupSep, decSep, decSepUseAlways = false
nc.valueToString('9007199254740989.07') // returns '9,007,199,254,740,989.07'
nc.stringToValue('9,007,199,254,740,989.07') // returns '9007199254740989.07'
```

**Strip to null converter**

```javascript
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"repository": "OpusCapita/i18n",
"license": "Apache-2.0",
"dependencies": {
"bignumber.js": "9.0.0",
"flat": "2.0.1",
"lodash": "4.17.11",
"properties": "1.2.1",
Expand Down
193 changes: 193 additions & 0 deletions src/converters/BigNumberConverter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import Converter from './Converter';
import ParseError from './ParseError';
const BigNumber = require('bignumber.js');

export const ERROR_CODE = 'error.parse.number';

const floatNumberReg = /^-?\d+\.?\d*$/;
const intNumberReg = /^-?\d+$/;

export default class BigNumberConverter extends Converter {
constructor(format, groupSep, decSep, decSepUseAlways) {
super();

this._format = format;
this._groupSep = groupSep;
this._decSep = decSep;
this._decSepUseAlways = decSepUseAlways || false;

if (format.lastIndexOf('.') !== -1) {
this._integerFormat = format.substring(0, format.indexOf('.'));
this._decimalFormat = format.substring(format.indexOf('.') + 1);
} else {
this._integerFormat = format;
this._decimalFormat = '';
}
}

_validateStringIfItIsANumber(value) {
let stringValue = value;
if (this._groupSep) {
stringValue = stringValue.replace(new RegExp('\\' + this._groupSep, 'g'), '');
}

if (this._decSep !== undefined) {
stringValue = stringValue.replace(this._decSep, '.');
}

if (this._format.indexOf('.') !== -1) {
if (!floatNumberReg.test(stringValue)) {
throw new ParseError(ERROR_CODE, { value });
}
} else {
if (!intNumberReg.test(stringValue)) {
throw new ParseError(ERROR_CODE, { value });
}
}
}

_parseFractionalPart(bigNumber) {
// nothing to format
if (this._decimalFormat === '') {
return '';
}

const fractionalPartString = bigNumber.toFixed().split('.')[1] || '';

let result = '';
for (let i = 0; i < this._decimalFormat.length; i++) {
const currentDigit = fractionalPartString.charAt(i);
if (this._decimalFormat.charAt(i) === '0') {
// char does not exist
if (currentDigit === '') {
// add 0 anyway
result = `${result}0`;
} else {
result = `${result}${currentDigit}`;
}
} else {
// # is found in the pattern
const leftOptionalDigitsAmount = this._decimalFormat.length - i;
// take all left digits statring from i index but not more that amount of characters left in format
result = `${result}${fractionalPartString.substr(i, leftOptionalDigitsAmount)}`;
break;
}
}

return result;
}

_parseIntegerPart(bigNumber) {
let integerNumber = bigNumber;
// if there is not decimal separator in the format, then we round the value
// like if it done in DecimalFormat, see https://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html
if (this._format.indexOf('.') === -1) {
integerNumber = integerNumber.integerValue(BigNumber.ROUND_HALF_UP);
}

// cut fractional part
if (bigNumber.isNegative()) {
integerNumber = integerNumber.integerValue(BigNumber.ROUND_CEIL);
} else {
integerNumber = integerNumber.integerValue(BigNumber.ROUND_FLOOR);
}

if (this._integerFormat.charAt(this._integerFormat.length - 1) === '#' && integerNumber.isZero() === 0) {
return 0;
}

let result = '';

// convert number ot a string and cut - sign if any
const integerPartWithoutSign = integerNumber.abs().toFixed();

// find how many digits are in the group
let groupLength = 9999;
const groupSeparatorIndexInFormat = this._integerFormat.lastIndexOf(',');
if (groupSeparatorIndexInFormat !== -1) {
groupLength = this._integerFormat.length - groupSeparatorIndexInFormat - 1;
}

let groupCount = 0;
for (let k = integerPartWithoutSign.length - 1; k >= 0; k--) {
result = integerPartWithoutSign.charAt(k) + result;
groupCount++;
if (groupCount === groupLength && k !== 0) {
result = (this._groupSep || '') + result;
groupCount = 0;
}
}

// account for any pre-data 0's
if (this._integerFormat.length > result.length) {
const padStart = this._integerFormat.indexOf('0');
if (padStart !== -1) {
const padLen = this._integerFormat.length - padStart;

// pad to left with 0's
while (result.length < padLen) {
result = '0' + result;
}
}
}

return result;
}

valueToString(numberAsString) {
// null -> null is returned
if (numberAsString === null) {
return null;
}

// throw TypeError if value is not a string
if (typeof numberAsString !== 'string') {
throw TypeError(`'${numberAsString}' is not a String!`);
}

const bigNumber = new BigNumber(numberAsString);

if (bigNumber.isNaN()) {
throw TypeError(`'${numberAsString}' is not a Number!`);
}

// validate integer number
// parse integrer and fractional part separately
const integerPartString = this._parseIntegerPart(bigNumber);
const fractionalPartString = this._parseFractionalPart(bigNumber);

// setup decimal separator if it is needed
let decimalSeparator = '';
if (fractionalPartString !== '' || this._decSepUseAlways) {
decimalSeparator = this._decSep;
}
// setup negative sign
let minusSign = '';
if (bigNumber.isNegative()) {
minusSign = '-';
}

return minusSign + integerPartString + decimalSeparator + fractionalPartString;
}

stringToValue(string) {
if (string === null) {
return null;
}

if (typeof string !== 'string') {
throw TypeError(`'${string}' is not a String!`);
}

this._validateStringIfItIsANumber(string);

// removing decimal and grouping separator
let stringValue = string;
if (this._groupSep) {
while (stringValue.indexOf(this._groupSep) > -1) {
stringValue = stringValue.replace(this._groupSep, '');
}
}
return new BigNumber(stringValue.replace(this._decSep, '.')).toFixed().toString();
}
}
134 changes: 134 additions & 0 deletions src/converters/BigNumberConverter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { assert } from 'chai';

import BigNumberConverter from './BigNumberConverter';
import ParseError from './ParseError';

describe('BigNumberConverter', () => {
it('should decimal format with group separator', () => {
let dc = new BigNumberConverter('#,##0.00', ',', '.');

assert.strictEqual(dc.valueToString(null), null);
assert.strictEqual(dc.valueToString('10000000'), '10,000,000.00');
assert.strictEqual(dc.valueToString('-10000'), '-10,000.00');
assert.strictEqual(dc.valueToString('1100.99'), '1,100.99');
assert.throws(() => {return dc.valueToString(12345)}, TypeError, `'12345' is not a String!`);
assert.throws(() => {return dc.valueToString('werwe')}, TypeError, `'werwe' is not a Number!`);

assert.strictEqual(dc.stringToValue(null), null);
assert.strictEqual(dc.stringToValue('10,000.00'), '10000');
assert.strictEqual(dc.stringToValue('-10,000.00'), '-10000');
assert.strictEqual(dc.stringToValue('1,100.99'), '1100.99');
assert.throws(() => {return dc.stringToValue(12345)}, TypeError, `'12345' is not a String!`);
});

it('should decimal format', () => {
let dc = new BigNumberConverter('#,##0.00', null, '.');

assert.strictEqual(dc.valueToString('10000'), '10000.00');
assert.strictEqual(dc.valueToString('-10000'), '-10000.00');
assert.strictEqual(dc.valueToString('1100.99'), '1100.99');

assert.strictEqual(dc.stringToValue('10000.00'), '10000');
assert.strictEqual(dc.stringToValue('-10000.00'), '-10000');
assert.strictEqual(dc.stringToValue('1100.99'), '1100.99');

const badValue = '10,000.00';
assert.throws(() => {
dc.stringToValue(badValue);
}, ParseError, `invalid parsed value [${badValue}]`);
});

it('should decimal format with space group separator', () => {
let dc = new BigNumberConverter('#,##0.00', ' ', ',');

assert.strictEqual(dc.valueToString('10000'), '10 000,00');
assert.strictEqual(dc.valueToString('-10000'), '-10 000,00');
assert.strictEqual(dc.valueToString('1100.55'), '1 100,55');
assert.strictEqual(dc.valueToString('-1.1'), '-1,10');
assert.strictEqual(dc.valueToString('-1.9'), '-1,90');

assert.strictEqual(dc.stringToValue('10 000,00'), '10000');
assert.strictEqual(dc.stringToValue('-10 000,00'), '-10000');
assert.strictEqual(dc.stringToValue('1 100,99'), '1100.99');

const invalidValues = ['test', 'test1321321', '10,000.00', '5435432test'];

let convertToNumber = (value) => {
return () => {
return dc.stringToValue(value);
}
};
for (const value of invalidValues) {
assert.throws(convertToNumber(value), ParseError, `invalid parsed value [${value}]`);
}
});

it('should integer format with ` group separator', () => {
// formatting integer values
let dc = new BigNumberConverter('#,##0', '`');

assert.strictEqual(dc.valueToString('10000'), '10`000');
assert.strictEqual(dc.stringToValue('10`000'), '10000');

assert.strictEqual(dc.valueToString('10000.99'), '10`001');

assert.throws(() => {
dc.stringToValue('10`000.99');
}, ParseError, 'invalid parsed value [10`000.99]');
});

it('should decimal format with custom decimal separator', () => {
let dc = new BigNumberConverter('#.##', null, ',');

assert.strictEqual(dc.valueToString('100'), '100');

assert.strictEqual(dc.stringToValue('1000'), '1000');
assert.strictEqual(dc.stringToValue('100000,1'), '100000.1');

assert.strictEqual(dc.valueToString('100.01'), '100,01');

assert.strictEqual(dc.valueToString('0.0'), '0');
assert.strictEqual(dc.valueToString('0'), '0');
});

it('should decimal format with always decimal separator', () => {
let dc = new BigNumberConverter('#.##', null, ',', true);

assert.strictEqual(dc.valueToString('100'), '100,');

assert.strictEqual(dc.stringToValue('1000'), '1000');
assert.strictEqual(dc.stringToValue('100000,1'), '100000.1');

assert.strictEqual(dc.valueToString('100.01'), '100,01');

assert.strictEqual(dc.valueToString('0.0'), '0,');
assert.strictEqual(dc.valueToString('0.1'), '0,1');
});

it('should zero first for integer format', () => {
let dc = new BigNumberConverter('00', null, '.');
assert.strictEqual(dc.valueToString('9'), '09');
});

it('should decimal format with custom format', () => {
let dc = new BigNumberConverter('#.##########', null, ',', false);
assert.strictEqual(dc.valueToString('1000000'), '1000000');
});

it('should decimal format with custom format and group separator', () => {
let dc = new BigNumberConverter('#,##0.0#########', ',', '.');
assert.strictEqual(dc.valueToString('123456789.12'), '123,456,789.12');
});

it('should format big number', () => {
let dc = new BigNumberConverter('#,##0.00', ',', '.');
assert.strictEqual(dc.valueToString('9007199254740989.07'), '9,007,199,254,740,989.07');
assert.strictEqual(dc.stringToValue('9,007,199,254,740,989.07'), '9007199254740989.07');

dc = new BigNumberConverter('#.00', null, '.');
assert.strictEqual(dc.valueToString('21321312312312344'), '21321312312312344.00');
assert.strictEqual(dc.stringToValue('21321312312312344.00'), '21321312312312344');
assert.strictEqual(dc.valueToString('99999999999999999.99'), '99999999999999999.99');
assert.strictEqual(dc.stringToValue('99999999999999999.99'), '99999999999999999.99');
});
});

0 comments on commit 8787baa

Please sign in to comment.