Skip to content

Commit 3c5e269

Browse files
Merge pull request #16 from telerik/default-currency
Determine default currency based on chronological order
2 parents d1a09fc + 679ab90 commit 3c5e269

File tree

7 files changed

+225
-28
lines changed

7 files changed

+225
-28
lines changed

src/cldr/currency.js

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { cldr, getLocaleInfo } from './info';
2-
import localeTerritory from './territory';
32
import { errors } from '../errors';
3+
import localeTerritory from './territory';
4+
import parseRangeDate from './parse-range-date';
45

56
const {
67
NoCurrency,
78
NoCurrencyDisplay,
89
NoSupplementalCurrency,
9-
NoCurrencyRegion
10+
NoCurrencyRegion,
11+
NoValidCurrency
1012
} = errors;
1113

1214
const DEFAULT_CURRENCY_FRACTIONS = 2;
1315
const SYMBOL = "symbol";
16+
const INVALID_CURRENCY_CODE = 'XXX';
1417

1518
function getCurrencyInfo(locale, currency) {
1619
const info = getLocaleInfo(locale);
@@ -32,6 +35,38 @@ function lengthComparer(a, b) {
3235
return b.length - a.length;
3336
}
3437

38+
function regionCurrency(regionCurrencies) {
39+
let latestValidUntil, latestValidUntilRange;
40+
let latestStillValid, latestStillValidDate;
41+
42+
for (let idx = 0; idx < regionCurrencies.length; idx++) {
43+
const currency = regionCurrencies[idx];
44+
const code = Object.keys(currency)[0];
45+
const info = currency[code];
46+
if (code !== INVALID_CURRENCY_CODE && info._tender !== 'false' && info._from) {
47+
if (!info._to) {
48+
const stillValidDate = parseRangeDate(info._from);
49+
if (!latestStillValidDate || latestStillValidDate < stillValidDate) {
50+
latestStillValid = code;
51+
latestStillValidDate = stillValidDate;
52+
}
53+
} else if (!latestStillValid) {
54+
const validFrom = parseRangeDate(info._from);
55+
const validTo = parseRangeDate(info._to);
56+
if (!latestValidUntilRange || latestValidUntilRange.to < validTo || latestValidUntilRange.from < validFrom) {
57+
latestValidUntil = code;
58+
latestValidUntilRange = {
59+
from: validFrom,
60+
to: validTo
61+
};
62+
}
63+
}
64+
}
65+
}
66+
67+
return latestStillValid || latestValidUntil;
68+
}
69+
3570
export function currencyDisplays(locale, currency) {
3671
const currencyInfo = getCurrencyInfo(locale, currency);
3772
if (!currencyInfo.displays) {
@@ -92,19 +127,29 @@ export function territoryCurrencyCode(territory) {
92127
}
93128

94129
const regionCurrencies = currencyData.region[territory];
130+
95131
if (!regionCurrencies) {
96-
throw NoCurrencyRegion.error();
132+
throw NoCurrencyRegion.error(territory);
97133
}
98-
const currencyCode = Object.keys(regionCurrencies[regionCurrencies.length - 1])[0];
134+
135+
const currencyCode = regionCurrency(regionCurrencies);
99136

100137
return currencyCode;
101138
}
102139

103-
export function localeCurrency(locale) {
140+
export function localeCurrency(locale, throwIfNoValid) {
104141
const info = getLocaleInfo(locale);
105142
const numbers = info.numbers;
143+
106144
if (!numbers.localeCurrency) {
107-
numbers.localeCurrency = territoryCurrencyCode(localeTerritory(info));
145+
const currency = territoryCurrencyCode(localeTerritory(info));
146+
147+
if (!currency && throwIfNoValid) {
148+
throw NoValidCurrency.error(info.name);
149+
}
150+
151+
numbers.localeCurrency = currency;
108152
}
153+
109154
return numbers.localeCurrency;
110155
}

src/cldr/parse-range-date.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function(value) {
2+
const parts = value.split('-');
3+
const year = parseInt(parts[0], 10);
4+
const month = parseInt(parts[1], 10) - 1;
5+
const day = parseInt(parts[2], 10);
6+
7+
return new Date(year, month, day);
8+
}

src/error-details.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export default {
99
"NoCurrencyDisplay": "Cannot determine currency display information. Please load the locale currencies data. The default culture does not include the all currencies data.",
1010
"NoGMTInfo": "Cannot determine locale GMT format. Please load the locale timeZoneNames data.",
1111
"NoWeekData": "Cannot determine locale first day of week. Please load the supplemental weekData.",
12-
"NoFirstDay": "Cannot determine locale first day of week. Please load the supplemental weekData. The default culture includes only the 'en-US' first day info."
12+
"NoFirstDay": "Cannot determine locale first day of week. Please load the supplemental weekData. The default culture includes only the 'en-US' first day info.",
13+
"NoValidCurrency": "Cannot determine a default currency for the {0} locale. Please specify explicitly the currency with the format options."
1314
};

src/numbers/format-currency-symbol.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { currencyDisplay, localeCurrency } from '../cldr';
22

33
export default function formatCurrencySymbol(info, options = {}) {
44
if (!options.currency) {
5-
options.currency = localeCurrency(info);
5+
options.currency = localeCurrency(info, true);
66
}
77

88
const display = currencyDisplay(info, options);

src/numbers/parse-number.js

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,31 @@ const exponentRegExp = /[eE][\-+]?[0-9]+/;
44
const nonBreakingSpaceRegExp = /\u00A0/g;
55

66
function cleanCurrencyNumber(value, info, format) {
7-
const currency = format.currency || localeCurrency(info);
8-
const displays = currencyDisplays(info, currency);
97
let isCurrency = format.style === "currency";
108
let number = value;
119
let negative;
1210

11+
const currency = format.currency || localeCurrency(info, isCurrency);
1312

14-
for (let idx = 0; idx < displays.length; idx++) {
15-
let display = displays[idx];
16-
if (number.includes(display)) {
17-
number = number.replace(display, "");
18-
isCurrency = true;
19-
break;
13+
if (currency) {
14+
const displays = currencyDisplays(info, currency);
15+
for (let idx = 0; idx < displays.length; idx++) {
16+
let display = displays[idx];
17+
if (number.includes(display)) {
18+
number = number.replace(display, "");
19+
isCurrency = true;
20+
break;
21+
}
2022
}
21-
}
2223

23-
if (isCurrency) {
24-
const patterns = info.numbers.currency.patterns;
25-
if (patterns.length > 1) {
26-
const parts = (patterns[1] || "").replace("$", "").split("n");
27-
if (number.indexOf(parts[0]) > -1 && number.indexOf(parts[1]) > -1) {
28-
number = number.replace(parts[0], "").replace(parts[1], "");
29-
negative = true;
24+
if (isCurrency) {
25+
const patterns = info.numbers.currency.patterns;
26+
if (patterns.length > 1) {
27+
const parts = (patterns[1] || "").replace("$", "").split("n");
28+
if (number.indexOf(parts[0]) > -1 && number.indexOf(parts[1]) > -1) {
29+
number = number.replace(parts[0], "").replace(parts[1], "");
30+
negative = true;
31+
}
3032
}
3133
}
3234
}

test/cldr.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,104 @@ describe('firstDay', () => {
277277
});
278278

279279
describe('localeCurrency', () => {
280+
281+
cldr.currencyTest = {
282+
identity: {
283+
territory: "currencyTest"
284+
},
285+
numbers: {}
286+
};
287+
288+
afterEach(() => {
289+
delete cldr.currencyTest.numbers.localeCurrency;
290+
});
291+
280292
it('should return default currency code for locale', () => {
281293
expect(localeCurrency('en')).toEqual('USD');
282294
expect(localeCurrency('bg')).toEqual('BGN');
283295
});
296+
297+
it('throws error if second parameter is true and there is no valid currency for the locale', () => {
298+
cldr.supplemental.currencyData.region.currencyTest = [{ XXX: {} }];
299+
300+
expect(() => {
301+
localeCurrency('currencyTest', true);
302+
}).toThrowError(/NoValidCurrency/);
303+
});
304+
305+
it('returns still valid currency', () => {
306+
cldr.supplemental.currencyData.region.currencyTest = [
307+
{
308+
"Foo": {
309+
"_from": "1915-01-01"
310+
}
311+
}, {
312+
"Bar": {
313+
"_from": "1872-08-26",
314+
"_to": "2002-05-15"
315+
}
316+
}
317+
];
318+
319+
expect(localeCurrency('currencyTest')).toEqual('Foo');
320+
});
321+
322+
it('returns latest still valid currency', () => {
323+
cldr.supplemental.currencyData.region.currencyTest = [
324+
{
325+
"Foo": {
326+
"_from": "1915-01-01"
327+
}
328+
}, {
329+
"Bar": {
330+
"_from": "1872-08-26"
331+
}
332+
}
333+
];
334+
335+
expect(localeCurrency('currencyTest')).toEqual('Foo');
336+
});
337+
338+
it('returns latest valid until currency', () => {
339+
cldr.supplemental.currencyData.region.currencyTest = [
340+
{
341+
"Foo": {
342+
"_from": "1994-01-24",
343+
"_to": "2002-05-15"
344+
}
345+
}, {
346+
"Bar": {
347+
"_from": "2003-02-04",
348+
"_to": "2006-06-03"
349+
}
350+
}, {
351+
"Baz": {
352+
"_from": "2002-05-15",
353+
"_to": "2006-06-03"
354+
}
355+
}
356+
];
357+
358+
expect(localeCurrency('currencyTest')).toEqual('Bar');
359+
});
360+
361+
it('ignores currencies with _tender equal to false', () => {
362+
cldr.supplemental.currencyData.region.currencyTest = [
363+
{
364+
"Foo": {
365+
"_from": "1915-01-01",
366+
"_tender": "false"
367+
}
368+
}, {
369+
"Bar": {
370+
"_from": "1872-08-26"
371+
}
372+
}
373+
];
374+
375+
expect(localeCurrency('currencyTest')).toEqual('Bar');
376+
});
377+
284378
});
285379

286380
describe('currencyDisplay', () => {

test/numbers.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { load } from '../src/cldr';
1+
import { load, cldr } from '../src/cldr';
22
import { formatNumber, parseNumber } from '../src/numbers';
33

44
const likelySubtags = require("cldr-data/supplemental/likelySubtags.json");
@@ -7,7 +7,7 @@ const currencies = require("cldr-data/main/bg/currencies.json");
77
const currencyData = require("cldr-data/supplemental/currencyData.json");
88

99
// CUSTOM region is used in tests below
10-
currencyData.supplemental.currencyData.region.CUSTOM = [{}];
10+
currencyData.supplemental.currencyData.region.CUSTOM = [{ XXX: {} }];
1111

1212
load(likelySubtags, currencyData, numbers, currencies);
1313

@@ -16,7 +16,8 @@ function loadCustom(options) {
1616
"main": {
1717
"custom": {
1818
"identity": {
19-
"language": "custom"
19+
"language": "custom",
20+
"territory": "CUSTOM"
2021
},
2122
"numbers": {
2223
"symbols-numberSystem-latn": {
@@ -50,6 +51,30 @@ describe('formatNumber', () => {
5051
expect(formatNumber(Infinity)).toBe(Infinity);
5152
expect(formatNumber("foo")).toEqual("foo");
5253
});
54+
55+
56+
describe('errors', () => {
57+
currencyData.supplemental.currencyData.region.CUSTOM = [{ XXX: {} }];
58+
loadCustom({ currencies: { USD: { symbol: "$" } } });
59+
60+
it('throws error if the default locale currency cannot be determined', () => {
61+
expect(() => {
62+
formatNumber(10, 'c', 'custom');
63+
}).toThrowError(/NoValidCurrency/);
64+
});
65+
66+
it('does not throw error if default locale currency cannot be determined but the currency is specified', () => {
67+
expect(() => {
68+
formatNumber(10, { style: 'currency', currency: 'USD' }, 'custom');
69+
}).not.toThrow();
70+
});
71+
72+
it('does not throw error if default locale currency cannot be determined but the format does not require it', () => {
73+
expect(() => {
74+
formatNumber(10, 'n', 'custom');
75+
}).not.toThrow();
76+
});
77+
});
5378
});
5479

5580
describe('standard scientific formatting', () => {
@@ -570,4 +595,26 @@ describe('parseNumber', () => {
570595
expect(parseNumber("1,23432e+5", "bg")).toEqual(123432);
571596
});
572597

573-
});
598+
describe('errors', () => {
599+
currencyData.supplemental.currencyData.region.CUSTOM = [{ XXX: {} }];
600+
loadCustom({ currencies: { USD: { symbol: "$" } } });
601+
602+
it('throws error if the default locale currency cannot be determined', () => {
603+
expect(() => {
604+
parseNumber("10", 'custom', { style: "currency" });
605+
}).toThrowError(/NoValidCurrency/);
606+
});
607+
608+
it('does not throw error if default locale currency cannot be determined but the currency is specified', () => {
609+
expect(() => {
610+
parseNumber("10", 'custom', { style: "currency", currency: 'USD' });
611+
}).not.toThrow();
612+
});
613+
614+
it('does not throw error if default locale currency cannot be determined but the format does not require it', () => {
615+
expect(() => {
616+
parseNumber("10", 'custom', "n");
617+
}).not.toThrow();
618+
});
619+
});
620+
});

0 commit comments

Comments
 (0)