Skip to content

Commit 2cc82ff

Browse files
danielkaradachkigyoshev
authored andcommitted
feat: add currency accounting formatting
1 parent 7b11337 commit 2cc82ff

File tree

7 files changed

+127
-52
lines changed

7 files changed

+127
-52
lines changed

src/cldr/load-numbers.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
import { cldr } from './info';
2+
import { CURRENCY, ACCOUNTING, DECIMAL, CURRENCY_PLACEHOLDER, NUMBER_PLACEHOLDER, LIST_SEPARATOR, GROUP_SEPARATOR, POINT } from '../common/constants';
23

34
const LATIN_NUMBER_FORMATS = "Formats-numberSystem-latn";
45
const LATIN_NUMBER_SYMBOLS = "symbols-numberSystem-latn";
5-
const GROUP_SEPARATOR = ",";
6-
const LIST_SEPARATOR = ";";
7-
const DECIMAL_SEPARATOR = ".";
86

97
const patternRegExp = /([ #,0. ]+)/g;
108
const cldrCurrencyRegExp = /¤/g;
119

1210
function getPatterns(pattern) {
1311
patternRegExp.lastIndex = 0;
1412

15-
return pattern.replace(cldrCurrencyRegExp, "$").replace(patternRegExp, "n").split(";");
13+
return pattern.replace(cldrCurrencyRegExp, CURRENCY_PLACEHOLDER).replace(patternRegExp, NUMBER_PLACEHOLDER).split(LIST_SEPARATOR);
1614
}
1715

1816
function getGroupSize(pattern) {
1917
patternRegExp.lastIndex = 0;
2018

21-
const numberPatterns = patternRegExp.exec(pattern.split(LIST_SEPARATOR)[0])[0].split(DECIMAL_SEPARATOR);
19+
const numberPatterns = patternRegExp.exec(pattern.split(LIST_SEPARATOR)[0])[0].split(POINT);
2220
const integer = numberPatterns[0];
2321

2422
const groupSize = integer.split(GROUP_SEPARATOR).slice(1).map(function(group) {
@@ -31,7 +29,7 @@ function getGroupSize(pattern) {
3129
function loadCurrencyUnitPatterns(currencyInfo, currencyFormats) {
3230
for (let field in currencyFormats) {
3331
if (field.startsWith("unitPattern")) {
34-
currencyInfo[field] = currencyFormats[field].replace("{0}", "n").replace("{1}", "$");
32+
currencyInfo[field] = currencyFormats[field].replace("{0}", NUMBER_PLACEHOLDER).replace("{1}", CURRENCY_PLACEHOLDER);
3533
}
3634
}
3735
}
@@ -49,9 +47,13 @@ export default function loadNumbersInfo(locale, info) {
4947
numbers[style] = {
5048
patterns: getPatterns(pattern)
5149
};
52-
if (style === "currency") {
53-
numbers[style].groupSize = getGroupSize((info["decimal" + LATIN_NUMBER_FORMATS] || info[field]).standard);
50+
if (style === CURRENCY) {
51+
numbers[style].groupSize = getGroupSize((info[DECIMAL + LATIN_NUMBER_FORMATS] || info[field]).standard);
5452
loadCurrencyUnitPatterns(numbers[style], info[field]);
53+
numbers[ACCOUNTING] = {
54+
patterns: getPatterns(info[field][ACCOUNTING]),
55+
groupSize: numbers[style].groupSize
56+
};
5557
} else {
5658
numbers[style].groupSize = getGroupSize(pattern);
5759
}

src/common/constants.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ export const ACCOUNTING = "accounting";
44
export const PERCENT = "percent";
55
export const SCIENTIFIC = "scientific";
66

7-
//rename to placeholder
8-
export const CURRENCY_SYMBOL = "$";
9-
export const PERCENT_SYMBOL = "%";
10-
export const NUMBER_SYMBOL = "n";
7+
export const CURRENCY_PLACEHOLDER = "$";
8+
export const PERCENT_PLACEHOLDER = "%";
9+
export const NUMBER_PLACEHOLDER = "n";
1110

1211
export const LIST_SEPARATOR = ";";
1312
export const GROUP_SEPARATOR = ",";

src/numbers/format-number.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
import { localeInfo } from '../cldr';
2+
import { CURRENCY, ACCOUNTING, DECIMAL, PERCENT, SCIENTIFIC, DEFAULT_LOCALE, NUMBER_PLACEHOLDER, EMPTY } from '../common/constants';
23
import standardNumberFormat from './standard-number-format';
34
import customNumberFormat from './custom-number-format';
45

5-
const standardFormatRegExp = /^(n|c|p|e)(\d*)$/i;
6+
const standardFormatRegExp = /^(n|c|p|e|a)(\d*)$/i;
67

78
function standardFormatOptions(format) {
89
const formatAndPrecision = standardFormatRegExp.exec(format);
910

1011
if (formatAndPrecision) {
1112
const options = {
12-
style: "decimal"
13+
style: DECIMAL
1314
};
1415

1516
let style = formatAndPrecision[1].toLowerCase();
1617

1718
if (style === "c") {
18-
options.style = "currency";
19-
}
20-
21-
if (style === "p") {
22-
options.style = "percent";
23-
}
24-
25-
if (style === "e") {
26-
options.style = "scientific";
19+
options.style = CURRENCY;
20+
} else if (style === "a") {
21+
options.style = ACCOUNTING;
22+
} else if (style === "p") {
23+
options.style = PERCENT;
24+
} else if (style === "e") {
25+
options.style = SCIENTIFIC;
2726
}
2827

2928
if (formatAndPrecision[2]) {
@@ -45,9 +44,9 @@ function getFormatOptions(format) {
4544
return formatOptions;
4645
}
4746

48-
export default function formatNumber(number, format = "n", locale = "en") {
47+
export default function formatNumber(number, format = NUMBER_PLACEHOLDER, locale = DEFAULT_LOCALE) {
4948
if (number === undefined || number === null) {
50-
return "";
49+
return EMPTY;
5150
}
5251

5352
if (!isFinite(number)) {
@@ -59,7 +58,7 @@ export default function formatNumber(number, format = "n", locale = "en") {
5958

6059
let result;
6160
if (formatOptions) {
62-
const style = (formatOptions || {}).style || "decimal";
61+
const style = (formatOptions || {}).style || DECIMAL;
6362
result = standardNumberFormat(number, Object.assign({}, info.numbers[style], formatOptions), info);
6463
} else {
6564
result = customNumberFormat(number, format, info);

src/numbers/is-currency-style.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { CURRENCY, ACCOUNTING } from '../common/constants';
2+
3+
export default function isCurrencyStyle(style) {
4+
return style === CURRENCY || style === ACCOUNTING;
5+
}

src/numbers/parse-number.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { localeInfo, localeCurrency, currencyDisplays } from '../cldr';
2+
import { PERCENT, NUMBER_PLACEHOLDER, CURRENCY_PLACEHOLDER, DEFAULT_LOCALE, EMPTY, POINT } from '../common/constants';
3+
import isCurrencyStyle from './is-currency-style';
24

35
const exponentRegExp = /[eE][\-+]?[0-9]+/;
46
const nonBreakingSpaceRegExp = /\u00A0/g;
57

68
function cleanCurrencyNumber(value, info, format) {
7-
let isCurrency = format.style === "currency";
9+
let isCurrency = isCurrencyStyle(format.style);
810
let number = value;
911
let negative;
1012

@@ -16,7 +18,7 @@ function cleanCurrencyNumber(value, info, format) {
1618
for (let idx = 0; idx < displays.length; idx++) {
1719
let display = displays[idx];
1820
if (number.includes(display)) {
19-
number = number.replace(display, "");
21+
number = number.replace(display, EMPTY);
2022
isCurrency = true;
2123
break;
2224
}
@@ -26,9 +28,9 @@ function cleanCurrencyNumber(value, info, format) {
2628
if (isCurrency) {
2729
const patterns = info.numbers.currency.patterns;
2830
if (patterns.length > 1) {
29-
const parts = (patterns[1] || "").replace("$", "").split("n");
31+
const parts = (patterns[1] || EMPTY).replace(CURRENCY_PLACEHOLDER, EMPTY).split(NUMBER_PLACEHOLDER);
3032
if (number.indexOf(parts[0]) > -1 && number.indexOf(parts[1]) > -1) {
31-
number = number.replace(parts[0], "").replace(parts[1], "");
33+
number = number.replace(parts[0], EMPTY).replace(parts[1], EMPTY);
3234
negative = true;
3335
}
3436
}
@@ -41,7 +43,7 @@ function cleanCurrencyNumber(value, info, format) {
4143
};
4244
}
4345

44-
export default function parseNumber(value, locale = "en", format = {}) {
46+
export default function parseNumber(value, locale = DEFAULT_LOCALE, format = {}) {
4547
if (!value && value !== 0) {
4648
return null;
4749
}
@@ -57,7 +59,7 @@ export default function parseNumber(value, locale = "en", format = {}) {
5759
let isPercent;
5860

5961
if (exponentRegExp.test(number)) {
60-
number = parseFloat(number.replace(symbols.decimal, "."));
62+
number = parseFloat(number.replace(symbols.decimal, POINT));
6163
return isNaN(number) ? null : number;
6264
}
6365

@@ -72,15 +74,15 @@ export default function parseNumber(value, locale = "en", format = {}) {
7274
number = newNumber;
7375
isNegative = negativeCurrency !== undefined ? negativeCurrency : isNegative;
7476

75-
if (format.style === "percent" || number.indexOf(symbols.percentSign) > -1) {
76-
number = number.replace(symbols.percentSign, "");
77+
if (format.style === PERCENT || number.indexOf(symbols.percentSign) > -1) {
78+
number = number.replace(symbols.percentSign, EMPTY);
7779
isPercent = true;
7880
}
7981

80-
number = number.replace("-", "")
82+
number = number.replace("-", EMPTY)
8183
.replace(nonBreakingSpaceRegExp, " ")
82-
.split(symbols.group.replace(nonBreakingSpaceRegExp, " ")).join("")
83-
.replace(symbols.decimal, ".");
84+
.split(symbols.group.replace(nonBreakingSpaceRegExp, " ")).join(EMPTY)
85+
.replace(symbols.decimal, POINT);
8486

8587
number = parseFloat(number);
8688

src/numbers/standard-number-format.js

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { PERCENT, SCIENTIFIC, NUMBER_PLACEHOLDER, CURRENCY_PLACEHOLDER, PERCENT_PLACEHOLDER, EMPTY, POINT } from '../common/constants';
12
import formatCurrencySymbol from './format-currency-symbol';
23
import groupInteger from './group-integer';
4+
import isCurrencyStyle from './is-currency-style';
35
import pad from '../common/pad';
46
import round from '../common/round';
57
import { currencyFractionOptions } from '../cldr';
@@ -8,15 +10,10 @@ const DEFAULT_DECIMAL_ROUNDING = 3;
810
const DEFAULT_PERCENT_ROUNDING = 0;
911

1012
const trailingZeroRegex = /0+$/;
11-
const DECIMAL_PLACEHOLDER = "n";
12-
const CURRENCY = "currency";
13-
const PERCENT = "percent";
14-
const EMPTY = "";
15-
const POINT = ".";
1613

1714
function fractionOptions(options) {
1815
let { minimumFractionDigits, maximumFractionDigits, style } = options;
19-
const isCurrency = style === CURRENCY;
16+
const isCurrency = isCurrencyStyle(style);
2017
let currencyFractions;
2118
if (isCurrency) {
2219
currencyFractions = currencyFractionOptions(options.currency);
@@ -47,9 +44,9 @@ function applyPattern(value, pattern, symbol) {
4744
for (let idx = 0, length = pattern.length; idx < length; idx++) {
4845
let ch = pattern.charAt(idx);
4946

50-
if (ch === DECIMAL_PLACEHOLDER) {
47+
if (ch === NUMBER_PLACEHOLDER) {
5148
result += value;
52-
} else if (ch === "$" || ch === "%") {
49+
} else if (ch === CURRENCY_PLACEHOLDER || ch === PERCENT_PLACEHOLDER) {
5350
result += symbol;
5451
} else {
5552
result += ch;
@@ -62,7 +59,7 @@ function currencyUnitPattern(info, value) {
6259
const currencyInfo = info.numbers.currency;
6360
let pattern = value !== 1 ? currencyInfo["unitPattern-count-other"] : currencyInfo["unitPattern-count-one"];
6461
if (value < 0) {
65-
pattern = pattern.replace("n", "-n");
62+
pattern = pattern.replace(NUMBER_PLACEHOLDER, `-${ NUMBER_PLACEHOLDER }`);
6663
}
6764

6865
return pattern;
@@ -72,17 +69,18 @@ function currencyUnitPattern(info, value) {
7269
export default function standardNumberFormat(number, options, info) {
7370
const symbols = info.numbers.symbols;
7471
const { style } = options;
72+
const isCurrency = isCurrencyStyle(style);
7573

7674
//return number in exponential format
77-
if (style === "scientific") {
75+
if (style === SCIENTIFIC) {
7876
let exponential = options.minimumFractionDigits !== undefined ? number.toExponential(options.minimumFractionDigits) : number.toExponential();
7977
return exponential.replace(POINT, symbols.decimal);
8078
}
8179

8280
let value = number;
8381
let symbol;
8482

85-
if (style === CURRENCY) {
83+
if (isCurrency) {
8684
options.value = value;
8785
symbol = formatCurrencySymbol(info, options);
8886
}
@@ -98,7 +96,7 @@ export default function standardNumberFormat(number, options, info) {
9896

9997
const negative = value < 0;
10098

101-
const parts = value.split(".");
99+
const parts = value.split(POINT);
102100

103101
let integer = parts[0];
104102
let fraction = pad(parts[1] ? parts[1].replace(trailingZeroRegex, EMPTY) : EMPTY, minimumFractionDigits, true);
@@ -120,14 +118,14 @@ export default function standardNumberFormat(number, options, info) {
120118

121119
let pattern;
122120

123-
if (style === CURRENCY && options.currencyDisplay === "name") {
121+
if (isCurrency && options.currencyDisplay === "name") {
124122
pattern = currencyUnitPattern(info, number);
125123
} else {
126124
const patterns = options.patterns;
127125
pattern = negative ? patterns[1] || ("-" + patterns[0]) : patterns[0];
128126
}
129127

130-
if (pattern === DECIMAL_PLACEHOLDER && !negative) {
128+
if (pattern === NUMBER_PLACEHOLDER && !negative) {
131129
return formattedValue;
132130
}
133131

test/numbers.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ function loadCustom(options) {
2525
"standard": options.pattern || "#,##0.###"
2626
},
2727
"currencyFormats-numberSystem-latn": {
28-
"standard": options.currencyPattern || "¤#,##0.00"
28+
"standard": options.currencyPattern || "¤#,##0.00",
29+
"accounting": options.accountingPattern || "#,##0.00¤"
2930
},
3031
currencies: options.currencies
3132
}
@@ -324,6 +325,75 @@ describe('standard currency formatting', () => {
324325

325326
});
326327

328+
describe('standard accounting formatting', () => {
329+
330+
beforeAll(() => {
331+
loadCustom({ currencies: { USD: { symbol: "$" }} });
332+
cldr.custom.numbers.localeCurrency = "USD";
333+
});
334+
335+
afterAll(() => {
336+
delete cldr.custom;
337+
});
338+
339+
it('should apply format', () => {
340+
expect(formatNumber(10, 'a', 'custom')).toEqual("10.00$");
341+
});
342+
343+
it('should apply format with precision', () => {
344+
expect(formatNumber(10, 'a0')).toEqual("$10");
345+
});
346+
347+
it('should apply format for negative numbers', () => {
348+
expect(formatNumber(-10.3337, 'a3')).toEqual("($10.334)");
349+
});
350+
351+
it("should apply group separators", () => {
352+
expect(formatNumber(123456789, 'a')).toEqual("$123,456,789.00");
353+
});
354+
355+
it("should not apply group separators to numbers with less digits", () => {
356+
expect(formatNumber(123, "a")).toEqual("$123.00");
357+
});
358+
359+
it("should apply format when passing language", () => {
360+
expect(formatNumber(10, "a", "bg")).toEqual("10,00 лв.");
361+
});
362+
363+
it("should apply format when passing language and territory", () => {
364+
expect(formatNumber(10, "a", "bg-BG")).toEqual("10,00 лв.");
365+
});
366+
367+
it("should apply format when passing object", () => {
368+
expect(formatNumber(10, { style: "accounting" })).toEqual("$10.00");
369+
});
370+
371+
it("should apply format for specific currency", () => {
372+
expect(formatNumber(10, { style: "accounting", currency: "BGN" })).toEqual("BGN10.00");
373+
});
374+
375+
it("should apply specific currency display", () => {
376+
expect(formatNumber(10, { style: "accounting", currency: "BGN", currencyDisplay: "name" })).toEqual("10.00 Bulgarian leva");
377+
});
378+
379+
it("should format negative currency with name", () => {
380+
expect(formatNumber(-10, { style: "accounting", currency: "BGN", currencyDisplay: "name" })).toEqual("-10.00 Bulgarian leva");
381+
});
382+
383+
it("should format currency equal to one with name", () => {
384+
expect(formatNumber(1, { style: "accounting", currency: "BGN", currencyDisplay: "name" })).toEqual("1.00 Bulgarian lev");
385+
});
386+
387+
it("should apply minimumFractionDigits", () => {
388+
expect(formatNumber(10, { style: "accounting", minimumFractionDigits: 5 })).toEqual("$10.00000");
389+
});
390+
391+
it("should apply maximumFractionDigits", () => {
392+
expect(formatNumber(10.1235, { style: "accounting", maximumFractionDigits: 3 })).toEqual("$10.124");
393+
});
394+
395+
});
396+
327397
describe('custom formatting', () => {
328398

329399
it('replaces whole part of the number', () => {

0 commit comments

Comments
 (0)