blob: bfa717b6e33b91476563d1de039927d6733d7ea5 [file] [log] [blame]
// 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));
// Remeber 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;