5

Other than just guessing (like I've done below), is there a more direct and efficient way of reflectively retrieving a list of all currencies supported by your JavaScript environment?

function getSupportedCurrencies() {
  function $(amount, currency) {
    let locale = 'en-US';
    let options = {
      style: 'currency',
      currency: currency,
      currencyDisplay: "name"
    };
    return Intl.NumberFormat(locale, options).format(amount);
  }
  const getAllPossibleThreeLetterWords = () => {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    const arr = [];
    let text = '';
    for (let i = 0; i < chars.length; i++) {
      for (let x = 0; x < chars.length; x++) {
        for (let j = 0; j < chars.length; j++) {
          text += chars[i];
          text += chars[x];
          text += chars[j];
          arr.push(text);
          text = '';
        }
      }
    }
    return arr;
  };
  let ary = getAllPossibleThreeLetterWords();
  let currencies = [];
  const rx = /(?<= ).+/; // This line doesn't work in Firefox versions older than version 78 due to bug 1225665: https://bugzilla.mozilla.org/show_bug.cgi?id=1225665
  ary.forEach((cur) => {
    let output = $(0, cur).trim();
    if (output.replace(/^[^ ]+ /, '') !== cur) {
      let obj = {};
      obj.code = cur;
      obj.name = output.match(rx)[0];
      currencies.push(obj);
    }
  });
  return currencies;
}
console.log(getSupportedCurrencies());
Lonnie Best
  • 9,936
  • 10
  • 57
  • 97
  • I didn't submit this code for review. I simply provided it as an inefficient example (for what would have otherwise been a once sentence question). – Lonnie Best Sep 20 '19 at 13:38
  • 1
    Interesting question. But does it matter? `Intl.NumberFormat()`, without the locale specified, will use the default of the users computer, which is almost always what I need, so I'm curious why you need the full list of locales the user' computer has installed. – Shilly Sep 20 '19 at 13:38
  • 1
    @Shilly it's not all *locales*, it's all *currencies*. And not from the *computer* but from *the current environment*. – VLAZ Sep 20 '19 at 13:40
  • 1
    Perhaps a version of this https://stackoverflow.com/questions/35126247/how-to-see-what-locales-a-given-node-js-version-supports-and-how-to-enable-missi/40272584#40272584 – mplungjan Sep 20 '19 at 13:41
  • 1
    Could we like, have a powershell or bash script inject all available locales as an environment variable into node? Since at least powershell can loop over all installed locales. Or would that be worse than testing all letter codes? – Shilly Sep 20 '19 at 13:58
  • Ideally, the spec should evolve to standardize this type of environmental reflection. The specification should prescribe a `getSupportedCurrencies` function of its own, so that programmers don't have to resort to inefficient Cartesian Product Based Solutions. – Lonnie Best Sep 20 '19 at 23:16

2 Answers2

3

Right now, exhaustive testing, as the accepted answer offers, is probably the most reasonable practical tack here. Moreover, new currencies do not arise, and old currencies do not die, with particular frequency. The currencies supported by any implementation being kept up to date, are going to reflect reality almost always. So the try-and-see approach really isn't going to fail much.

But to elaborate further on this from the spec side, the spec really only cares about currencies being "well-formed": three ASCII letters. If the resulting code is a known currency, you will get congenial behavior. Otherwise, you get roughly graceful fallback to the code itself. So there's debatably not a need to expose the supported list: currency codes are at least a relatively understandable thing to many users, probably, as in most UI seeing something like "3 USD" or "5 CAD" where prices or costs are implicated is going to connote the user's currency generally.

In the future, however, a spec proposal that exposes the set of recognized currencies is well on the way to standardization. Initial implementations will probably start showing up in web browsers' JS implementations before the end of 2021, letting you do this:

// This will return an array of currency codes supported
// by Intl.NumberFormat and Intl.DisplayNames, e.g.:
//   ["ADP", "AED", ..., "JPY", ..., "USD", ...]
var currencies = Intl.supportedValuesOf("currency");
Jeff Walden
  • 7,008
  • 2
  • 38
  • 55
2

You could load a known list via this XML:

https://www.currency-iso.org/dam/downloads/lists/list_one.xml

The list was found here: https://www.currency-iso.org/en/home/tables/table-a1.html

<ISO_4217 Pblshd="2018-08-29">
  <CcyTbl>
    <CcyNtry>
      <CtryNm>
        UNITED KINGDOM OF GREAT BRITAIN AND NORTHERN IRELAND (THE)
      </CtryNm>
      <CcyNm>Pound Sterling</CcyNm>
      <Ccy>GBP</Ccy>
      <CcyNbr>826</CcyNbr>
      <CcyMnrUnts>2</CcyMnrUnts>
    </CcyNtry>
    <CcyNtry>
      <CtryNm>UNITED STATES OF AMERICA (THE)</CtryNm>
      <CcyNm>US Dollar</CcyNm>
      <Ccy>USD</Ccy>
      <CcyNbr>840</CcyNbr>
      <CcyMnrUnts>2</CcyMnrUnts>
    </CcyNtry>
  </CcyTbl>
</ISO_4217>

var xmlString = getSampleCurrencyXml();
var xmlData = (new window.DOMParser()).parseFromString(xmlString, "text/xml");
var knownCodes = [].slice.call(xmlData.querySelectorAll('Ccy')).map(n => n.textContent)

// Fetch the XML instead?
fetch('https://www.currency-iso.org/dam/downloads/lists/list_one.xml', { cache: 'default' })
  .then(response => response.text())
  .then(xmlStr => (new window.DOMParser()).parseFromString(xmlStr, "text/xml"))
  .then(data => knownCodes = data); // This may not work in the Stack Snippet

console.log(getSupportedCurrencies().map(c => c.code + '\t' + c.name).join('\n'));

function getSupportedCurrencies() {
  function $(amount, currency) {
    return Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currency,
      currencyDisplay: 'name'
    }).format(amount);
  }
  return knownCodes.reduce((currencies, cur) => {
    return (output => {
      return output.replace(/^[^ ]+ /, '') !== cur ?
        currencies.concat({
          code: cur,
          name: output.match(/(?<= ).+/)[0]
        }) :
        currencies;
    })($(0, cur).trim());
  }, []);
}

