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