onepub-dev / money.dart

Dart implementation of Money and Currency classes with Money formatter.
MIT License
60 stars 32 forks source link

Support all ISO 4217 currencies and locales #43

Open britannio opened 3 years ago

britannio commented 3 years ago

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 commented 3 years ago

https://github.com/bengourley/currency-symbol-map/blob/master/map.js may help

bsutton commented 3 years ago

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 commented 3 years ago

I'll look into it this week!

britannio commented 3 years ago

So far I've found https://github.com/google/closure-library/blob/master/closure/goog/i18n/numberformatsymbols.js based off of the CLDR.

bsutton commented 3 years ago

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

bsutton commented 2 years ago

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

britannio commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

We should expand the currency to include the additional fields

luissalgadofreire commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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:

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 commented 2 years ago

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 commented 2 years ago

OK, so that is a slightly different problem than what I thought you were talking about. So yes I agree that is an issue.

A key objective of the Money2 package is that it should be a swiss army knife for utilising Money so I don't see forcing users to resort to intl as being an option. intl also won't do what we need.

The intl.NumberFormat doesn't support all of the features of Money so that would be a backward step. The formatter is also not compatible with Money2 formatter which would be a major breaking change. The formatter is actually not the problem as what we have works and will work if we move in the direction I outline below.

The issue is that I incorrectly associated currency with a format, when it should have been a locale.

So I think this means that we need to support locales in the money package.

From your research it looks like we can get a complete set of locales and then generate the necessary code. I don't want to force users to ship a json file as there is no consistent way of doing this. In a flutter app you can use assets but on the cli you have to use something like 'dcli pack'. A core aim of Money2 is that it is simple to use. Having to manage an asset to ship Money2 falls out of my definition of simple :) I'm also not in love with the performance cost of parsing a large json file on startup. Memory usage is not a consideration as the json file will still need to be loaded into memory. I guess we could use a serialisation technique but this would either require the user to know the set of currencies they use in advance (which is some case they would) or simply re-parse the json file each time they mention a new locale (this sounds problematic).

So the question is how we do this whilst minimising breaking changes.

Using your suggestions of getting a json file containing all of the locales we can generate a new 'locales' file similar to the existing common currencies.

The issue is that currently we have a lt of methods of the from 'fromCurrency('USD'); These would need to be changed to fromLocale('en_US'); I'm also not a fan of users using strings such as 'en_US' so we need to define these.

We could do something like fromLocale(Locales.enUS);

We then deprecate the existing fromCurrency type methods and eventually removing them in favour of the fromLocale equivalents.

S. Brett Sutton Noojee Contact Solutions 03 8320 8100

On Sat, 18 Dec 2021 at 09:02, luissalgadofreire @.***> wrote:

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 https://www.freeformatter.com/germany-standards-code-snippets.html: 999 999 999,99 € Germany https://www.freeformatter.com/france-standards-code-snippets.html: 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 http://cldr.unicode.org/:

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 https://polaris.shopify.com/foundations/formatting-localized-currency by Shopify.

They use Common Locale Database Repository (CLDR) http://cldr.unicode.org/ 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.

— Reply to this email directly, view it on GitHub https://github.com/noojee/money.dart/issues/43#issuecomment-997057011, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG32OFUFG3DC57RBEPZHY3UROXQVANCNFSM5AQE5IVA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

You are receiving this because you commented.Message ID: <noojee/money. @.***>

luissalgadofreire commented 2 years ago

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:

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

Okay so what is required here ?

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

luissalgadofreire commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

luissalgadofreire commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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:

That's quite convenient and straight out of intl.

cedvdb commented 2 years ago

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

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

bsutton commented 2 years ago

Appreciate the contribution.

I'm snowed for the next month or two but this is in my to-do list.

On Tue, 26 Apr 2022, 11:44 am cedvdb, @.***> wrote:

@bsutton https://github.com/bsutton here are all generated classes if that helps:

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

— Reply to this email directly, view it on GitHub https://github.com/noojee/money.dart/issues/43#issuecomment-1109210638, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG32ODED4PZ4ZLVQS45DNDVG5C7VANCNFSM5AQE5IVA . You are receiving this because you were mentioned.Message ID: @.***>

lukepighetti commented 9 months ago

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 commented 9 months ago

Luke, if you raise a PR for SEK support I'm happy to publish a new version.

You need to add it to CommonCurrencies.dart

On Fri, Dec 29, 2023 at 7:08 AM Luke Pighetti @.***> wrote:

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

— Reply to this email directly, view it on GitHub https://github.com/onepub-dev/money.dart/issues/43#issuecomment-1871459569, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG32OCJWJS2J2QW5NAMMH3YLXGUHAVCNFSM5AQE5IVKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOBXGE2DKOJVGY4Q . You are receiving this because you were mentioned.Message ID: @.***>

bsutton commented 6 months ago

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.