intl: Make parsing two-digit years match the documented behavior

The `DateFormat` documentation claims that two-digit years will be
interpreted to be within 80 years before and 20 years after the
current date.  I don't know if that was ever true (that documentation
predates the creation of the GitHub repository).

While I'm tempted to just remove that false claim, there are multiple
GitHub issues about this not working, so some people want it.

* https://github.com/dart-lang/intl/issues/123
* https://github.com/dart-lang/intl/issues/275

PiperOrigin-RevId: 311079631
PiperOrigin-RevId: 311101503
PiperOrigin-RevId: 311412441
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5f8823..133fd7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.17.0
+ * **Breaking Change** [#123][]: Fix parsing of two-digit years to match the
+   documented behavior. Previously a two-digit year would be parsed to a value
+   in the range [0, 99]. Now it is parsed relative to the current date,
+   returning a value between 80 years in the past and 20 years in the future.
+ * Use package:clock to get the current date/time.
+ * Fix some more analysis complaints.
+
 ## 0.16.2
  * Fix bug with dates in January being treated as ordinal. e.g. 2020-01-32 would
    be accepted as valid and the day treated as day-of-year.
@@ -353,3 +361,5 @@
 * Handle two different messages with the same text.
 
 * Allow complex string literals in arguments (e.g. multi-line)
+
+[#123]: https://github.com/dart-lang/intl/issues/123
diff --git a/lib/intl.dart b/lib/intl.dart
index 7132c18..98f3899 100644
--- a/lib/intl.dart
+++ b/lib/intl.dart
@@ -23,6 +23,8 @@
 import 'dart:convert';
 import 'dart:math';
 
+import 'package:clock/clock.dart';
+
 import 'date_symbols.dart';
 import 'number_symbols.dart';
 import 'number_symbols_data.dart';
diff --git a/lib/src/intl/date_format.dart b/lib/src/intl/date_format.dart
index 1bab781..e2c9033 100644
--- a/lib/src/intl/date_format.dart
+++ b/lib/src/intl/date_format.dart
@@ -201,12 +201,13 @@
 /// DateFormat must interpret the abbreviated year relative to some
 /// century. It does this by adjusting dates to be within 80 years before and 20
 /// years after the time the parse function is called. For example, using a
-/// pattern of 'MM/dd/yy' and a DateParse instance created on Jan 1, 1997,
+/// pattern of 'MM/dd/yy' and a DateFormat instance created on Jan 1, 1997,
 /// the string '01/11/12' would be interpreted as Jan 11, 2012 while the string
 /// '05/04/64' would be interpreted as May 4, 1964. During parsing, only
 /// strings consisting of exactly two digits will be parsed into the default
 /// century. Any other numeric string, such as a one digit string, a three or
-/// more digit string will be interpreted as its face value.
+/// more digit string will be interpreted as its face value. Tests that parse
+/// two-digit years can control the current date with package:clock.
 ///
 /// If the year pattern does not have exactly two 'y' characters, the year is
 /// interpreted literally, regardless of the number of digits. So using the
diff --git a/lib/src/intl/date_format_field.dart b/lib/src/intl/date_format_field.dart
index 22e00f9..360998c 100644
--- a/lib/src/intl/date_format_field.dart
+++ b/lib/src/intl/date_format_field.dart
@@ -324,7 +324,7 @@
         case 'v':
           break; // time zone id
         case 'y':
-          handleNumericField(input, builder.setYear);
+          parseYear(input, builder);
           break;
         case 'z':
           break; // time zone
@@ -443,6 +443,11 @@
     return longestResult;
   }
 
+  void parseYear(_Stream input, _DateBuilder builder) {
+    handleNumericField(input, builder.setYear);
+    builder.setHasAmbiguousCentury(width == 2);
+  }
+
   String formatMonth(DateTime date) {
     switch (width) {
       case 5:
diff --git a/lib/src/intl/date_format_helpers.dart b/lib/src/intl/date_format_helpers.dart
index e9c0ec6..be42b84 100644
--- a/lib/src/intl/date_format_helpers.dart
+++ b/lib/src/intl/date_format_helpers.dart
@@ -45,6 +45,11 @@
   bool pm = false;
   bool utc = false;
 
+  /// Whether the century portion of [year] is ambiguous.
+  ///
+  /// Ignored if `year < 0` or `year >= 100`.
+  bool _hasAmbiguousCentury = false;
+
   /// The locale, kept for logging purposes when there's an error.
   final String _locale;
 
@@ -85,6 +90,12 @@
     year = x;
   }
 
+  /// Sets whether [year] should be treated as ambiguous because it lacks a
+  /// century.
+  void setHasAmbiguousCentury(bool isAmbiguous) {
+    _hasAmbiguousCentury = isAmbiguous;
+  }
+
   void setMonth(int x) {
     month = x;
   }
@@ -171,6 +182,22 @@
     }
   }
 
