Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support all ISO 4217 currencies and locales #43

Open
britannio opened this issue Jul 16, 2021 · 40 comments
Open

Support all ISO 4217 currencies and locales #43

britannio opened this issue Jul 16, 2021 · 40 comments
Labels
help wanted Extra attention is needed

Comments

@britannio
Copy link

britannio commented Jul 16, 2021

Alternatively, the currencies listed in https://support.google.com/googleplay/android-developer/answer/9306917 would suffice for my use case.

The missing currencies from that link are:
AED CLP COP CRC CZK DKK EGP HKD HUF IDR ILS LBP MYR PEN PKR RON SAR SEK SGD THB UAH UYU VND
@britannio
Copy link
Author

@bsutton
Copy link
Collaborator

bsutton commented Jul 16, 2021

Would you be door to submit a pr with an updated CommonCurrencies class.

I don't have the time to do this at the moment.

@britannio
Copy link
Author

I'll look into it this week!

@britannio
Copy link
Author

@bsutton
Copy link
Collaborator

bsutton commented Jul 19, 2021

If your are going to port the code check the licence is compatible.

@bsutton bsutton added the help wanted Extra attention is needed label Nov 21, 2021
@bsutton
Copy link
Collaborator

bsutton commented Nov 21, 2021

I would be great if someone could help build out a full set of iso code.

@britannio
Copy link
Author

I found https://pub.dev/documentation/intl/latest/intl/NumberFormat/NumberFormat.simpleCurrency.html which supports my use case. The intl package has ISO 4217 data that would be useful here.

@luissalgadofreire
Copy link

luissalgadofreire commented Dec 17, 2021

If I understand correctly, all that is required is to add more code snippets like the below to the CommonCurrencies class:

/// Australian Dollar
  final Currency aud = Currency.create('AUD', 2,
      pattern: 'S0.00',
      country: 'Australian',
      unit: 'Dollar',
      name: 'Australian Dollar');

There are a few sources out there with enough information to fill the above with at least close to all the ISO 4217 currencies.

This repository, for instance, has a MIT license and provides a JSON file with a format like:

{
  'AMD': {                          // ISO 4217 currency code.
     'name': 'Armenian Dram',       // Currency name.
     'fractionSize': 2,             // Fraction size, a number of decimal places.
     'symbol': {                    // Currency symbol information.
         'grapheme': 'դր.',         // Currency symbol.
         'template': '1 $',         // Template showing where the currency symbol should be located
                                    // (before or after amount).
         'rtl': false               // Writing direction.
     },
     'uniqSymbol': {                // Alternative currency symbol. We recommend to use it when you want
                                    // to exclude a repetition of symbols in different currencies.
         'grapheme': 'դր.',         // Alternative currency symbol.
         'template': '1 $',         // Template showing where the alternative currency symbol should be
                                    // located (before or after amount).
         'rtl': false               // Writing direction.
     }
  },
  ...
}

The accompanying library actually provides a formatter based on the above JSON.

I'll investigate a little further to validate and come back with a proper source.

If I find there is viable info with an appropriate license (again, the above is MIT), I might take some time to help out.

If this goes well, I suggest then adding the possibility of creating a currency with a simple static method like:

Currency.fromCode('USD')

This will make it easy to integrate with any source that simply provides an ISO 4127 currency code.

Better yet, create a Money instance like:

Money.fromIntWithCurrency(1000, 'USD')

instead of instantiating the currency before, manually.

@luissalgadofreire
Copy link

Yet another source with MIT license. This one is much more recent and seems to have more complete base info. Sample currency info provided:

{
    "name": "Afghan Afghani",
    "iso4217": "AFN",
    "isoNumeric": null,
    "symbol": "؋",
    "subunit": null,
    "subunitToUnit": null,
    "prefix": "",
    "suffix": "؋",
    "decimalMark": ".",
    "thousandsSeparator": ",",
    "decimalPlaces": 2,
    "shortName": "Afghani"
  }

@bsutton
Copy link
Collaborator

bsutton commented Dec 17, 2021

Thanks for the interest.

If I understated you correctly you are proposing that we ship a json file with the package and the proposed new method parses the json file and generates a currency.

Can I suggest an alternate method.

