Open britannio opened 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.
I'll look into it this week!
So far I've found https://github.com/google/closure-library/blob/master/closure/goog/i18n/numberformatsymbols.js based off of the CLDR.
If your are going to port the code check the licence is compatible.
I would be great if someone could help build out a full set of iso code.
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.
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.
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"
}
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.
We should expand the currency to include the additional fields
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.
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.
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')
As an alternative, because
intl
exists in the Dart world, one can try to simply wrapintl
's NumberFormat inmoney2
's formatting methods, just asPyMoney
usesbabel
.If
intl
does the job well, you can provide the convenience inmoney2
without needing to maintain formatting rules per locale.
Int'l doesn't do the job which is why I created the format method.
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.
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.
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. @.***>
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.
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.
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.
Okay so what is required here ?
Concerning the original issue: Which file should be used to generate the Currencies classes ?
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.
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
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
.
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.
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.
Don't you risk overflowing for big values ? Bitcoins can have huge values, the int is not 64 bit when running on web platform
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.
But again, the code is shared as inspiration for the formatting part. The rest is covered already by money2
.
@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.
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.
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.
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.
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
.
@bsutton here are all generated dart instances if that helps:
https://github.com/cedvdb/dart_price/blob/main/lib/src/currency_map.dart
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: @.***>
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
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: @.***>
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.
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