Skip to content

Commit

Permalink
intl: Make parsing two-digit years match the documented behavior
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Dart Team authored and jamesderlin committed May 28, 2020
1 parent 49a1f81 commit 9e48191
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 9 deletions.
10 changes: 10 additions & 0 deletions pkgs/intl/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions pkgs/intl/lib/intl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import 'dart:collection';
import 'dart:convert';
import 'dart:math';

import 'package:clock/clock.dart';

import 'date_symbols.dart';
import 'number_symbols.dart';
import 'number_symbols_data.dart';
Expand Down
5 changes: 3 additions & 2 deletions pkgs/intl/lib/src/intl/date_format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,13 @@ part of intl;
/// 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
Expand Down
7 changes: 6 additions & 1 deletion pkgs/intl/lib/src/intl/date_format_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ class _DateFormatPatternField extends _DateFormatField {
case 'v':
break; // time zone id
case 'y':
handleNumericField(input, builder.setYear);
parseYear(input, builder);
break;
case 'z':
break; // time zone
Expand Down Expand Up @@ -443,6 +443,11 @@ class _DateFormatPatternField extends _DateFormatField {
return longestResult;
}

void parseYear(_Stream input, _DateBuilder builder) {
handleNumericField(input, builder.setYear);
builder.setHasAmbiguousCentury(width == 2);
}

String formatMonth(DateTime date) {
switch (width) {
case 5:
Expand Down
72 changes: 67 additions & 5 deletions pkgs/intl/lib/src/intl/date_format_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ class _DateBuilder {
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;

Expand Down Expand Up @@ -85,6 +90,12 @@ class _DateBuilder {
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;
}
Expand Down Expand Up @@ -171,19 +182,70 @@ class _DateBuilder {
}
}

/// 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}) {
// TODO(alanknight): Validate the date, especially for things which
// 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;
Expand Down
3 changes: 2 additions & 1 deletion pkgs/intl/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: intl
version: 0.16.2-dev
version: 0.17.0-dev
author: Dart Team <[email protected]>
homepage: https://github.com/dart-lang/intl
description: >-
Expand All @@ -11,6 +11,7 @@ environment:
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'
Expand Down
23 changes: 23 additions & 0 deletions pkgs/intl/test/date_time_format_test_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -209,6 +210,28 @@ void runDateTests(SubsetFuncType subsetFunc) {
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);
Expand Down

0 comments on commit 9e48191

Please sign in to comment.