We use the json file to generate the common currency class.

The removes the overhead of parsing json and means we don't have to find a way to ship the json file with the package.

@bsutton
Copy link
Collaborator

bsutton commented Dec 17, 2021

We should expand the currency to include the additional fields

@luissalgadofreire
Copy link

luissalgadofreire commented Dec 17, 2021

Hmm.

After some investigation, I think it is a problem to want to keep currency formatting in this library.

Investigating further, the localization of a currency does not depend on the currency but on the locale. That means that one currency may be displayed differently depending on the locale. One can maintain a simpler JSON file with the main representation of a currency as per its (main) source country/region; but CLDR, the international standard, maintains a JSON file per locale/country, where all currencies are referenced for each locale/country. This makes it a huge source to maintain localization, best handled by a specialized library like Intl.

I would therefore tend to use this library for its precise calculation as per currency rules; and leave formatting to more specialized libraries.

Django's PyMoney, for instance, does allow formatting. But formatting, as you can see, requires passing a locale:

>>> from moneyed.l10n import format_money
>>> format_money(Money(10, USD), locale='en_US')
'$10.00'

But, underneath, that method above is a thin wrapper of Python's known babel localization library. In their documentation, you can see the following example, where USD is formatted differently according to locale:

>>> format_currency(1099.98, 'USD', locale='en_US')
u'$1,099.98'
>>> format_currency(1099.98, 'USD', locale='es_CO')
u'US$\xa01.099,98'

I would therefore recommend focusing on the proper math of manipulating money and leaving formatting to a specialized localization library, like intl.

To be honest, my interest in money2 is solely on the calculation side of it. I will leave the formatting to Intl. Still, it would be interesting to have the currencies predefined with the allowed decimal positions for proper money handling.

@luissalgadofreire
Copy link

luissalgadofreire commented Dec 17, 2021

As an alternative, because intl exists in the Dart world, one can try to simply wrap intl's NumberFormat in money2's formatting methods, just as PyMoney uses babel.

If intl does the job well, you can provide the convenience in money2 without needing to maintain formatting rules per locale.

@bsutton
Copy link
Collaborator

bsutton commented Dec 17, 2021

the localization of a currency does not depend on the currency but on the locale.

I don't think your interpretation is correct.

Common currencies provides the default formatting for a specific currency.

With money, if you want to format other currencies with your locale you can do this by providing a custom format.

It does make we wonder if we need a standard method to do this.

Money.formatUsing('usd')

@bsutton
Copy link
Collaborator

bsutton commented Dec 17, 2021

As an alternative, because intl exists in the Dart world, one can try to simply wrap intl's NumberFormat in money2's formatting methods, just as PyMoney uses babel.

If intl does the job well, you can provide the convenience in money2 without needing to maintain formatting rules per locale.

Int'l doesn't do the job which is why I created the format method.

@luissalgadofreire
Copy link

luissalgadofreire commented Dec 17, 2021

I may be wrong but I have seen enough evidence suggesting that a proper complete way of handling currency formatting requires using locale.

An example for France and Germany, both having EUR as currency:

France: 999 999 999,99 €
Germany: 999.999.999,99 €

The main difference above is number formatting. France uses spaces, Germany uses dots for thousands separator.

Overall, we have the following concerns, as per CLDR:

a) Whether the currency symbol appears before or after the amount (for example, $250, 250 USD, 250 $)
b) Whether decimals are used (for example, there are no “cents” in Japanese yen)
c) Whether the decimal sign is a period or a comma (for example, 37,50 or 37.50)
d) How to group numbers (for example, 10,000 or 1,0000, or using spaces)

The above concerns are taken from this post by Shopify.

They use Common Locale Database Repository (CLDR) for localization formatting for currency, date, time, and amount.

As per Shopify, CLDR:

  • It’s the recognized international standard
  • It automatically formats numbers and currency based on the merchant’s locale
  • The repository is maintained by a third party.

c) and d) above are supported by something like intl. b) depends on currency information we can get from ISO 4217. Not sure though if a) is currency specific or locale specific.

But it seems to me a combination of localized number formatting and specific currency rules is what provides the proper localized format for a currency.

