diff --git a/README.md b/README.md index 287da82..1be646a 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/index.js b/index.js index ff91d09..0068ed8 100644 --- a/index.js +++ b/index.js @@ -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; } @@ -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; @@ -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 diff --git a/package.json b/package.json index 1f0926e..3fad186 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/test.js b/test.js index 2b67a00..8770632 100644 --- a/test.js +++ b/test.js @@ -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"); + }); + }); + }); +});