Skip to content

Commit

Permalink
Merge pull request #4 from coingecko/crypto-non-btc-eth-formatting
Browse files Browse the repository at this point in the history
Support crypto currencies not supported by Intl.NumberFormat
  • Loading branch information
ernsheong authored Jul 4, 2018
2 parents 300584d + 67a9616 commit 474ff03
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 59 deletions.
132 changes: 82 additions & 50 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// A map of supported currency codes to currency symbols.
const currencySymbols = {
const supportedCurrencySymbols = {
BTC: "Ƀ",
ETH: "Ξ",
USD: "$",
Expand Down Expand Up @@ -47,6 +47,14 @@ function IntlNumberFormatSupported() {
return !!(typeof Intl == "object" && Intl && typeof Intl.NumberFormat == "function");
}

function isBTCETH(isoCode) {
return isoCode === "BTC" || isoCode === "ETH";
}

export function isCrypto(isoCode) {
return isBTCETH(isoCode) || supportedCurrencySymbols[isoCode] == null;
}

// Function to transform the output from Intl.NumberFormat#format
function formatCurrencyOverride(formattedCurrency, locale = "en") {
// If currency code remains in front
Expand All @@ -57,7 +65,7 @@ function formatCurrencyOverride(formattedCurrency, locale = "en") {
// Replace currency code with symbol if whitelisted.
const overrideObj = symbolOverrides[code];
if (overrideObj && overrideObj.location.start && overrideObj.forLocales[locale]) {
return formattedCurrency.replace(currencyCodeFrontMatch[0], currencySymbols[code]);
return formattedCurrency.replace(currencyCodeFrontMatch[0], supportedCurrencySymbols[code]);
} else {
return formattedCurrency;
}
Expand All @@ -71,7 +79,7 @@ function formatCurrencyOverride(formattedCurrency, locale = "en") {
// Replace currency code with symbol if whitelisted.
const overrideObj = symbolOverrides[code];
if (overrideObj && overrideObj.location.end && overrideObj.forLocales[locale]) {
return formattedCurrency.replace(code, currencySymbols[code]);
return formattedCurrency.replace(code, supportedCurrencySymbols[code]);
} else {
return formattedCurrency;
}
Expand All @@ -80,25 +88,58 @@ function formatCurrencyOverride(formattedCurrency, locale = "en") {
return formattedCurrency;
}

// Generates a formatter from Intl.NumberFormat
function generateIntlNumberFormatter(isoCode, locale, numDecimals) {
let formatter;
try {
formatter = new Intl.NumberFormat(locale, {
style: "currency",
currency: isoCode,
currencyDisplay: "symbol",
minimumFractionDigits: numDecimals,
maximumFractionDigits: numDecimals
});
} catch (e) {
// Unsupported currency, etc.
// Use primitive fallback
return generateFallbackFormatter(isoCode, locale, numDecimals);
}
return formatter;
}

// 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)}`;
return isCrypto(isoCode)
? `${value.toFixed(numDecimals)} ${isoCode}`
: `${isoCode} ${value.toFixed(numDecimals)}`;
}
};
} else {
return {
format: value => {
return `${isoCode} ${value.toLocaleString(locale)}`;
return isCrypto(isoCode)
? `${value.toLocaleString(locale)} ${isoCode}`
: `${isoCode} ${value.toLocaleString(locale)}`;
}
};
}
}

function generateFormatter(isoCode, locale, numDecimals) {
const isNumberFormatSupported = IntlNumberFormatSupported();

const useIntlNumberFormatter =
isNumberFormatSupported && (!isCrypto(isoCode) || isBTCETH(isoCode));
return useIntlNumberFormatter
? generateIntlNumberFormatter(isoCode, locale, numDecimals)
: generateFallbackFormatter(isoCode, locale, numDecimals);
}

// State variables
let currentISOCode;
let currencyFormatterNormal;
Expand All @@ -107,51 +148,42 @@ let currencyFormatterMedium;
let currencyFormatterSmall;
let currencyFormatterVerySmall;

// If a page has to display multiple currencies, formatters would have to be created for each of them
// To save some effort, we save formatters for reuse
let formattersCache = {};

export function clearCache() {
formattersCache = {};
}

function initializeFormatters(isoCode, locale) {
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);
const cachedFormatter = formattersCache[isoCode];

currencyFormatterNormal = cachedFormatter
? cachedFormatter.currencyFormatterNormal
: generateFormatter(isoCode, locale);
currencyFormatterNoDecimal = cachedFormatter
? cachedFormatter.currencyFormatterNoDecimal
: generateFormatter(isoCode, locale, 0);
currencyFormatterMedium = cachedFormatter
? cachedFormatter.currencyFormatterMedium
: generateFormatter(isoCode, locale, 3);
currencyFormatterSmall = cachedFormatter
? cachedFormatter.currencyFormatterSmall
: generateFormatter(isoCode, locale, 6);
currencyFormatterVerySmall = cachedFormatter
? cachedFormatter.currencyFormatterVerySmall
: generateFormatter(isoCode, locale, 8);

// Save in cache
if (cachedFormatter == null) {
formattersCache[isoCode] = {};
formattersCache[isoCode].currencyFormatterNormal = currencyFormatterNormal;
formattersCache[isoCode].currencyFormatterNoDecimal = currencyFormatterNoDecimal;
formattersCache[isoCode].currencyFormatterMedium = currencyFormatterMedium;
formattersCache[isoCode].currencyFormatterSmall = currencyFormatterSmall;
formattersCache[isoCode].currencyFormatterVerySmall = currencyFormatterVerySmall;
}
}

// Moderate crypto amount threshold
Expand All @@ -169,7 +201,7 @@ export function formatCurrency(amount, isoCode, locale = "en", raw = false) {
initializeFormatters(isoCode, locale);
}

if (isoCode === "BTC" || isoCode === "ETH") {
if (isCrypto(isoCode)) {
let price = parseFloat(amount);

if (raw) {
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.2.0",
"version": "0.2.1",
"description": "Javascript library to format and display cryptocurrencies and fiat",
"main": "index.js",
"scripts": {
Expand Down
33 changes: 25 additions & 8 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { formatCurrency } from "./index";
import { formatCurrency, isCrypto, clearCache } from "./index";

describe("is BTC or ETH", () => {
test("isCrypto", () => {
expect(isCrypto("BTC")).toBe(true);
expect(isCrypto("DOGE")).toBe(true);
expect(isCrypto("USD")).toBe(false);
expect(isCrypto("IDR")).toBe(false);
});

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

Expand All @@ -23,6 +31,10 @@ describe("is BTC or ETH", () => {

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

// Non-BTC or ETH
expect(formatCurrency(1.1, "DOGE", "en")).toBe("1.100000 DOGE");
expect(formatCurrency(1.1, "LTC", "en")).toBe("1.100000 LTC");
});
});
});
Expand Down Expand Up @@ -64,6 +76,7 @@ describe("is fiat", () => {
describe("Intl.NumberFormat not supported", () => {
beforeAll(() => {
Intl.NumberFormat = null;
clearCache();
});

describe("is BTC or ETH", () => {
Expand All @@ -75,20 +88,24 @@ describe("Intl.NumberFormat not supported", () => {

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

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

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

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

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

// Non-BTC or ETH
expect(formatCurrency(1.1, "DOGE", "en")).toBe("1.100000 DOGE");
expect(formatCurrency(1.1, "LTC", "en")).toBe("1.100000 LTC");
});
});
});
Expand Down

0 comments on commit 474ff03

Please sign in to comment.