Skip to content

Commit

Permalink
Merge pull request #2 from coingecko/jl-intl-numberformat-fallback
Browse files Browse the repository at this point in the history
Add fallback for lack of Intl.NumberFormat support
  • Loading branch information
ernsheong authored Jul 3, 2018
2 parents ff4228c + 2d049f6 commit b10426c
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 40 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Often an altcoin can be worth much less than $0.01 USD, and thus we need to format this value by providing more decimal places in the formatting to prevent losing precious information.

`cryptoformat` also tries to handle different locales and currency formatting by deferring the work to the browser's [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat)
`cryptoformat` also tries to handle different locales and currency formatting by deferring the work to the browser's [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat). If `Intl.NumberFormat` is not supported by the browser, `cryptoformat` provides a primitive fallback for currency display.

## Install

Expand Down Expand Up @@ -38,6 +38,7 @@ formatCurrency(123400, "EUR", "de");
// "123.400 €"
```

## Issues
## Known Issues

`Intl.NumberFormat` does not always behave consistently across browsers. `cryptoformat` does some manual overrides in order to ensure that "MYR123.00" is displayed as "RM123.00", for example. Unfortunately given that country detection for locale is quite hard to do, e.g. "en-MY", `cryptoformat` does not try to do country sniffing. It is the responsibility of the caller to provide that if possible, but providing only "en" should also work for the most part, but not perfectly: users in different regions may expect a different formatting for the same language.
1. `Intl.NumberFormat` does not always behave consistently across browsers. `cryptoformat` does some manual overrides in order to ensure that "MYR123.00" is displayed as "RM123.00", for example.
2. Given that country detection for locale is quite hard to do, e.g. "en-MY", `cryptoformat` does not try to do country sniffing. It is the responsibility of the caller to provide that if possible, but providing only "en" should also work for the most part, but not perfectly: users in different regions may expect a different formatting for the same language.
107 changes: 71 additions & 36 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,22 @@ const symbolOverrides = {
ETH: { location: { start: true }, forLocales: { en: true } }
};

// Feature detection for Intl.NumberFormat
function IntlNumberFormatSupported() {
return !!(typeof Intl == "object" && Intl && typeof Intl.NumberFormat == "function");
}

// Function to transform the output from Intl.NumberFormat#format
const formatCurrencyOverride = function(formattedCurrency, locale = "en") {
// If currency code remains in front
const currencyCodeFrontMatch = formattedCurrency.match(/^[A-Z]{3}/);
const currencyCodeFrontMatch = formattedCurrency.match(/^[A-Z]{3}\s?/);
if (currencyCodeFrontMatch != null) {
const code = currencyCodeFrontMatch[0];
const code = currencyCodeFrontMatch[0].trim(); // trim possible trailing space

// Replace currency code with symbol if whitelisted.
const overrideObj = symbolOverrides[code];
if (overrideObj && overrideObj.location.start && overrideObj.forLocales[locale]) {
return formattedCurrency.replace(code, currencySymbols[code]);
return formattedCurrency.replace(currencyCodeFrontMatch[0], currencySymbols[code]);
} else {
return formattedCurrency;
}
Expand All @@ -75,6 +80,25 @@ const formatCurrencyOverride = function(formattedCurrency, locale = "en") {
return formattedCurrency;
};

// Generates a primitive fallback formatter with no symbol support.
function generateFallbackFormatter(isoCode, locale, numDecimals = 2) {
isoCode = isoCode.toUpperCase();

if (numDecimals > 2) {
return {
format: value => {
return `${isoCode} ${value.toFixed(numDecimals)}`;
}
};
} else {
return {
format: value => {
return `${isoCode} ${value.toLocaleString(locale)}`;
}
};
}
}

// State variables
let currentISOCode;
let currencyFormatterNormal;
Expand All @@ -84,39 +108,50 @@ let currencyFormatterSmall;
let currencyFormatterVerySmall;

function initializeFormatters(isoCode, locale) {
currencyFormatterNormal = new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol"
});
currencyFormatterNoDecimal = new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 0,
maximumFractionDigits: 0
});
currencyFormatterMedium = new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 3,
maximumFractionDigits: 3
});
currencyFormatterSmall = new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 6,
maximumFractionDigits: 6
});
currencyFormatterVerySmall = new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 8,
maximumFractionDigits: 8
});
const isNumberFormatSupported = IntlNumberFormatSupported();
currencyFormatterNormal = isNumberFormatSupported
? new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol"
})
: generateFallbackFormatter(isoCode, locale);
currencyFormatterNoDecimal = isNumberFormatSupported
? new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 0,
maximumFractionDigits: 0
})
: generateFallbackFormatter(isoCode, locale);
currencyFormatterMedium = isNumberFormatSupported
? new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 3,
maximumFractionDigits: 3
})
: generateFallbackFormatter(isoCode, locale, 3);
currencyFormatterSmall = isNumberFormatSupported
? new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 6,
maximumFractionDigits: 6
})
: generateFallbackFormatter(isoCode, locale, 6);
currencyFormatterVerySmall = isNumberFormatSupported
? new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: 8,
maximumFractionDigits: 8
})
: generateFallbackFormatter(isoCode, locale, 8);
}

// Moderate crypto amount threshold
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coingecko/cryptoformat",
"version": "0.1.4",
"version": "0.2.0",
"description": "Javascript library to format and display cryptocurrencies and fiat",
"main": "index.js",
"scripts": {
Expand Down
67 changes: 67 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,70 @@ describe("is fiat", () => {
});
});
});

describe("Intl.NumberFormat not supported", () => {
beforeAll(() => {
Intl.NumberFormat = null;
});

describe("is BTC or ETH", () => {
describe("raw = true", () => {
test("returns precision of 8", () => {
expect(formatCurrency(0.00001, "BTC", "en", true)).toBe("0.000010000000");
});
});

describe("raw = false", () => {
test("returns currency with ISO Code", () => {
expect(formatCurrency(0.0, "BTC", "en")).toBe("Ƀ0");

// Large cyrpto, no decimals
expect(formatCurrency(1001, "BTC", "en")).toBe("Ƀ1,001");

// Medium cyrpto, 3 decimals
expect(formatCurrency(51.1, "BTC", "en")).toBe("Ƀ51.100");

// Small cyrpto, 6 decimals
expect(formatCurrency(11.1, "BTC", "en")).toBe("Ƀ11.100000");
expect(formatCurrency(9.234, "ETH", "en")).toBe("Ξ9.234000");

// Very small cyrpto, 8 decimals
expect(formatCurrency(0.5, "BTC", "en")).toBe("Ƀ0.50000000");
});
});
});

describe("is fiat", () => {
describe("raw = true", () => {
test("returns formatted raw", () => {
// Very small fiat, 8 decimals
expect(formatCurrency(0.00001, "USD", "en", true)).toBe("0.00001000");

// Small fiat, 6 decimals
expect(formatCurrency(0.5, "USD", "en", true)).toBe("0.500000");

// Normal fiat, 2 decimals
expect(formatCurrency(10, "USD", "en", true)).toBe("10.00");
});
});

describe("raw = false", () => {
test("returns formatted with symbol", () => {
// 0 fiat, no decimals
expect(formatCurrency(0.0, "USD", "en")).toBe("USD 0");

// Very small fiat, 8 decimals
expect(formatCurrency(0.00002, "USD", "en")).toBe("USD 0.00002000");

// Small fiat, 6 decimals
expect(formatCurrency(0.5, "USD", "en")).toBe("USD 0.500000");

// Medium fiat, normal decimals
expect(formatCurrency(1001, "USD", "en")).toBe("USD 1,001");

// Large fiat, no decimals
expect(formatCurrency(51100, "USD", "en")).toBe("USD 51,100");
});
});
});
});

0 comments on commit b10426c

Please sign in to comment.