Automated g4 rollback of changelist 173286563.
*** Reason for rollback ***
Causing regressions. See eye3 link in comments, and also https://buganizer.corp.google.com/issues/68277865
*** Original change description ***
More robust handling of DateTime parsing with daylight savings-type transitions, particularly when they occur at midnight (e.g. Brazil)
***
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=173558448
diff --git a/lib/src/intl/date_format.dart b/lib/src/intl/date_format.dart
index a1fc6fb..e9def84 100644
--- a/lib/src/intl/date_format.dart
+++ b/lib/src/intl/date_format.dart
@@ -324,7 +324,6 @@
// values with no delimiters, which we currently don't do. Should we?
var dateFields = new _DateBuilder();
if (utc) dateFields.utc = true;
- dateFields._dateOnly = this.dateOnly;
var stream = new _Stream(inputString);
_formatFields.forEach((f) => f.parse(stream, dateFields));
if (strict && !stream.atEnd()) {
@@ -335,13 +334,6 @@
return dateFields.asDate();
}
- /// Does our format only only date fields, and no time fields.
- ///
- /// For example, 'yyyy-MM-dd' would be true, but 'dd hh:mm' would be false.
- bool get dateOnly => _dateOnly ??= _checkDateOnly;
- bool _dateOnly;
- bool get _checkDateOnly => _formatFields.every((each) => each.forDate);
-
/// Given user input, attempt to parse the [inputString] into the anticipated
/// format, treating it as being in UTC.
///
diff --git a/lib/src/intl/date_format_field.dart b/lib/src/intl/date_format_field.dart
index 8a5d25e..495f8f9 100644
--- a/lib/src/intl/date_format_field.dart
+++ b/lib/src/intl/date_format_field.dart
@@ -22,10 +22,6 @@
_trimmedPattern = pattern.trim();
}
- /// Does this field potentially represent part of a Date, i.e. is not
- /// time-specific.
- bool get forDate => true;
-
/// Return the width of [pattern]. Different widths represent different
/// formatting options. See the comment for DateFormat for details.
int get width => pattern.length;
@@ -257,17 +253,6 @@
new _LoosePatternField(pattern, parent).parse(input, dateFields);
}
- bool _forDate;
-
- /// Is this field involved in computing the date portion, as opposed to the
- /// time.
- ///
- /// The [pattern] will contain one or more of a particular format character,
- /// e.g. "yyyy" for a four-digit year. This hard-codes all the pattern
- /// characters that pertain to dates. The remaining characters, 'ahHkKms' are
- /// all time-related. See e.g. [formatField]
- bool get forDate => _forDate ??= 'cdDEGLMQvyZz'.contains(pattern[0]);
-
/// Parse a field representing part of a date pattern. Note that we do not
/// return a value, but rather build up the result in [builder].
void parseField(_Stream input, _DateBuilder builder) {
@@ -593,8 +578,27 @@
return padTo(width, date.day);
}
- String formatDayOfYear(DateTime date) =>
- padTo(width, _dayOfYear(date.month, date.day, _isLeapYear(date)));
+ String formatDayOfYear(DateTime date) => padTo(width, dayNumberInYear(date));
+
+ /// Return the ordinal day, i.e. the day number in the year.
+ int dayNumberInYear(DateTime date) {
+ if (date.month == 1) return date.day;
+ if (date.month == 2) return date.day + 31;
+ return ordinalDayFromMarchFirst(date) + 59 + (isLeapYear(date) ? 1 : 0);
+ }
+
+ /// Return the day of the year counting March 1st as 1, after which the
+ /// number of days per month is constant, so it's easier to calculate.
+ /// Formula from http://en.wikipedia.org/wiki/Ordinal_date
+ int ordinalDayFromMarchFirst(DateTime date) =>
+ ((30.6 * date.month) - 91.4).floor() + date.day;
+
+ /// Return true if this is a leap year. Rely on [DateTime] to do the
+ /// underlying calculation, even though it doesn't expose the test to us.
+ bool isLeapYear(DateTime date) {
+ var feb29 = new DateTime(date.year, 2, 29);
+ return feb29.month == 2;
+ }
String formatDayOfWeek(DateTime date) {
// Note that Dart's weekday returns 1 for Monday and 7 for Sunday.
diff --git a/lib/src/intl/date_format_helpers.dart b/lib/src/intl/date_format_helpers.dart
index 171b549..3ad651d 100644
--- a/lib/src/intl/date_format_helpers.dart
+++ b/lib/src/intl/date_format_helpers.dart
@@ -4,31 +4,6 @@
part of intl;
-/// Given a month and day number, return the day of the year, all one-based.
-///
-/// For example,
-/// * January 2nd (1, 2) -> 2.
-/// * February 5th (2, 5) -> 36.
-/// * March 1st of a non-leap year (3, 1) -> 60.
-int _dayOfYear(int month, int day, bool leapYear) {
- if (month == 1) return day;
- if (month == 2) return day + 31;
- return ordinalDayFromMarchFirst(month, day) + 59 + (leapYear ? 1 : 0);
-}
-
-/// Return true if this is a leap year. Rely on [DateTime] to do the
-/// underlying calculation, even though it doesn't expose the test to us.
-bool _isLeapYear(DateTime date) {
- var feb29 = new DateTime(date.year, 2, 29);
- return feb29.month == 2;
-}
-
-/// Return the day of the year counting March 1st as 1, after which the
-/// number of days per month is constant, so it's easier to calculate.
-/// Formula from http://en.wikipedia.org/wiki/Ordinal_date
-int ordinalDayFromMarchFirst(int month, int day) =>
- ((30.6 * month) - 91.4).floor() + day;
-
/// A class for holding onto the data for a date so that it can be built
/// up incrementally.
class _DateBuilder {
@@ -44,18 +19,6 @@
bool pm = false;
bool utc = false;
- /// Is this constructing a pure date.
- ///
- /// This is important because some locales change times at midnight,
- /// e.g. Brazil. So if we try to create a DateTime representing a date at
- /// midnight on the day of transition it will jump forward or back 1 hour. If
- /// it jumps forward that's mostly harmless if we only care about the
- /// date. But if it jumps backwards that will change the date, which is
- /// bad. Compensate by adjusting the time portion forward. But only do that
- /// when we're explicitly trying to construct a date, which we can tell from
- /// the format.
- bool _dateOnly = false;
-
// Functions that exist just to be closurized so we can pass them to a general
// method.
void setYear(x) {
@@ -104,16 +67,7 @@
// which will catch cases like "14:00:00 PM".
var date = asDate();
_verify(hour24, date.hour, date.hour, "hour", s, date);
- if (day > 31) {
- // We have an ordinal date, compute the corresponding date for the result
- // and compare to that.
- var leapYear = _isLeapYear(date);
- var correspondingDay = _dayOfYear(date.month, date.day, leapYear);
- _verify(day, correspondingDay, correspondingDay, "day", s, date);
- } else {
- // We have the day of the month, compare directly.
- _verify(day, date.day, date.day, "day", s, date);
- }
+ _verify(day, date.day, date.day, "day", s, date);
_verify(year, date.year, date.year, "year", s, date);
}
@@ -129,62 +83,23 @@
/// 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}) {
+ DateTime asDate({int retries: 10}) {
// TODO(alanknight): Validate the date, especially for things which
// can crash the VM, e.g. large month values.
+ var result;
if (utc) {
- return new DateTime.utc(
+ result = new DateTime.utc(
year, month, day, hour24, minute, second, fractionalSecond);
} else {
- var preliminaryResult = new DateTime(
+ result = new DateTime(
year, month, day, hour24, minute, second, fractionalSecond);
- return _correctForErrors(preliminaryResult, retries);
- }
- }
-
- static final Duration _zeroDuration = new Duration();
-
- /// Given a local DateTime, check for errors and try to compensate for them if
- /// possible.
- DateTime _correctForErrors(DateTime result, int retries) {
- // There are 3 kinds of errors that we know of
- //
- // 1 - Issue 15560, sometimes we get UTC even when we asked for local, or
- // they get constructed as if in UTC and then have the offset
- // subtracted. Retry, possibly several times, until we get something that
- // looks valid, or we give up.
- //
- // 2 - Timezone transitions. If we ask for the time during a timezone
- // transition then it will offset it by that transition. This is
- // particularly a problem if the timezone transition happens at midnight,
- // and we're looking for a date with no time component. This happens in
- // Brazil, and we can end up with 11:00pm the previous day. Add time to
- // compensate.
- //
- // 3 - Invalid input which the constructor nevertheless accepts. Just
- // return what it created, and verify will catch it if we're in strict
- // mode.
- var leapYear = _isLeapYear(result);
- var correspondingDay = _dayOfYear(result.month, result.day, leapYear);
-
- var looksLikeUtc = result.timeZoneOffset == _zeroDuration;
- if (looksLikeUtc &&
- (result.hour != hour24 || result.day != correspondingDay)) {
- // This may be a UTC failure. Retry and if the result doesn't look
- // like it's in the UTC time zone, use that instead.
- var retry = asDate(retries: retries - 1);
- if (retry.timeZoneOffset != _zeroDuration) return retry;
- }
- if (_dateOnly && day != correspondingDay) {
- // If we're _dateOnly, then hours should be zero, but might have been
- // offset to e.g. 11:00pm the previous day. Add that time back in. We
- // only care about jumps backwards. If we were offset to e.g. 1:00am the
- // same day that's all right for a date. It gets the day correct, and we
- // have no way to even represent midnight on a day when it doesn't
- // happen.
- var adjusted = result.add(new Duration(hours: (24 - result.hour)));
- if (_dayOfYear(adjusted.month, adjusted.day, leapYear) == day)
- return adjusted;
+ // TODO(alanknight): Issue 15560 means non-UTC dates occasionally come out
+ // in UTC, or, alternatively, are constructed as if in UTC and then have
+ // the offset subtracted. If that happens, retry, several times if
+ // necessary.
+ if (retries > 0 && (result.hour != hour24 || result.day != day)) {
+ result = asDate(retries: retries - 1);
+ }
}
return result;
}
diff --git a/test/brazil_timezone_test.dart b/test/brazil_timezone_test.dart
deleted file mode 100644
index 7c4f0ab..0000000
--- a/test/brazil_timezone_test.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-/// Test date formatting and parsing while the system time zone is set to
-/// America/Sao Paulo.
-///
-/// In Brazil the time change spring/fall happens at midnight. This can make
-/// operations working with dates as midnight on a particular day fail. For
-/// example, in the (Brazilian) autumn, a date might "fall back" an hour and be
-/// on the previous day. This test verifies that we're handling those situations.
-import 'package:test/test.dart';
-import 'dart:io';
-import 'dart:convert';
-
-// This test relies on setting the TZ environment variable to affect the
-// system's time zone calculations. That's only effective on Linux environments,
-// and would only work in a browser if we were able to set it before the browser
-// launched, which we aren't. So restrict this test to the VM and Linux.
-@TestOn('vm')
-@TestOn('linux')
-
-/// The VM arguments we were given, most importantly package-root.
-final vmArgs = Platform.executableArguments;
-
-final dart = Platform.executable;
-
-main() {
- // The VM can be invoked with a "-DPACKAGE_DIR=<directory>" argument to
- // indicate the root of the Intl package. If it is not provided, we assume
- // that the root of the Intl package is the current directory.
- var packageDir = new String.fromEnvironment('PACKAGE_DIR');
- var packageRelative = 'test/date_time_format_local_even_test.dart';
- var fileToSpawn =
- packageDir == null ? packageRelative : '$packageDir/$packageRelative';
-
- test("Run tests in Sao Paulo time zone", () async {
- List<String> args = []
- ..addAll(vmArgs)
- ..add(fileToSpawn);
- var result = await Process.run(dart, args,
- stdoutEncoding: UTF8,
- stderrEncoding: UTF8,
- includeParentEnvironment: true,
- environment: {'TZ': 'America/Sao_Paulo'});
- // Because the actual tests are run in a spawned parocess their output isn't
- // directly visible here. To debug, it's necessary to look at the output of
- // that test, so we print it here for convenience.
- print(
- "Spawning test to run in the America/Sao Paulo time zone. Stderr is:");
- print(result.stderr);
- print("Spawned test in America/Sao Paulo time zone has Stdout:");
- print(result.stdout);
- expect(result.exitCode, 0,
- reason: "Spawned test failed. See the test log from stderr to debug");
- });
-}
diff --git a/test/date_time_format_test_core.dart b/test/date_time_format_test_core.dart
index e6f88f5..c1a4dc0 100644
--- a/test/date_time_format_test_core.dart
+++ b/test/date_time_format_test_core.dart
@@ -160,7 +160,7 @@
var format = new DateFormat(skeleton, localeName);
if (forceAscii) format.useNativeDigits = false;
var actualResult = format.format(date);
- var parsed = format.parseStrict(actualResult);
+ var parsed = format.parse(actualResult);
var thenPrintAgain = format.format(parsed);
expect(thenPrintAgain, equals(actualResult));
}
@@ -399,11 +399,7 @@
Map<int, DateTime> generateDates(int year, int leapDay) =>
new Iterable.generate(365 + leapDay, (n) => n + 1)
.map((day) {
- // Typically a "date" would have a time value of zero, but we
- // give them an hour value, because they can get created with an
- // offset to the previous day in time zones where the daylight
- // savings transition happens at midnight (e.g. Brazil).
- var result = new DateTime(year, 1, day, 3);
+ var result = new DateTime(year, 1, day);
// TODO(alanknight): This is a workaround for dartbug.com/15560.
if (result.toUtc() == result) result = new DateTime(year, 1, day);
return result;
@@ -419,12 +415,7 @@
expect(formatted, (number + 1).toString());
var formattedWithYear = withYear.format(date);
var parsed = withYear.parse(formattedWithYear);
- // Only compare the date portion, because time zone changes (e.g. DST) can
- // cause the hour values to be different.
- expect(parsed.year, date.year);
- expect(parsed.month, date.month);
- expect(parsed.day, date.day,
- reason: 'Mismatch between parsed ($parsed) and original ($date)');
+ expect(parsed, date);
});
}