mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 15:17:07 +00:00
df97aca1fa
Closes https://github.com/dart-lang/sdk/pull/50764 GitOrigin-RevId: ee2fe9a75d50e877f4ad2fe3743acdbc04f186ef Change-Id: Ia73cd22da4e6ec95e84772aa4e1345ce2dbde215 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/276360 Reviewed-by: Erik Ernst <eernst@google.com> Commit-Queue: Erik Ernst <eernst@google.com>
190 lines
6.9 KiB
Dart
190 lines
6.9 KiB
Dart
// Copyright (c) 2021, 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.
|
|
|
|
import "package:expect/expect.dart";
|
|
|
|
// Tests that local DateTime constructor works correctly around
|
|
// time zone changes.
|
|
|
|
void main() {
|
|
// Find two points in time with different time zones.
|
|
// Search linearly back from 2020-01-01 in steps of 60 days.
|
|
// Stop if reaching 1970-01-01 (epoch) without finding anything.
|
|
var time = DateTime.utc(2020, 1, 1).millisecondsSinceEpoch;
|
|
var offset =
|
|
DateTime.fromMillisecondsSinceEpoch(time).timeZoneOffset.inMilliseconds;
|
|
var time2 = time;
|
|
var offset2 = offset;
|
|
// Whether the first change found moved the clock forward.
|
|
bool changeForward = false;
|
|
// 60 days.
|
|
const delta = 60 * Duration.millisecondsPerDay;
|
|
while (time2 > 0) {
|
|
time2 -= delta;
|
|
offset2 = DateTime.fromMillisecondsSinceEpoch(time2)
|
|
.timeZoneOffset
|
|
.inMilliseconds;
|
|
if (verbose) {
|
|
print("Search: ${tz(time2, offset2)} - ${tz(time, offset)}");
|
|
}
|
|
if (offset2 != offset) {
|
|
// Two different time zones found. Now find the precise (to the minute)
|
|
// time where a change happened, and test that.
|
|
test(findChange(time2, time));
|
|
// Remember if the change moved the clock forward or backward.
|
|
changeForward = offset2 < offset;
|
|
break;
|
|
}
|
|
}
|
|
time = time2;
|
|
// Find a change in the other direction.
|
|
// Keep iterating backwards to find another time zone
|
|
// where the change was in the other direction.
|
|
while (time > 0) {
|
|
time -= delta;
|
|
offset =
|
|
DateTime.fromMillisecondsSinceEpoch(time).timeZoneOffset.inMilliseconds;
|
|
if (verbose) {
|
|
print("Search: ${tz(time2, offset2)} - ${tz(time, offset)}");
|
|
}
|
|
if (offset != offset2) {
|
|
if ((offset < offset2) != changeForward) {
|
|
test(findChange(time, time2));
|
|
break;
|
|
} else {
|
|
// Another change in the same direction.
|
|
// Probably rare, but move use this time
|
|
// as end-point instead, so the binary search will be shorter.
|
|
time2 = time;
|
|
offset2 = offset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tests that a local time zone change is correctly represented
|
|
/// by local time [DateTime] objects created from date-time values.
|
|
void test(TimeZoneChange change) {
|
|
if (verbose) print("Test of $change");
|
|
// Sanity check. The time zones match the [change] one second
|
|
// before and after the change.
|
|
var before = DateTime.fromMillisecondsSinceEpoch(
|
|
change.msSinceEpoch - Duration.millisecondsPerSecond);
|
|
Expect.equals(change.msOffsetBefore, before.timeZoneOffset.inMilliseconds);
|
|
var after = DateTime.fromMillisecondsSinceEpoch(
|
|
change.msSinceEpoch + Duration.millisecondsPerSecond);
|
|
Expect.equals(change.msOffsetAfter, after.timeZoneOffset.inMilliseconds);
|
|
|
|
if (verbose) print("From MS : ${dtz(before)} --- ${dtz(after)}");
|
|
|
|
// Create local DateTime objects for the same YMDHMS as the
|
|
// values above. See that we pick the correct local time for them.
|
|
|
|
// One second before the change, even if clock moves backwards,
|
|
// we pick a value that is in the earlier time zone.
|
|
var localBefore = DateTime(before.year, before.month, before.day, before.hour,
|
|
before.minute, before.second);
|
|
Expect.equals(before, localBefore);
|
|
|
|
// Asking for a calendar date one second after the change.
|
|
var localAfter = DateTime(after.year, after.month, after.day, after.hour,
|
|
after.minute, after.second);
|
|
if (verbose) print("From YMDHMS: ${dtz(localBefore)} --- ${dtz(localAfter)}");
|
|
if (before.timeZoneOffset < after.timeZoneOffset) {
|
|
// Clock moved forwards.
|
|
// We're asking for a clock time which doesn't exist.
|
|
if (verbose) {
|
|
print("Forward: ${dtz(after)} vs ${dtz(localAfter)}");
|
|
}
|
|
Expect.equals(after, localAfter);
|
|
} else {
|
|
// Clock moved backwards.
|
|
// We're asking for a clock time which exists more than once.
|
|
// Should be in the former time zone.
|
|
Expect.equals(before.timeZoneOffset, localAfter.timeZoneOffset);
|
|
}
|
|
}
|
|
|
|
/// Finds a time zone change between [before] and [after].
|
|
///
|
|
/// The [before] time must be before [after],
|
|
/// and the local time zone at the two points must be different.
|
|
///
|
|
/// Finds the point in time, with one minute precision,
|
|
/// where the time zone changed, and returns this point,
|
|
/// as well as the time zone offset before and after the change.
|
|
TimeZoneChange findChange(int before, int after) {
|
|
var min = Duration.millisecondsPerMinute;
|
|
assert(before % min == 0);
|
|
assert(after % min == 0);
|
|
var offsetBefore =
|
|
DateTime.fromMillisecondsSinceEpoch(before).timeZoneOffset.inMilliseconds;
|
|
var offsetAfter =
|
|
DateTime.fromMillisecondsSinceEpoch(after).timeZoneOffset.inMilliseconds;
|
|
// Binary search for the precise (to 1 minute increments)
|
|
// time where the change happened.
|
|
while (after - before > min) {
|
|
var mid = before + (after - before) ~/ 2;
|
|
mid -= mid % min;
|
|
var offsetMid =
|
|
DateTime.fromMillisecondsSinceEpoch(mid).timeZoneOffset.inMilliseconds;
|
|
if (verbose) {
|
|
print(
|
|
"Bsearch: ${tz(before, offsetBefore)} - ${tz(mid, offsetMid)} - ${tz(after, offsetAfter)}");
|
|
}
|
|
if (offsetMid == offsetBefore) {
|
|
before = mid;
|
|
} else if (offsetMid == offsetAfter) {
|
|
after = mid;
|
|
} else {
|
|
// Third timezone in the middle. Probably rare.
|
|
// Use that as either before or after.
|
|
// Keep the direction of the time zone change.
|
|
var forwardChange = offsetAfter > offsetBefore;
|
|
if ((offsetMid > offsetBefore) == forwardChange) {
|
|
after = mid;
|
|
offsetAfter = offsetMid;
|
|
} else {
|
|
before = mid;
|
|
offsetBefore = offsetMid;
|
|
}
|
|
}
|
|
}
|
|
return TimeZoneChange(after, offsetBefore, offsetAfter);
|
|
}
|
|
|
|
/// A local time zone change.
|
|
class TimeZoneChange {
|
|
/// The point in time where the clocks were adjusted.
|
|
final int msSinceEpoch;
|
|
|
|
/// The time zone offset before the change.
|
|
final int msOffsetBefore;
|
|
|
|
/// The time zone offset since the change.
|
|
final int msOffsetAfter;
|
|
TimeZoneChange(this.msSinceEpoch, this.msOffsetBefore, this.msOffsetAfter);
|
|
String toString() {
|
|
var local = DateTime.fromMillisecondsSinceEpoch(msSinceEpoch);
|
|
var offsetBefore = Duration(milliseconds: msOffsetBefore);
|
|
var offsetAfter = Duration(milliseconds: msOffsetAfter);
|
|
return "$local (${ltz(offsetBefore)} -> ${ltz(offsetAfter)})";
|
|
}
|
|
}
|
|
|
|
// Helpers when printing timezones.
|
|
|
|
/// Point in time in ms since epoch, and known offset in ms.
|
|
String tz(int ms, int offset) => "${DateTime.fromMillisecondsSinceEpoch(ms)}"
|
|
"${ltz(Duration(milliseconds: offset))}";
|
|
|
|
/// Time plus Zone from DateTime
|
|
String dtz(DateTime dt) => "$dt${dt.isUtc ? "" : ltz(dt.timeZoneOffset)}";
|
|
|
|
/// Time zone from duration ("+h:ss" format).
|
|
String ltz(Duration d) => "${d.isNegative ? "-" : "+"}${d.inHours}"
|
|
":${(d.inMinutes % 60).toString().padLeft(2, "0")}";
|
|
|
|
/// Set to true if debugging.
|
|
const bool verbose = false;
|