dart-lang / i18n

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

Intl round doubles wrong #837

Closed asier-escofet-sagarribay closed 1 day ago

asier-escofet-sagarribay commented 1 month ago

Bug When the last decimal to round is a 5:

To Reproduce

void main(List<String> arguments) {
  // Correct: Round 12.5 -> 13 expected 13
  print(
      NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 0).format(12.5));
  // Correct: Round 12.75 -> 12.8 expected 12.8
  print(
      NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 1).format(12.75));
  // Inorrect: Round 12.85 -> 12.8 expected 12.9
  print(
      NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 1).format(12.85));
  // Inorrect: Round 12.95 -> 12.9 expected 13.00
  print(
      NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 1).format(12.95));
  // Correct: Round 12.195 -> 12.20 expected 12.20
  print(NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 2)
      .format(12.195));
  // Inorrect: Round 12.295 -> 12.29 expected 12.30
  print(NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 2)
      .format(12.295));
  // Correct: Round 12.29500000001 -> 12.30 expected 12.30
  print(NumberFormat.simpleCurrency(name: 'USD', decimalDigits: 2)
      .format(12.29500000001));
}

System info Dart SDK 3.3.0 Flutter SDK 3.19.0 intl 0.19.0

FourLeafTec commented 1 day ago

Same question with more precision.

// Got 1.123 expected 1.124 NumberFormat("#.###").format(1.1235)

mosuem commented 1 day ago

I believe this is working as intended, see the example in the docs.

FourLeafTec commented 1 day ago

Okay, but this really looks strange.

In the following example, I make the pattern one character shorter than the input each time to test the round function, and the last digit is sometimes 6 and sometimes 5.

  print('#       ${NumberFormat("#").format(5.5)}');
  print('#.#     ${NumberFormat("#.#").format(5.55)}');
  print('#.##    ${NumberFormat("#.##").format(5.555)}');
  print('#.###   ${NumberFormat("#.###").format(5.5555)}');
  print('#.####  ${NumberFormat("#.####").format(5.55555)}');
  print('#.##### ${NumberFormat("#.#####").format(5.555555)}');

  print('#.#     ${NumberFormat("#.#").format(5.550001)}');

Output:

#       6
#.#     5.5
#.##    5.55
#.###   5.556
#.####  5.5556
#.##### 5.55556
#.#     5.6

In my understanding, regardless of whether mathematical rounding or banker's rounding(IEEE 754) is used, the result should be the same, either all 5s or all 6s.

eernstg commented 1 day ago

@FourLeafTec, the results are surprising to you, presumably, because you are assuming that a piece of source code like 5.55 is evaluated at run time as a floating point representation whose value is precisely 5.55. This is not true. For example:

void main() {
  print(5.5.toStringAsFixed(20)); // 5.50000000000000000000
  print(5.55.toStringAsFixed(20)); // 5.54999999999999982236
  print(5.555.toStringAsFixed(20)); // 5.55499999999999971578
  print(5.5555.toStringAsFixed(20)); // 5.55550000000000032685
  print(5.55555.toStringAsFixed(20)); // 5.55555000000000021032
  print(5.555555.toStringAsFixed(20)); // 5.55555500000000002103
  print(5.5555555.toStringAsFixed(20)); // 5.55555549999999964683
  print(5.55555555.toStringAsFixed(20)); // 5.55555555000000023114
}

The reason why the floating point value (using a representation that is specified in IEEE 754) cannot precisely represent a value like 5.55 is that it uses the binary rather than the decimal number system. There is simply no finite sequence of binary digits that yields the value 5.55. In contrast, 5.5 decimal is exactly 101.1 binary.

It's somewhat similar to the situation with the decimal number system and a value like a third: values like 0.3, 0.33, 0.333 are better and better approximations of one third, but there is no finite sequence of digits that encodes one third exactly.

Using the number system with base 3 it would be trivial: 0.1 (as a "ternary" number) is exactly one third.

When you don't start out with precisely 5.55 but rather have 5.54999999999999982236 (approximately, because we are now looking at a decimal encoding of a value which was given in binary ;-), rounding will behave differently.

So it is still working as specified. ;-)

FourLeafTec commented 1 day ago

OMG!

I can't believe I was so foolish to forget about the precision loss issue with float/double. @mosuem You're absolutely right, this is working as intended. @eernstg Thank you so much for clarifying this for me.