dart-sdk/tests/corelib/local_date_time_test.dart
Lasse R.H. Nielsen 6af4987dc6 Change VM DateTime to-local-time computation to not fail for <1h changes.
The DateTime conversion to local time amounts to solving an
equation, finding an offset where the current local time is that offset.

The current approach doesn't actually do that, it just tries to simulate
the JS approach, with a tweak to avoid DST change issues, but that tweak
assumes that all time zone adjustments are one hour changes.

This CL should work correctly for time zone changes of up to one hour,
but might still fail for changes of more than one hour.
It works by trying more offsets, which obviously has a performance cost,
which depends on how efficient the operation system is at providing
the local time zone for a specific milliseconds-since-epoch.

Change-Id: I80dc6e62e0639d9966d3c5a06430787d8acc4ff1
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182041
Commit-Queue: Lasse R.H. Nielsen <lrn@google.com>
Reviewed-by: Nate Bosch <nbosch@google.com>
2021-04-08 16:51:00 +00:00

191 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));
// 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;