Updating plural rules to support num instead of int

Subset of cr/247298271

See this document for more context:
https://docs.google.com/document/d/103PiFk2oopzG2ZT8n1TbWL2lMPgiXiEI3o05Eq45Lr4

The solution part is not up to date, but the problems identified are still valid.
That is what I am trying to solve, by improving the existing class, no a rewrite.

PiperOrigin-RevId: 258316144
diff --git a/lib/intl.dart b/lib/intl.dart
index 0981c86..5af5d07 100644
--- a/lib/intl.dart
+++ b/lib/intl.dart
@@ -173,7 +173,7 @@
   /// the placeholder automatically translated.
   static String message(String message_str,
           {String desc: '',
-          Map<String, Object> examples: const {},
+          Map<String, Object> examples,
           String locale,
           String name,
           List<Object> args,
@@ -267,11 +267,17 @@
     return '${aLocale[0]}${aLocale[1]}_$region';
   }
 
-  /// Format a message differently depending on [howMany]. Normally used
-  /// as part of an `Intl.message` text that is to be translated.
-  /// Selects the correct plural form from
-  /// the provided alternatives. The [other] named argument is mandatory.
-  static String plural(int howMany,
+  /// Formats a message differently depending on [howMany].
+  ///
+  /// Selects the correct plural form from the provided alternatives.
+  /// The [other] named argument is mandatory.
+  /// The [precision] is the number of fractional digits that would be rendered
+  /// when [howMany] is formatted. In some cases just knowing the numeric value
+  /// of [howMany] itsef is not enough, for example "1 mile" vs "1.00 miles"
+  ///
+  /// For an explanation of plurals and the [zero], [one], [two], [few], [many]
+  /// categories see http://cldr.unicode.org/index/cldr-spec/plural-rules
+  static String plural(num howMany,
       {String zero,
       String one,
       String two,
@@ -281,6 +287,7 @@
       String desc,
       Map<String, Object> examples,
       String locale,
+      int precision,
       String name,
       List<Object> args,
       String meaning,
@@ -295,12 +302,13 @@
         many: many,
         other: other,
         locale: locale,
+        precision: precision,
         name: name,
         args: args,
         meaning: meaning);
   }
 
-  static String _plural(int howMany,
+  static String _plural(num howMany,
       {String zero,
       String one,
       String two,
@@ -308,6 +316,7 @@
       String many,
       String other,
       String locale,
+      int precision,
       String name,
       List<Object> args,
       String meaning}) {
@@ -325,29 +334,40 @@
             few: few,
             many: many,
             other: other,
-            locale: locale);
+            locale: locale,
+            precision: precision);
   }
 
   /// Internal: Implements the logic for plural selection - use [plural] for
   /// normal messages.
-  static pluralLogic(int howMany,
-      {zero, one, two, few, many, other, String locale, String meaning}) {
+  static pluralLogic(num howMany,
+      {zero, one, two, few, many, other, String locale, int precision,
+      String meaning}) {
     if (other == null) {
       throw new ArgumentError("The 'other' named argument must be provided");
     }
     if (howMany == null) {
       throw new ArgumentError("The howMany argument to plural cannot be null");
     }
-    // If there's an explicit case for the exact number, we use it. This is not
-    // strictly in accord with the CLDR rules, but it seems to be the
-    // expectation. At least I see e.g. Russian translations that have a zero
-    // case defined. The rule for that locale will never produce a zero, and
-    // treats it as other. But it seems reasonable that, even if the language
-    // rules treat zero as other, we might want a special message for zero.
-    if (howMany == 0 && zero != null) return zero;
-    if (howMany == 1 && one != null) return one;
-    if (howMany == 2 && two != null) return two;
-    var pluralRule = _pluralRule(locale, howMany);
+
+    // This is for backward compatibility.
+    // We interpret the presence of [precision] parameter as an "opt-in" to
+    // the new behavior, since [precision] did not exist before.
+    // For an English example: if the precision is 2 then the formatted string
+    // would not map to 'one' (for example "1.00 miles")
+    if (precision == null || precision == 0) {
+      // If there's an explicit case for the exact number, we use it. This is
+      // not strictly in accord with the CLDR rules, but it seems to be the
+      // expectation. At least I see e.g. Russian translations that have a zero
+      // case defined. The rule for that locale will never produce a zero, and
+      // treats it as other. But it seems reasonable that, even if the language
+      // rules treat zero as other, we might want a special message for zero.
+      if (howMany == 0 && zero != null) return zero;
+      if (howMany == 1 && one != null) return one;
+      if (howMany == 2 && two != null) return two;
+    }
+
+    var pluralRule = _pluralRule(locale, howMany, precision);
     var pluralCase = pluralRule();
     switch (pluralCase) {
       case plural_rules.PluralCase.ZERO:
@@ -371,8 +391,8 @@
   static var _cachedPluralRule;
   static String _cachedPluralLocale;
 
-  static _pluralRule(String locale, int howMany) {
-    plural_rules.startRuleEvaluation(howMany);
+  static _pluralRule(String locale, num howMany, int precision) {
+    plural_rules.startRuleEvaluation(howMany, precision);
     var verifiedLocale = Intl.verifiedLocale(
         locale, plural_rules.localeHasPluralRules,
         onFailure: (locale) => 'default');
@@ -414,7 +434,6 @@
       String male,
       String other,
       String desc,
-      Map<String, Object> examples,
       String locale,
       String name,
       List<Object> args,
diff --git a/lib/src/plural_rules.dart b/lib/src/plural_rules.dart
index d6174f3..efa906c 100644
--- a/lib/src/plural_rules.dart
+++ b/lib/src/plural_rules.dart
@@ -16,6 +16,8 @@
 /// * t	- visible fractional digits in n, without trailing zeros.
 library plural_rules;
 
+import 'dart:math' as math;
+
 typedef PluralCase PluralRule();
 
 /// The possible cases used in a plural rule.
@@ -26,8 +28,12 @@
 
 /// This must be called before evaluating a new rule, because we're using
 /// library-global state to both keep the rules terse and minimize space.
-startRuleEvaluation(int howMany) {
+startRuleEvaluation(num howMany, [int precision = 0]) {
   _n = howMany;
+  _precision = precision;
+  _i = _n.round();
+  _updateVF(_n, _precision);
+  _updateWT(_f, _v);
 }
 
 /// The number whose [PluralCase] we are trying to find.
@@ -37,27 +43,81 @@
 // not introduce a subclass per locale or have instance tear-offs which
 // we can't cache. This is fine as long as these methods aren't async, which
 // they should never be.
-int _n;
+num _n;
 
-/// The integer part of [_n] - since we only support integers, it's the same as
-/// [_n].
-int get _i => _n;
-int opt_precision; // Not currently used.
+/// The integer part of [_n]
+int _i;
+int _precision;
 
-/// Number of visible fraction digits. Always zero since we only support int.
-int get _v => 0;
+/// Returns the number of digits in the fractional part of a number
+/// (3.1416 => 4)
+///
+/// Takes the item count [n] and a [precision].
+/// That's because a just looking at the value of a number is not enough to
+/// decide the plural form. For example "1 dollar" vs "1.00 dollars", the
+/// value is 1, but how it is formatted also matters.
+int _decimals(num n, int precision) {
+  var str = _precision == null ? '$n' : n.toStringAsFixed(precision);
+  var result = str.indexOf('.');
+  return (result == -1) ? 0 : str.length - result - 1;
+}
 
-/// Number of visible fraction digits without trailing zeros. Always zero
-/// since we only support int.
-//int get _w => 0;
+/// Calculates and sets the _v and _f as per CLDR plural rules.
+///
+/// The short names for parameters / return match the CLDR syntax and UTS #35
+///     (https://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax)
+/// Takes the item count [n] and a [precision].
+_updateVF(num n, int precision) {
+  int defaultDigits = 3;
 
-/// The visible fraction digits in n, with trailing zeros. Always zero since
-/// we only support int.
-int get _f => 0;
+  _v = precision ?? math.min(_decimals(n, precision), defaultDigits);
 
-/// The visible fraction digits in n, without trailing zeros. Always zero since
-/// we only support int.
-int get _t => 0;
+  int base = math.pow(10, _v);
+  _f = (n * base).floor() % base;
+}
+
+/// Calculates and sets _w and _t as per CLDR plural rules.
+///
+/// The short names for parameters / return match the CLDR syntax and UTS #35
+///     (https://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax)
+/// @param v Calculated previously.
+/// @param f Calculated previously.
+_updateWT(int v, int f) {
+  if (f == 0) {
+    // Unused, for now _w = 0;
+    _t = 0;
+    return;
+  }
+
+  while ((f % 10) == 0) {
+    f = (f / 10).floor();
+    v--;
+  }
+
+  // Unused, for now _w = v;
+  _t = f;
+}
+
+/// Number of visible fraction digits.
+int _v = 0;
+
+/// Number of visible fraction digits without trailing zeros.
+// Unused, for now int _w = 0;
+
+/// The visible fraction digits in n, with trailing zeros.
+int _f = 0;
+
+/// The visible fraction digits in n, without trailing zeros.
+int _t = 0;
+
+// An example, for precision n = 3.1415 and precision = 7)
+//   n  : 3.1415
+// str n: 3.1415000 (the "formatted" n, 7 fractional digits)
+//   i  : 3         (the integer part of n)
+//   f  :   1415000 (the fractional part of n)
+//   v  : 7         (how many digits in f)
+//   t  :   1415    (f, without trailing 0s)
+//   w  : 4         (how many digits in t)
 
 PluralCase get ZERO => PluralCase.ZERO;
 PluralCase get ONE => PluralCase.ONE;
diff --git a/test/plural_test.dart b/test/plural_test.dart
index f89f82b..9ab718e 100644
--- a/test/plural_test.dart
+++ b/test/plural_test.dart
@@ -195,6 +195,20 @@
     expect(() => plural(null, null), throwsArgumentError);
     expect(() => plural(null, "ru"), throwsArgumentError);
   });
+
+  verify_with_precision('1 dollar', 'en', 1, 0);
+  // This would not work in back-compatibility for one vs. =1 in plurals,
+  // because of this test in intl.dart:
+  //    if (howMany == 1 && one != null) return one;
+  // That one will ignore the precision and always return one, while the
+  // test below requires the result to be 'other'
+  // verify_with_precision('1.00 dollars', 'en', 1, 2);
+
+  verify_with_precision('1 dollar', 'en', 1.2, 0);
+  verify_with_precision('1.20 dollars', 'en', 1.2, 2);
+
+  verify_with_precision('3 dollars', 'en', 3.14, 0);
+  verify_with_precision('3.14 dollars', 'en', 3.14, 2);
 }
 
 verify(String expectedValues, String locale, pluralFunction) {
@@ -206,3 +220,15 @@
     });
   }
 }
+
+verify_with_precision(String expected, String locale, num n, int precision) {
+  test('verify_with_precision(howMany: $n, precision: $precision)', () {
+    var nString = n.toStringAsFixed(precision);
+    var actual = Intl.plural(n,
+        locale: locale,
+        precision: precision,
+        one: '$nString dollar',
+        other: '$nString dollars');
+    expect(actual, expected);
+  });
+}