+  /// Offsets a [DateTime] by a specified number of years.
+  ///
+  /// All other fields of the [DateTime] normally will remain unaffected.  An
+  /// exception is if the resulting [DateTime] otherwise would represent an
+  /// invalid date (e.g. February 29 of a non-leap year).
+  DateTime _offsetYear(DateTime dateTime, int offsetYears) =>
+      _dateTimeConstructor(
+          dateTime.year + offsetYears,
+          dateTime.month,
+          dateTime.day,
+          dateTime.hour,
+          dateTime.minute,
+          dateTime.second,
+          dateTime.millisecond,
+          dateTime.isUtc);
+
   /// Return a date built using our values. If no date portion is set,
   /// use the 'Epoch' of January 1, 1970.
   DateTime asDate({int retries = 3}) {
@@ -178,12 +205,47 @@
     // can crash the VM, e.g. large month values.
     if (_date != null) return _date;
 
-    if (utc) {
-      _date = _dateTimeConstructor(year, month, dayOrDayOfYear, hour24, minute,
-          second, fractionalSecond, utc);
-    } else {
-      var preliminaryResult = _dateTimeConstructor(year, month, dayOrDayOfYear,
+    DateTime preliminaryResult;
+    final hasCentury = !_hasAmbiguousCentury || year < 0 || year >= 100;
+    if (hasCentury) {
+      preliminaryResult = _dateTimeConstructor(year, month, dayOrDayOfYear,
           hour24, minute, second, fractionalSecond, utc);
+    } else {
+      var now = clock.now();
+      if (utc) {
+        now = now.toUtc();
+      }
+
+      const lookBehindYears = 80;
+      var lowerDate = _offsetYear(now, -lookBehindYears);
+      var upperDate = _offsetYear(now, 100 - lookBehindYears);
+      var lowerCentury = (lowerDate.year ~/ 100) * 100;
+      var upperCentury = (upperDate.year ~/ 100) * 100;
+      preliminaryResult = _dateTimeConstructor(upperCentury + year, month,
+          dayOrDayOfYear, hour24, minute, second, fractionalSecond, utc);
+
+      // Our interval must be half-open since there otherwise could be ambiguity
+      // for a date that is exactly 20 years in the future or exactly 80 years
+      // in the past (mod 100).  We'll treat the lower-bound date as the
+      // exclusive bound because:
+      // * It's farther away from the present, and we're less likely to care
+      //   about it.
+      // * By the time this function exits, time will have advanced to favor
+      //   the upper-bound date.
+      //
+      // We don't actually need to check both bounds.
+      if (preliminaryResult.compareTo(upperDate) <= 0) {
+        // Within range.
+        assert(preliminaryResult.compareTo(lowerDate) > 0);
+      } else {
+        preliminaryResult = _dateTimeConstructor(lowerCentury + year, month,
+            dayOrDayOfYear, hour24, minute, second, fractionalSecond, utc);
+      }
+    }
+
+    if (utc && hasCentury) {
+      _date = preliminaryResult;
+    } else {
       _date = _correctForErrors(preliminaryResult, retries);
     }
     return _date;
diff --git a/pubspec.yaml b/pubspec.yaml
index 064d030..d989d61 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: intl
-version: 0.16.2-dev
+version: 0.17.0-dev
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/intl
 description: >-
@@ -11,6 +11,7 @@
   sdk: '>=2.5.0 <3.0.0'
 
 dependencies:
+  clock: ^1.0.1
   path: '>=0.9.0 <2.0.0'
 dev_dependencies:
   fixnum: '>=0.9.0 <0.11.0'
diff --git a/test/date_time_format_test_core.dart b/test/date_time_format_test_core.dart
index 83982c0..3c7a539 100644
--- a/test/date_time_format_test_core.dart
+++ b/test/date_time_format_test_core.dart
@@ -8,6 +8,7 @@
 
 library date_time_format_tests;
 
+import 'package:clock/clock.dart';
 import 'package:intl/intl.dart';
 import 'package:test/test.dart';
 import 'date_time_format_test_data.dart';
@@ -209,6 +210,28 @@
         orderedEquals(['hh', ':', 'mm', ':', 'ss']));
   });
 
+  test('Two-digit years', () {
+    withClock(Clock.fixed(DateTime(2000, 1, 1)), () {
+      var dateFormat = DateFormat('yy');
+      expect(dateFormat.parse('99'), DateTime(1999));
+      expect(dateFormat.parse('00'), DateTime(2000));
+      expect(dateFormat.parse('19'), DateTime(2019));
+      expect(dateFormat.parse('20'), DateTime(2020));
+      expect(dateFormat.parse('21'), DateTime(1921));
+
+      expect(dateFormat.parse('2000'), DateTime(2000));
+
+      dateFormat = DateFormat('MM-dd-yy');
+      expect(dateFormat.parse('12-31-19'), DateTime(2019, 12, 31));
+      expect(dateFormat.parse('1-1-20'), DateTime(2020, 1, 1));
+      expect(dateFormat.parse('1-2-20'), DateTime(1920, 1, 2));
+
+      expect(DateFormat('y').parse('99'), DateTime(99));
+      expect(DateFormat('yyy').parse('99'), DateTime(99));
+      expect(DateFormat('yyyy').parse('99'), DateTime(99));
+    });
+  });
+
   test('Test ALL the supported formats on representative locales', () {
     var aDate = DateTime(2012, 1, 27, 20, 58, 59, 0);
     testLocale('en_US', english, aDate);