Also, babel, which I mentioned above, also uses CLDR: http://babel.pocoo.org/en/latest/dev.html#tracking-the-cld. I suspect most localization tools including currency localization, will.

@luissalgadofreire
Copy link

luissalgadofreire commented Dec 17, 2021

Trying it out in dartpad, works as expected, including currency fractional units.

import 'package:intl/intl.dart';

void main() {
  var formatter = new NumberFormat.currency(name: 'EUR', locale: "fr_FR");
  print(formatter.format(1000.567));
  // 1 000,57 EUR
  formatter = new NumberFormat.currency(name: 'EUR', locale: "de");
  print(formatter.format(1000.567));
  // 1.000,57 EUR
  formatter = new NumberFormat.currency(name: 'EUR', locale: "fr_FR", symbol: "€");
  print(formatter.format(1000.567));
  // 1 000,57 €
  formatter = new NumberFormat.currency(name: 'EUR', locale: "de", symbol: "€");
  print(formatter.format(1000.567));
  // 1.000,57 €
}

A simple wrapper around this would do the trick.

Link to intl's data file: https://github.com/dart-lang/intl/blob/master/lib/number_symbols_data.dart, also based on CLDR.

Each locale includes currency pattern and default currency code.

All this said, this would not exclude the need for an updated CommonCurrencies, as minor units is also important for proper rounding and calculations.

@bsutton
Copy link
Collaborator

bsutton commented Dec 18, 2021 via email

@luissalgadofreire
Copy link

luissalgadofreire commented Dec 18, 2021

I decided to port something I already have in Java for my purposes. I also require rounding modes, among other things.

I would however suggest you viewing money2 as two separate but interconnected parts:

  • money2 the calculation engine:
    a. For this, it makes sense to add all the ISO 4217 currencies with their respective minor units. That information is available on wikipedia.
    b. I would also add the locale as an optional argument to the constructors and Money.from* factory methods and assume a default locale if none is passed. That way you can make sure any time toString is called, a proper localized currency format is used. But that formatting implementation would be the responsibility of the bullet below.
    c. I would add an init() static method to set the default locale and currency at class level, so that these could be determined by the user at application start and not burden them with setting them for every instantiation of Money, for apps with single currency, the majority.
  • money2 the currency formatter:
    a. I would honestly replace all the internals in money2 for formatting with intl. You already have the dependency. It's just a matter of using it for currency formatting too. Maintaining up-to-date alignment with an international standard like CLDR can be cumbersome. Google has the means and resources for that. You would just have to call their currency formatter as in the examples above and leave the maintenance of the underlying data, whether in dart or json, to them.

That way, you would have sane, convenient, powerful money calculations as per now plus enterprise-grade localization of currency formats, with minimum maintenance.

You would still have a Swiss army knife for money handling and with your own API but using underlying resources in an efficient way.

@cedvdb
Copy link

cedvdb commented Mar 24, 2022

So there is two issues here, localizations and all currences which are separate issues.

I got to say I agree with what was said about localization. I'm not sure what is the problem with intl package as outlined before it formats numbers correctly with locale, as opposed to money2 which does not. It is also currently the go to package, that is the most likely to be maintained (although it really is not well maintained currently despite being a dart-lang package). I'd much rather have a solid money library focused on that task without requiring much maintenance.

As for the generation of the currency instances with json, that's definitely a better approach than reading the json file. I could do it if I happen to choose this package or roll my own for price management. However depending on when static meta programming is a thing, it might be worth to wait for it, although, depending on the json completeness, it's really an easy task.

@bsutton
Copy link
Collaborator

bsutton commented Mar 24, 2022

I would have to go back and look but I think there was a problem with the intl formatter not supporting all the options we do. From recollection, we use the intl formatter but have to break the parts up (decimal, integer) to make it work.

I'm also not convinced that supporting the formats for each locale is particularly burdensome as I would be shocked if any of these ever change.
This makes it a matter of finding a list of the formats and building a once-off parser to generate the formats in a Money2 compatible manner.
In response to @luissalgadofreire suggestion about an init call, I don't think this will be necessary if we make the locale a named arg with a default that is the system default locale.

@cedvdb
Copy link

cedvdb commented Mar 30, 2022

Okay so what is required here ?

