dart-lang / i18n

A general mono-repo for Dart i18n and l10n packages.
BSD 3-Clause "New" or "Revised" License
64 stars 39 forks source link

Can't format BigInt #692

Open uzuki-P opened 1 year ago

uzuki-P commented 1 year ago

Describe the bug Formatting BigInt value return type 'int' is not a subtype of type 'BigInt' of 'bigInt'. Is it intended?

To Reproduce Add a minimal working example or, if not possible or available, any code which might help to reproduce the problem

test('BigInt on Formatter', () async {
  const number = '12345678901234567890.1234567890';
  final leftSplitNumber = BigInt.parse(number);
  final f = NumberFormat("###,###.########", "en_US");
  final formatted = f.format(leftSplitNumber).toString();

  print(formatted);
});

System info

System Info ``` Dart SDK 3.0.6 Flutter SDK 3.10.6 bigint 1.0.0+1 dependencies: - cupertino_icons 1.0.5 - flutter 0.0.0 [characters collection js material_color_utilities meta vector_math sky_engine] - intl 0.18.1 [clock meta path] dev dependencies: - flutter_lints 2.0.2 [lints] - flutter_test 0.0.0 [flutter test_api path fake_async clock stack_trace vector_math async boolean_selector characters collection js matcher material_color_utilities meta source_span stream_channel string_scanner term_glyph] transitive dependencies: - async 2.11.0 [collection meta] - boolean_selector 2.1.1 [source_span string_scanner] - characters 1.3.0 - clock 1.1.1 - collection 1.17.1 - fake_async 1.3.1 [clock collection] - js 0.6.7 [meta] - lints 2.1.1 - matcher 0.12.15 [async meta stack_trace term_glyph test_api] - material_color_utilities 0.2.0 - meta 1.9.1 - path 1.8.3 - sky_engine 0.0.99 - source_span 1.9.1 [collection path term_glyph] - stack_trace 1.11.0 [path] - stream_channel 2.1.1 [async] - string_scanner 1.2.0 [source_span] - term_glyph 1.2.1 - test_api 0.5.1 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph matcher] - vector_math 2.1.4 ```
mosuem commented 1 year ago

I think it was not intended, as the type of the format method was intentionally kept to be dynamic and not num to also allow other numeric types such as BigInt to be a valid input.

However, the logic inside the _formatFixed method called by format in your example relies on the argument supporting things like multiplication with a double using the * operator, which BigInt does not do. So effectively, BigInt is not supported for all cases.

A possible solution would be to refactor the code to accept any class supporting a tbd interface with all necessary methods. This would enable users to pass in their own preferred numeric type, provided it implements this interface. This interface could also be implemented by int and double or num to avoid having to wrap those classes.

Unfortunately, such logic can become quite verbose, as many cases have to be considered.

rakudrama commented 1 year ago

It is possible to write a wrapper class that provides the required operations with the types used by the algorithm. (I know of one case where a BigInt is wrapped in a class that makes it look like the value, but scaled to 'micro-units', e.g. 1234567 represents 1.234567). This is a fairly heavy-weight solution since there can be many intermediate objects of this class in the formatting computation.

It is unlikely that an interface will be added to int.

An easier to-use option is to make the formatting work with a string of digits.

One way this could happen is for the format method to accept a string and optional scale. This permits the formatting of 'numeric' data from any source:

    final f1 = NumberFormat("###,###.00", "en_US");
    print(f1.format('123456789'));                //  123,456,789.00
    print(f1.format('123456789', scale: -6));     //  123.46
    final f2 = NumberFormat("###,###.00", "my");
    print(f2.format('123456789', scale: -6));     //  ၁၂၃.၄၆

The rounding arithmetic would have to happen on the digits.

As a convenience we could add an interface that some classes implement that return the digits and scale.

abstract interface class NumberFormatDigits {
  (String, int) digitsAndScale(/*some parameters TBD*/);
}

If the input to format is not a num or String, but implements this interface, the results are used as if the format method is passed the digits and scale.

This interface would probably have to be parts of dart:core so that classes like Int64, BigInt, BigDecimal, WeirdTenDigitNumber don't have a dependency on Intl.

digitsAndScale may require some parameters to help generate digits. The there are an infinite number of digits in the decimal expansion of the rational number 2/3, but only a few are required for format "#.00". The returned records ("666", -3) and ("666666", -6) would give the same formatted result (0.67). The design of these parameters requires more work, but should be such that many cases can ignore them.

/cc @lrhn What do you think about dart:core having some minimal support like this abstract interface?

mosuem commented 1 year ago

AFAIK, treating any input as a (List<int> digits, int scale, bool sign) is how ICU4X is solving the problem. Thus the upcoming package:intl4x will hopefully be able to format any kind of number. This solution is also possible for the current package:intl, but would require rewriting all formatting operations in terms of this new interface, which would be a large change.

lrhn commented 1 year ago

The (bool negative, List<int> digits, int exponent) representation is equivalent to toString, so you could just use that. The only difference would be that you can omit trailing zeros, moving them into the exponent, which may be a significant number of digits for a BigInt, and don't have to search backwards from the end looking for an e, to see if there is an exponent or not. But that's cheap compared to creating the string to begin with.

If you can ask for a specific number of significant digits, it becomes more interesting. A big part of double's toString is figuring out how many digits are needed to be unique. For integers, it's probably not that big a deal, since you have to do all the divisions by 10 to get to the high digits anyway.

kikuchy commented 6 months ago

Any updates? We have to format numbers with large digits.

mosuem commented 6 months ago

Any updates? We have to format numbers with large digits.

This is a planned functionality for package:intl4x.