function getSampleCurrencyXml() {
  return `
    <ISO_4217 Pblshd="2018-08-29">
      <CcyTbl>
        <CcyNtry>
          <CtryNm>
            UNITED KINGDOM OF GREAT BRITAIN AND NORTHERN IRELAND (THE)
          </CtryNm>
          <CcyNm>Pound Sterling</CcyNm>
          <Ccy>GBP</Ccy>
          <CcyNbr>826</CcyNbr>
          <CcyMnrUnts>2</CcyMnrUnts>
        </CcyNtry>
        <CcyNtry>
          <CtryNm>UNITED STATES OF AMERICA (THE)</CtryNm>
          <CcyNm>US Dollar</CcyNm>
          <Ccy>USD</Ccy>
          <CcyNbr>840</CcyNbr>
          <CcyMnrUnts>2</CcyMnrUnts>
        </CcyNtry>
      </CcyTbl>
    </ISO_4217>
  `;
}
.as-console-wrapper { top: 0; max-height: 100% !important; }

If you want to generate the codes still, you can use a product iterable.

The following is based on Python's itertools.product function.

let ary = product('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), 3).map(a => a.join(''));

function product(iterables, repeat) {
  var argv = Array.prototype.slice.call(arguments), argc = argv.length;
  if (argc === 2 && !isNaN(argv[argc - 1])) {
    var copies = [];
    for (var i = 0; i < argv[argc - 1]; i++) { copies.push(argv[0].slice()); }
    argv = copies;
  }
  return argv.reduce((accumulator, value) => {
    var tmp = [];
    accumulator.forEach(a0 => value.forEach(a1 => tmp.push(a0.concat(a1))));
    return tmp;
  }, [[]]);
}

Demo

console.log(getSupportedCurrencies().map(c => c.code + '\t' + c.name).join('\n'));

function getSupportedCurrencies() {
  function $(amount, currency) {
    return Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currency,
      currencyDisplay: 'name'
    }).format(amount);
  }
  let ary = product('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), 3).map(a => a.join(''));
  return ary.reduce((currencies, cur) => {
    return (output => {
      return output.replace(/^[^ ]+ /, '') !== cur
        ? currencies.concat({ code : cur, name : output.match(/(?<= ).+/)[0] })
        : currencies;
    })($(0, cur).trim());
  }, []);
}

function product(iterables, repeat) {
  var argv = Array.prototype.slice.call(arguments), argc = argv.length;
  if (argc === 2 && !isNaN(argv[argc - 1])) {
    var copies = [];
    for (var i = 0; i < argv[argc - 1]; i++) { copies.push(argv[0].slice()); }
    argv = copies;
  }
  return argv.reduce((accumulator, value) => {
    var tmp = [];
    accumulator.forEach(a0 => value.forEach(a1 => tmp.push(a0.concat(a1))));
    return tmp;
  }, [[]]);
}
.as-console-wrapper { top: 0; max-height: 100% !important; }
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
  • You probably want to filter all the things in that list that have `IsFund="true"` and any currency code that starts with `X` because those aren't really currencies. – Boris Verkhovskiy Sep 01 '20 at 19:54