Concerning the original issue: Which file should be used to generate the Currencies classes ?

@luissalgadofreire
Copy link

luissalgadofreire commented Mar 30, 2022

I ended up implementing my own stuff based on this repo, which has a permissible BSD3 license.

There is a considerable difference in their approach. It stores and operates amounts as integers with minor units to avoid rounding issues. It's the alternative to using a decimal type offering the precision that float lacks. However, that is not relevant for money2. It's the formatting part that is being discussed here.

I made several additions to the above mentioned repo's code but the main addition pertains to formatting.

Take a look at the fundamental files here. You'll find a currency.dart and currency.data.dart files. You'll also find a region Formatting Methods in the money.dart file.

The currency.data.dart contains the currency data, including minor units and symbol, used by intl to properly format currency amounts.

The money.dart file contains the format(), simpleFormat(), compactFormat(), and compactSimpleFormat(). They all use intl's NumberFormat underneath, informed by locale and currency code, minor units, and symbol.

Feel free to use it as you see fit.

@cedvdb
Copy link

cedvdb commented Mar 30, 2022

Thank you, are you going to publish it on pub.dev? The only thing I see missing is to compose your own currency registry, currently you are using all the currencies but an user of the API might want to use only two (for example EUR and USD), the implementation does not allow tree shaking like this.

Something like the registry in this lib https://pub.dev/packages/money

@luissalgadofreire
Copy link

luissalgadofreire commented Mar 30, 2022

I'm afraid I'm not planning on publishing it.

I don't mind if you do or if you use it just to pick relevant parts from it to update money2.

However, bear in mind that my version leads to storing amounts in int, not in decimal. Many developers may prefer to use decimal instead, as with money2.

@cedvdb
Copy link

cedvdb commented Mar 30, 2022

yes I saw, I guess that could be a problem for currencies like bitcoin.

I'll update money2 with the values found there even if I end up not using it in about 2 days once I get onto my money task.

@luissalgadofreire
Copy link

luissalgadofreire commented Mar 30, 2022

I believe it will work for any currency, including bitcoin. It's just a matter of preference, that's all. Personally, I don't mind using this method, which I believe is common in the banking sector.

@cedvdb
Copy link

cedvdb commented Mar 30, 2022

Don't you risk overflowing for big values ? Bitcoins can have huge values, the int is not 64 bit when running on web platform

@luissalgadofreire
Copy link

Didn't understand you were referring to int size. If that comes to a problem, one can just use BigInt, which is now an official type in dart 2+.

My remark was the fact that amounts are expressed as integers, not decimals, and that is a matter of preference.

@luissalgadofreire
Copy link

But again, the code is shared as inspiration for the formatting part. The rest is covered already by money2.

@luissalgadofreire
Copy link

luissalgadofreire commented Mar 30, 2022

@cedvdb, just a side note. You reminded me of my postponement to evolve from int. I just moved to BigInt. All tests complete successfully, including new ones testing extremely big integers. Updated in the snippets. Thanks for the reminder.

@bsutton
Copy link
Collaborator

bsutton commented Mar 30, 2022

There was a reason we move from bigint to decimal.
This need came out of the currency conversion code which required a fixed decimal value which was not a money.
The result was the Fixed package.
The developer of decimal offer to help implement fixed which is why we ended up using decimal under the hood as it greatly simplified the implementation (compared to bigint) with no loss of precision and support for large number/large scale as required by bitcoin etc.

As to the registry, I don't believe that will help with tree shaking unless we remove a chunk of functionality in the parser that is able to 'find' a currency.

@cedvdb
Copy link

cedvdb commented Mar 30, 2022

The registry will help if the currencies are provided by the user.

EG

CurrencyRegistry(allCurrencies); 
or
CurrencyRegistry([usd,eur]); // eur and usd imported from money2, the whole array isn't imported and is therefor tree shaken

Admittedly that feature might not be worth it, I personally do not have a use for this.

I'm also still skeptical about the non use of intl package. Could you highlight what feature was not supported by intl ? Have you had a look at https://api.flutter.dev/flutter/intl/NumberFormat/NumberFormat.currency.html

Most people already use it so, there is no need to have two version of formats. Maybe the size of all the formats is negligible but I see no reason to live with duplicates, and there will be duplicates because flutter apps with localization also use intl.

It also means that the formats might differ from an official flutter widget.

@luissalgadofreire
Copy link

Yes, conversions require the possibility of having multiplication/division operations between BigInt and non-BigInt numbers, which BigInt does not support. However, I used rational for those very specific operations. And it works fine, now with huge numbers, also. It turns out decimal uses rational underneath for such operations.

Nevertheless, using BigInt or decimal are both valid approaches.

The main concern in this thread was formatting. I just shared how I proceeded and honestly am quite convinced using intl underneath is what makes sense, with the addition of a proper currency data structure as in the code I shared. It works with little effort.

Good luck with the project.

@luissalgadofreire
Copy link

luissalgadofreire commented Mar 30, 2022

Last remark - formatting functions at work:

test('format()', () {
    Money.init(defaultLocale: 'en_US');
    expect(Money.simpleDouble(1200000.594, 'USD').format(), 'USD1,200,000.59');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'USD').format(), '1 200 000,59 USD');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'USD').format(), '1.200.000,59 USD');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'EUR').format(), '1 200 000,59 EUR');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'EUR').format(), '1.200.000,59 EUR');
});

test('simpleFormat()', () {
    Money.init(defaultLocale: 'en_US');
    expect(Money.simpleDouble(1200000.594, 'USD').simpleFormat(), '\$1,200,000.59');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'USD').simpleFormat(), '1 200 000,59 \$');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'USD').simpleFormat(), '1.200.000,59 \$');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'EUR').simpleFormat(), '1 200 000,59 €');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'EUR').simpleFormat(), '1.200.000,59 €');
});

test('compactFormat()', () {
    Money.init(defaultLocale: 'en_US');
    expect(Money.simpleDouble(1200000.594, 'USD').compactFormat(), 'USD1.2M');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'USD').compactFormat(), '1,2 M USD');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'USD').compactFormat(), '1,2 Mio. USD');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'EUR').compactFormat(), '1,2 M EUR');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'EUR').compactFormat(), '1,2 Mio. EUR'); 
});

test('compactSimpleFormat()', () {
    Money.init(defaultLocale: 'en_US');
    expect(Money.simpleDouble(1200000.594, 'USD').compactSimpleFormat(), '\$1.2M');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'USD').compactSimpleFormat(), '1,2 M \$');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'USD').compactSimpleFormat(), '1,2 Mio. \$');
    Money.init(defaultLocale: 'fr_FR');
    expect(Money.simpleDouble(1200000.594, 'EUR').compactSimpleFormat(), '1,2 M €');
    Money.init(defaultLocale: 'de_DE');
    expect(Money.simpleDouble(1200000.594, 'EUR').compactSimpleFormat(), '1,2 Mio. €');
});

The first one, format(), allows passing in a custom pattern. Moreover, in all of them - where relevant, locale, decimalDigits, and symbol can be overridden.

As you can see, intl knows:

  • if the currency code/symbol should be placed before or after the amount depending on locale;
  • the currency's minor units (2 decimal points for above used currencies/locales);
  • the localized number format;
  • the localized abbreviation for quantities (e.g. Mio. for millions in the German locale);
  • even the currency symbol for some currencies.

That's quite convenient and straight out of intl.

@cedvdb
Copy link

cedvdb commented Apr 26, 2022

@bsutton here are all generated dart instances if that helps:

https://github.com/cedvdb/dart_price/blob/main/lib/src/currency_map.dart

@bsutton
Copy link
Collaborator

bsutton commented Apr 26, 2022 via email

@bsutton bsutton changed the title Support all ISO 4217 currencies Support all ISO 4217 currencies and locales Dec 8, 2022
@lukepighetti
Copy link

Just started getting errors for SEK currency not supported. This is when using money2 to parse and format dates from RevenueCat for in-app purchases and subscriptions

@bsutton
Copy link
Collaborator

bsutton commented Dec 28, 2023 via email

@bsutton
Copy link
Collaborator

bsutton commented Apr 4, 2024

A full sets of currency codes is now supported (5.0.1) thanks to https://github.com/fueripe-desu.

Locales is still an outstanding issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

5 participants