From fb057ea4e07de2af624688455925c0ca07afe04f Mon Sep 17 00:00:00 2001 From: Stephen Adams Date: Wed, 22 May 2024 05:18:52 +0000 Subject: [PATCH] [js_runtime, js_dev_runtime] Implement `microsecond` field of `DataTime` - Move DateTime implementation for dart2js and DDC into a shared place to reduce duplication. - Add a _microsecond field to the web DateTime to track microseconds outside of the JavaScript Date. - The cute dart2js optimization whereby `DateTime.now().millisecondsSinceEpoch` is compiled to `Date.now()` still works. - Both implementations report better errors. - Fixed VM bug with in-range sentinel. Change-Id: I9156255bdb6ecc195500ae9bc88f91fb315b6297 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/366963 Reviewed-by: Alexander Aprelev Reviewed-by: Martin Kustermann Reviewed-by: Lasse Nielsen Commit-Queue: Stephen Adams --- CHANGELOG.md | 14 ++ .../deferred_loading/data/lazy_types/lib.dart | 2 +- .../js_dev_runtime/patch/core_patch.dart | 141 ------------- .../js_dev_runtime/private/js_helper.dart | 27 +-- .../_internal/js_runtime/lib/core_patch.dart | 145 ------------- .../_internal/js_runtime/lib/js_helper.dart | 44 ++-- .../js_shared/lib/date_time_patch.dart | 195 ++++++++++++++++++ .../_internal/vm_shared/lib/date_patch.dart | 143 +++++++------ sdk/lib/core/date_time.dart | 80 +++---- sdk/lib/libraries.json | 10 +- sdk/lib/libraries.yaml | 8 +- tests/corelib/date_time_extremes_test.dart | 131 +++++++----- 12 files changed, 466 insertions(+), 474 deletions(-) create mode 100644 sdk/lib/_internal/js_shared/lib/date_time_patch.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ba51e36a7..cdc5cfc4632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,20 @@ [#55418]: https://github.com/dart-lang/sdk/issues/55418 [#55436]: https://github.com/dart-lang/sdk/issues/55436 +### Libraries + +#### `dart:core` + +- `DateTime` on the web platform now stores microseconds. Fixes [#44876][]. + The web imlementation is now practically compatible with the native + implementation. Small discrepancies due to rounding of web integers may still + occur for (1) `microsecondsSinceEpoch` outside the safe range, corresponding + to dates with a year outside of 1685..2255, and (2) arithmetic (`add`, + `subtract`, `difference`) where the `Duration` argument or result exceeds 570 + years. + +[#44876]: https://github.com/dart-lang/sdk/issues/44876 + ### Tools #### Linter diff --git a/pkg/compiler/test/deferred_loading/data/lazy_types/lib.dart b/pkg/compiler/test/deferred_loading/data/lazy_types/lib.dart index 751b18c5b0e..f028c43522e 100644 --- a/pkg/compiler/test/deferred_loading/data/lazy_types/lib.dart +++ b/pkg/compiler/test/deferred_loading/data/lazy_types/lib.dart @@ -11,7 +11,7 @@ class Foo { int? x; /*member: Foo.:member_unit=4{libB}*/ Foo() { - x = DateTime.now().millisecond; + x = DateTime.now().millisecondsSinceEpoch; } /*member: Foo.method:member_unit=4{libB}*/ @pragma('dart2js:noInline') diff --git a/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart index 7c391788d81..65cc146b9a5 100644 --- a/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart +++ b/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart @@ -267,147 +267,6 @@ class Error { } } -// Patch for DateTime implementation. -@patch -class DateTime { - @patch - DateTime.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, - {bool isUtc = false}) - : this._withValue(millisecondsSinceEpoch, isUtc: isUtc); - - @patch - DateTime.fromMicrosecondsSinceEpoch(int microsecondsSinceEpoch, - {bool isUtc = false}) - : this._withValue( - _microsecondInRoundedMilliseconds(microsecondsSinceEpoch), - isUtc: isUtc); - - @patch - DateTime._internal(int year, int month, int day, int hour, int minute, - int second, int millisecond, int microsecond, bool isUtc) - : isUtc = isUtc, - _value = checkInt(Primitives.valueFromDecomposedDate( - year, - month, - day, - hour, - minute, - second, - millisecond + _microsecondInRoundedMilliseconds(microsecond), - isUtc)); - - @patch - DateTime._now() - : isUtc = false, - _value = Primitives.dateNow(); - - @patch - DateTime._nowUtc() - : isUtc = true, - _value = Primitives.dateNow(); - - /// Rounds the given [microsecond] to the nearest milliseconds value. - /// - /// For example, invoked with argument `2600` returns `3`. - static int _microsecondInRoundedMilliseconds(int microsecond) { - return (microsecond / 1000).round(); - } - - @patch - static int? _brokenDownDateToValue(int year, int month, int day, int hour, - int minute, int second, int millisecond, int microsecond, bool isUtc) { - return Primitives.valueFromDecomposedDate( - year, - month, - day, - hour, - minute, - second, - millisecond + _microsecondInRoundedMilliseconds(microsecond), - isUtc); - } - - @patch - String get timeZoneName { - if (isUtc) return "UTC"; - return Primitives.getTimeZoneName(this); - } - - @patch - Duration get timeZoneOffset { - if (isUtc) return Duration.zero; - return Duration(minutes: Primitives.getTimeZoneOffsetInMinutes(this)); - } - - @patch - DateTime add(Duration duration) { - return DateTime._withValue(_value + duration.inMilliseconds, isUtc: isUtc); - } - - @patch - DateTime subtract(Duration duration) { - return DateTime._withValue(_value - duration.inMilliseconds, isUtc: isUtc); - } - - @patch - Duration difference(DateTime other) { - return Duration(milliseconds: _value - other.millisecondsSinceEpoch); - } - - @patch - int get millisecondsSinceEpoch => _value; - - @patch - int get microsecondsSinceEpoch => _value * 1000; - - @patch - int get year => Primitives.getYear(this); - - @patch - int get month => Primitives.getMonth(this); - - @patch - int get day => Primitives.getDay(this); - - @patch - int get hour => Primitives.getHours(this); - - @patch - int get minute => Primitives.getMinutes(this); - - @patch - int get second => Primitives.getSeconds(this); - - @patch - int get millisecond => Primitives.getMilliseconds(this); - - @patch - int get microsecond => 0; - - @patch - int get weekday => Primitives.getWeekday(this); - - @patch - bool operator ==(Object other) => - other is DateTime && - _value == other.millisecondsSinceEpoch && - isUtc == other.isUtc; - - @patch - bool isBefore(DateTime other) => _value < other.millisecondsSinceEpoch; - - @patch - bool isAfter(DateTime other) => _value > other.millisecondsSinceEpoch; - - @patch - bool isAtSameMomentAs(DateTime other) => - _value == other.millisecondsSinceEpoch; - - @patch - int compareTo(DateTime other) => - _value.compareTo(other.millisecondsSinceEpoch); -} - // Patch for Stopwatch implementation. @patch class Stopwatch { diff --git a/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart b/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart index 2c1c1a0489c..6e7c3634b8f 100644 --- a/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart +++ b/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart @@ -392,6 +392,7 @@ class Primitives { @nullCheck int minutes, @nullCheck int seconds, @nullCheck int milliseconds, + @nullCheck int microseconds, @nullCheck bool isUtc) { final int MAX_MILLISECONDS_SINCE_EPOCH = 8640000000000000; var jsMonth = month - 1; @@ -403,6 +404,11 @@ class Primitives { years += 400; jsMonth -= 400 * 12; } + // JavaScript `Date` does not handle microseconds, so ensure the provided + // microseconds is in range [0..999]. + final remainder = microseconds % 1000; + milliseconds += (microseconds - remainder) ~/ 1000; + microseconds = remainder; int value; if (isUtc) { value = JS('!', r'Date.UTC(#, #, #, #, #, #, #)', years, jsMonth, @@ -413,23 +419,13 @@ class Primitives { } if (value.isNaN || value < -MAX_MILLISECONDS_SINCE_EPOCH || - value > MAX_MILLISECONDS_SINCE_EPOCH) { + value > MAX_MILLISECONDS_SINCE_EPOCH || + value == MAX_MILLISECONDS_SINCE_EPOCH && microseconds != 0) { return null; } - if (years <= 0 || years < 100) return patchUpY2K(value, years, isUtc); return value; } - static int patchUpY2K(value, years, isUtc) { - var date = JS('!', r'new Date(#)', value); - if (isUtc) { - JS('!', r'#.setUTCFullYear(#)', date, years); - } else { - JS('!', r'#.setFullYear(#)', date, years); - } - return JS('!', r'#.valueOf()', date); - } - // Lazily keep a JS Date stored in the JS object. static lazyAsJsDate(DateTime receiver) { if (JS('!', r'#.date === (void 0)', receiver)) { @@ -493,13 +489,6 @@ class Primitives { return (weekday + 6) % 7 + 1; } - static num valueFromDateString(str) { - if (str is! String) throw argumentErrorValue(str); - num value = JS('!', r'Date.parse(#)', str); - if (value.isNaN) throw argumentErrorValue(str); - return value; - } - static Object? getProperty(Object? object, Object key) { if (object == null || object is bool || object is num || object is String) { throw argumentErrorValue(object); diff --git a/sdk/lib/_internal/js_runtime/lib/core_patch.dart b/sdk/lib/_internal/js_runtime/lib/core_patch.dart index db471fb225e..d7aa5f82bbd 100644 --- a/sdk/lib/_internal/js_runtime/lib/core_patch.dart +++ b/sdk/lib/_internal/js_runtime/lib/core_patch.dart @@ -281,151 +281,6 @@ class Error { } } -// Patch for DateTime implementation. -@patch -class DateTime { - @patch - DateTime.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, - {bool isUtc = false}) - // `0 + millisecondsSinceEpoch` forces the inferred result to be non-null. - : this._withValue(0 + millisecondsSinceEpoch, isUtc: isUtc); - - @patch - DateTime.fromMicrosecondsSinceEpoch(int microsecondsSinceEpoch, - {bool isUtc = false}) - : this._withValue( - _microsecondInRoundedMilliseconds(microsecondsSinceEpoch), - isUtc: isUtc); - - @patch - DateTime._internal(int year, int month, int day, int hour, int minute, - int second, int millisecond, int microsecond, bool isUtc) - // checkBool is manually inlined here because dart2js doesn't inline it - // and [isUtc] is usually a constant. - : this.isUtc = - isUtc is bool ? isUtc : throw ArgumentError.value(isUtc, 'isUtc'), - _value = checkInt(Primitives.valueFromDecomposedDate( - year, - month, - day, - hour, - minute, - second, - millisecond + _microsecondInRoundedMilliseconds(microsecond), - isUtc)); - - @patch - DateTime._now() - : isUtc = false, - _value = Primitives.dateNow(); - - @patch - DateTime._nowUtc() - : isUtc = true, - _value = Primitives.dateNow(); - - /// Rounds the given [microsecond] to the nearest milliseconds value. - /// - /// For example, invoked with argument `2600` returns `3`. - static int _microsecondInRoundedMilliseconds(int microsecond) { - return (microsecond / 1000).round(); - } - - @patch - static int? _brokenDownDateToValue(int year, int month, int day, int hour, - int minute, int second, int millisecond, int microsecond, bool isUtc) { - return Primitives.valueFromDecomposedDate( - year, - month, - day, - hour, - minute, - second, - millisecond + _microsecondInRoundedMilliseconds(microsecond), - isUtc); - } - - @patch - String get timeZoneName { - if (isUtc) return "UTC"; - return Primitives.getTimeZoneName(this); - } - - @patch - Duration get timeZoneOffset { - if (isUtc) return Duration(); - return Duration(minutes: Primitives.getTimeZoneOffsetInMinutes(this)); - } - - @patch - DateTime add(Duration duration) { - return DateTime._withValue(_value + duration.inMilliseconds, isUtc: isUtc); - } - - @patch - DateTime subtract(Duration duration) { - return DateTime._withValue(_value - duration.inMilliseconds, isUtc: isUtc); - } - - @patch - Duration difference(DateTime other) { - return Duration(milliseconds: _value - other.millisecondsSinceEpoch); - } - - @patch - int get millisecondsSinceEpoch => _value; - - @patch - int get microsecondsSinceEpoch => 1000 * _value; - - @patch - int get year => Primitives.getYear(this); - - @patch - int get month => Primitives.getMonth(this); - - @patch - int get day => Primitives.getDay(this); - - @patch - int get hour => Primitives.getHours(this); - - @patch - int get minute => Primitives.getMinutes(this); - - @patch - int get second => Primitives.getSeconds(this); - - @patch - int get millisecond => Primitives.getMilliseconds(this); - - @patch - int get microsecond => 0; - - @patch - int get weekday => Primitives.getWeekday(this); - - @patch - bool operator ==(Object other) => - other is DateTime && - _value == other.millisecondsSinceEpoch && - isUtc == other.isUtc; - - @patch - bool isBefore(DateTime other) => _value < other.millisecondsSinceEpoch; - - @patch - bool isAfter(DateTime other) => _value > other.millisecondsSinceEpoch; - - @patch - bool isAtSameMomentAs(DateTime other) => - _value == other.millisecondsSinceEpoch; - - @patch - int compareTo(DateTime other) => - _value.compareTo(other.millisecondsSinceEpoch); -} - // Patch for Stopwatch implementation. @patch class Stopwatch { diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart index 1a04a339594..2359738a289 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart @@ -682,8 +682,16 @@ class Primitives { as int; } - static int? valueFromDecomposedDate(int years, int month, int day, int hours, - int minutes, int seconds, int milliseconds, bool isUtc) { + static int? valueFromDecomposedDate( + int years, + int month, + int day, + int hours, + int minutes, + int seconds, + int milliseconds, + int microseconds, + bool isUtc) { final int MAX_MILLISECONDS_SINCE_EPOCH = 8640000000000000; checkInt(years); checkInt(month); @@ -692,6 +700,7 @@ class Primitives { checkInt(minutes); checkInt(seconds); checkInt(milliseconds); + checkInt(microseconds); checkBool(isUtc); var jsMonth = month - 1; // The JavaScript Date constructor 'corrects' year NN to 19NN. Sidestep that @@ -702,6 +711,11 @@ class Primitives { years += 400; jsMonth -= 400 * 12; } + // JavaScript `Date` does not handle microseconds, so ensure the provided + // microseconds is in range [0..999]. + final remainder = microseconds % 1000; + milliseconds += (microseconds - remainder) ~/ 1000; + microseconds = remainder; num value; if (isUtc) { value = JS('num', r'Date.UTC(#, #, #, #, #, #, #)', years, jsMonth, day, @@ -712,7 +726,8 @@ class Primitives { } if (value.isNaN || value < -MAX_MILLISECONDS_SINCE_EPOCH || - value > MAX_MILLISECONDS_SINCE_EPOCH) { + value > MAX_MILLISECONDS_SINCE_EPOCH || + value == MAX_MILLISECONDS_SINCE_EPOCH && microseconds != 0) { return null; } return JS('int', '#', value); @@ -744,7 +759,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getYear(DateTime receiver) { + static int getYear(DateTime receiver) { return (receiver.isUtc) ? JS('int', r'(#.getUTCFullYear() + 0)', lazyAsJsDate(receiver)) : JS('int', r'(#.getFullYear() + 0)', lazyAsJsDate(receiver)); @@ -753,7 +768,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getMonth(DateTime receiver) { + static int getMonth(DateTime receiver) { return (receiver.isUtc) ? JS('JSUInt31', r'#.getUTCMonth() + 1', lazyAsJsDate(receiver)) : JS('JSUInt31', r'#.getMonth() + 1', lazyAsJsDate(receiver)); @@ -762,7 +777,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getDay(DateTime receiver) { + static int getDay(DateTime receiver) { return (receiver.isUtc) ? JS('JSUInt31', r'(#.getUTCDate() + 0)', lazyAsJsDate(receiver)) : JS('JSUInt31', r'(#.getDate() + 0)', lazyAsJsDate(receiver)); @@ -771,7 +786,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getHours(DateTime receiver) { + static int getHours(DateTime receiver) { return (receiver.isUtc) ? JS('JSUInt31', r'(#.getUTCHours() + 0)', lazyAsJsDate(receiver)) : JS('JSUInt31', r'(#.getHours() + 0)', lazyAsJsDate(receiver)); @@ -780,7 +795,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getMinutes(DateTime receiver) { + static int getMinutes(DateTime receiver) { return (receiver.isUtc) ? JS('JSUInt31', r'(#.getUTCMinutes() + 0)', lazyAsJsDate(receiver)) : JS('JSUInt31', r'(#.getMinutes() + 0)', lazyAsJsDate(receiver)); @@ -789,7 +804,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getSeconds(DateTime receiver) { + static int getSeconds(DateTime receiver) { return (receiver.isUtc) ? JS('JSUInt31', r'(#.getUTCSeconds() + 0)', lazyAsJsDate(receiver)) : JS('JSUInt31', r'(#.getSeconds() + 0)', lazyAsJsDate(receiver)); @@ -798,7 +813,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getMilliseconds(DateTime receiver) { + static int getMilliseconds(DateTime receiver) { return (receiver.isUtc) ? JS( 'JSUInt31', r'(#.getUTCMilliseconds() + 0)', lazyAsJsDate(receiver)) @@ -808,7 +823,7 @@ class Primitives { @pragma('dart2js:noSideEffects') @pragma('dart2js:noThrows') @pragma('dart2js:noInline') - static getWeekday(DateTime receiver) { + static int getWeekday(DateTime receiver) { int weekday = (receiver.isUtc) ? JS('int', r'#.getUTCDay() + 0', lazyAsJsDate(receiver)) : JS('int', r'#.getDay() + 0', lazyAsJsDate(receiver)); @@ -816,13 +831,6 @@ class Primitives { return (weekday + 6) % 7 + 1; } - static num valueFromDateString(str) { - if (str is! String) throw argumentErrorValue(str); - num value = JS('num', r'Date.parse(#)', str); - if (value.isNaN) throw argumentErrorValue(str); - return value; - } - static getProperty(object, key) { if (object == null || object is bool || object is num || object is String) { throw argumentErrorValue(object); diff --git a/sdk/lib/_internal/js_shared/lib/date_time_patch.dart b/sdk/lib/_internal/js_shared/lib/date_time_patch.dart new file mode 100644 index 00000000000..ecba7409489 --- /dev/null +++ b/sdk/lib/_internal/js_shared/lib/date_time_patch.dart @@ -0,0 +1,195 @@ +// Copyright (c) 2024, 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 'dart:_foreign_helper' show JS; +import 'dart:_internal' show patch; +import 'dart:_js_helper' show checkInt, Primitives; + +// Patch for DateTime implementation. +@patch +class DateTime { + /// The value component of this DateTime, equal to [millisecondsSinceEpoch]. + final int _value; + + /// The [microsecond] component of this DateTime, in the range [0...999]. + final int _microsecond; + + /// Constructor for pre-validated components. + DateTime._(this._value, this._microsecond, {required this.isUtc}); + + /// Constructs a new [DateTime] instance with the given value. + /// + /// If [isUtc] is false, then the date is in the local time zone. + DateTime._withValueChecked(int millisecondsSinceEpoch, int microsecond, + {required bool isUtc}) + : _value = _validate(millisecondsSinceEpoch, microsecond, isUtc), + _microsecond = microsecond, + this.isUtc = isUtc; + + @patch + DateTime.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, + {bool isUtc = false}) + : this._withValueChecked(millisecondsSinceEpoch, 0, isUtc: isUtc); + + @patch + DateTime.fromMicrosecondsSinceEpoch(int microsecondsSinceEpoch, + {bool isUtc = false}) + : this._withValueChecked( + (microsecondsSinceEpoch - microsecondsSinceEpoch % 1000) ~/ 1000, + microsecondsSinceEpoch % 1000, + isUtc: isUtc); + + @patch + DateTime._internal(int year, int month, int day, int hour, int minute, + int second, int millisecond, int microsecond, bool isUtc) + // checkBool is manually inlined here because dart2js doesn't inline it + // and [isUtc] is usually a constant. + : this.isUtc = + isUtc is bool ? isUtc : throw ArgumentError.value(isUtc, 'isUtc'), + _value = Primitives.valueFromDecomposedDate(year, month, day, hour, + minute, second, millisecond, microsecond, isUtc) ?? + _sentinel, + _microsecond = microsecond % 1000 { + if (_value == _sentinel) { + throw ArgumentError('($year, $month, $day,' + ' $hour, $minute, $second, $millisecond, $microsecond)'); + } + } + + static const _sentinel = _maxMillisecondsSinceEpoch * 10; + static const _sentinelConstraint = _sentinel < -_maxMillisecondsSinceEpoch || + _sentinel > _maxMillisecondsSinceEpoch; + static const _sentinelAssertion = 1 ~/ (_sentinelConstraint ? 1 : 0); + + @patch + DateTime._now() + : isUtc = false, + _value = Primitives.dateNow(), + _microsecond = 0; + + @patch + DateTime._nowUtc() + : isUtc = true, + _value = Primitives.dateNow(), + _microsecond = 0; + + @patch + DateTime _withUtc({required bool isUtc}) { + return DateTime._(_value, _microsecond, isUtc: isUtc); + } + + @patch + static DateTime? _finishParse(int year, int month, int day, int hour, + int minute, int second, int millisecond, int microsecond, bool isUtc) { + final value = Primitives.valueFromDecomposedDate(year, month, day, hour, + minute, second, millisecond, microsecond, isUtc); + if (value == null) return null; + return DateTime._withValueChecked(value, microsecond, isUtc: isUtc); + } + + @patch + String get timeZoneName { + if (isUtc) return "UTC"; + return Primitives.getTimeZoneName(this); + } + + @patch + Duration get timeZoneOffset { + if (isUtc) return Duration.zero; + return Duration(minutes: Primitives.getTimeZoneOffsetInMinutes(this)); + } + + @patch + DateTime add(Duration duration) => _addMicroseconds(duration.inMicroseconds); + + @patch + DateTime subtract(Duration duration) => + _addMicroseconds(0 - duration.inMicroseconds); + + DateTime _addMicroseconds(int durationMicroseconds) { + final durationLo = durationMicroseconds % 1000; + final durationHi = (durationMicroseconds - durationLo) ~/ 1000; + final sumLo = _microsecond + durationLo; + final microsecond = sumLo % 1000; + final carry = (sumLo - microsecond) ~/ 1000; + final milliseconds = _value + carry + durationHi; + return DateTime._withValueChecked(milliseconds, microsecond, isUtc: isUtc); + } + + @patch + Duration difference(DateTime other) { + final deltaMilliseconds = + millisecondsSinceEpoch - other.millisecondsSinceEpoch; + final deltaMicroseconds = microsecond - other.microsecond; + return Duration( + milliseconds: deltaMilliseconds, microseconds: deltaMicroseconds); + } + + @patch + int get millisecondsSinceEpoch => _value; + + @patch + int get microsecondsSinceEpoch => 1000 * _value + _microsecond; + + @patch + int get year => Primitives.getYear(this); + + @patch + int get month => Primitives.getMonth(this); + + @patch + int get day => Primitives.getDay(this); + + @patch + int get hour => Primitives.getHours(this); + + @patch + int get minute => Primitives.getMinutes(this); + + @patch + int get second => Primitives.getSeconds(this); + + @patch + int get millisecond => Primitives.getMilliseconds(this); + + @patch + int get microsecond => _microsecond; + + @patch + int get weekday => Primitives.getWeekday(this); + + @patch + bool operator ==(Object other) => + other is DateTime && + millisecondsSinceEpoch == other.millisecondsSinceEpoch && + microsecond == other.microsecond && + isUtc == other.isUtc; + + @patch + int get hashCode => Object.hash(_value, _microsecond); + + @patch + bool isBefore(DateTime other) => + millisecondsSinceEpoch < other.millisecondsSinceEpoch || + millisecondsSinceEpoch == other.millisecondsSinceEpoch && + microsecond < other.microsecond; + + @patch + bool isAfter(DateTime other) => + millisecondsSinceEpoch > other.millisecondsSinceEpoch || + millisecondsSinceEpoch == other.millisecondsSinceEpoch && + microsecond > other.microsecond; + + @patch + bool isAtSameMomentAs(DateTime other) => + millisecondsSinceEpoch == other.millisecondsSinceEpoch && + microsecond == other.microsecond; + + @patch + int compareTo(DateTime other) { + final r = millisecondsSinceEpoch.compareTo(other.millisecondsSinceEpoch); + if (r != 0) return r; + return microsecond.compareTo(other.microsecond); + } +} diff --git a/sdk/lib/_internal/vm_shared/lib/date_patch.dart b/sdk/lib/_internal/vm_shared/lib/date_patch.dart index b1fd03db8f3..1f0686724cc 100644 --- a/sdk/lib/_internal/vm_shared/lib/date_patch.dart +++ b/sdk/lib/_internal/vm_shared/lib/date_patch.dart @@ -29,8 +29,21 @@ class DateTime { static const _MONTH_INDEX = 7; static const _YEAR_INDEX = 8; + /// The value of this DateTime, equal to [microsecondsSinceEpoch]. + final int _value; + List? __parts; + /// Constructor for pre-validated components. + DateTime._(this._value, {required this.isUtc}); + + /// Constructs a new [DateTime] instance with the given value. + /// + /// If [isUtc] is false, then the date is in the local time zone. + DateTime._withValue(this._value, {required this.isUtc}) { + _validate(millisecondsSinceEpoch, microsecond, isUtc); + } + @patch DateTime.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, {bool isUtc = false}) @@ -44,7 +57,10 @@ class DateTime { {bool isUtc = false}) : this._withValue(microsecondsSinceEpoch, isUtc: isUtc); - static const _sentinelMs = -_maxMillisecondsSinceEpoch - 1; + static const _sentinel = -_maxMicrosecondsSinceEpoch - 1; + static const _sentinelConstraint = _sentinel < -_maxMicrosecondsSinceEpoch || + _sentinel > _maxMicrosecondsSinceEpoch; + static const _sentinelAssertion = 1 ~/ (_sentinelConstraint ? 1 : 0); @patch DateTime._internal(int year, int month, int day, int hour, int minute, @@ -52,8 +68,11 @@ class DateTime { : this.isUtc = checkNotNullable(isUtc, "isUtc"), this._value = _brokenDownDateToValue(year, month, day, hour, minute, second, millisecond, microsecond, isUtc) ?? - _sentinelMs { - if (_value == _sentinelMs) throw new ArgumentError(); + _sentinel { + if (_value == _sentinel) { + throw ArgumentError('($year, $month, $day,' + ' $hour, $minute, $second, $millisecond, $microsecond)'); + } } static int _validateMilliseconds(int millisecondsSinceEpoch) => @@ -73,6 +92,11 @@ class DateTime { : isUtc = true, _value = _getCurrentMicros(); + @patch + DateTime _withUtc({required bool isUtc}) { + return DateTime._(_value, isUtc: isUtc); + } + @patch String get timeZoneName { if (isUtc) return "UTC"; @@ -81,9 +105,9 @@ class DateTime { @patch Duration get timeZoneOffset { - if (isUtc) return new Duration(); + if (isUtc) return Duration(); int offsetInSeconds = _timeZoneOffsetInSeconds(microsecondsSinceEpoch); - return new Duration(seconds: offsetInSeconds); + return Duration(seconds: offsetInSeconds); } @patch @@ -92,6 +116,9 @@ class DateTime { _value == other.microsecondsSinceEpoch && isUtc == other.isUtc; + @patch + int get hashCode => (_value ^ (_value >> 30)) & 0x3FFFFFFF; + @patch bool isBefore(DateTime other) => _value < other.microsecondsSinceEpoch; @@ -106,11 +133,11 @@ class DateTime { int compareTo(DateTime other) => _value.compareTo(other.microsecondsSinceEpoch); - /** The first list contains the days until each month in non-leap years. The - * second list contains the days in leap years. */ - static const List> _DAYS_UNTIL_MONTH = const [ - const [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334], - const [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335] + /// The first list contains the days until each month in non-leap years. The + /// second list contains the days in leap years. + static const List> _DAYS_UNTIL_MONTH = [ + [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334], + [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335] ]; static List _computeUpperPart(int localMicros) { @@ -180,7 +207,7 @@ class DateTime { DateTime.daysPerWeek) + DateTime.monday; - List list = new List.filled(_YEAR_INDEX + 1, 0); + List list = List.filled(_YEAR_INDEX + 1, 0); list[_MICROSECOND_INDEX] = resultMicrosecond; list[_MILLISECOND_INDEX] = resultMillisecond; list[_SECOND_INDEX] = resultSecond; @@ -199,24 +226,22 @@ class DateTime { @patch DateTime add(Duration duration) { - return new DateTime._withValue(_value + duration.inMicroseconds, - isUtc: isUtc); + return DateTime._withValue(_value + duration.inMicroseconds, isUtc: isUtc); } @patch DateTime subtract(Duration duration) { - return new DateTime._withValue(_value - duration.inMicroseconds, - isUtc: isUtc); + return DateTime._withValue(_value - duration.inMicroseconds, isUtc: isUtc); } @patch Duration difference(DateTime other) { - return new Duration(microseconds: _value - other.microsecondsSinceEpoch); + return Duration(microseconds: _value - other.microsecondsSinceEpoch); } @patch int get millisecondsSinceEpoch => - _value ~/ Duration.microsecondsPerMillisecond; + _flooredDivision(_value, Duration.microsecondsPerMillisecond); @patch int get microsecondsSinceEpoch => _value; @@ -248,21 +273,19 @@ class DateTime { @patch int get year => _parts[_YEAR_INDEX]; - /** - * Returns the amount of microseconds in UTC that represent the same values - * as this [DateTime]. - * - * Say `t` is the result of this function, then - * * `this.year == new DateTime.fromMicrosecondsSinceEpoch(t, true).year`, - * * `this.month == new DateTime.fromMicrosecondsSinceEpoch(t, true).month`, - * * `this.day == new DateTime.fromMicrosecondsSinceEpoch(t, true).day`, - * * `this.hour == new DateTime.fromMicrosecondsSinceEpoch(t, true).hour`, - * * ... - * - * Daylight savings is computed as if the date was computed in [1970..2037]. - * If this [DateTime] lies outside this range then it is a year with similar - * properties (leap year, weekdays) is used instead. - */ + /// Returns the amount of microseconds in UTC that represent the same values + /// as this [DateTime]. + /// + /// Say `t` is the result of this function, then + /// * `this.year == new DateTime.fromMicrosecondsSinceEpoch(t, true).year`, + /// * `this.month == new DateTime.fromMicrosecondsSinceEpoch(t, true).month`, + /// * `this.day == new DateTime.fromMicrosecondsSinceEpoch(t, true).day`, + /// * `this.hour == new DateTime.fromMicrosecondsSinceEpoch(t, true).hour`, + /// * ... + /// + /// Daylight savings is computed as if the date was computed in [1970..2037]. + /// If this [DateTime] lies outside this range then a year with similar + /// properties (leap year, weekdays) is used instead. int get _localDateInUtcMicros { int micros = _value; if (isUtc) return micros; @@ -290,7 +313,6 @@ class DateTime { } /// Converts the given broken down date to microseconds. - @patch static int? _brokenDownDateToValue(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, bool isUtc) { // Simplify calculations by working with zero-based month. @@ -338,19 +360,26 @@ class DateTime { return microsecondsSinceEpoch; } + @patch + static DateTime? _finishParse(int year, int month, int day, int hour, + int minute, int second, int millisecond, int microsecond, bool isUtc) { + final value = _brokenDownDateToValue(year, month, day, hour, minute, second, + millisecond, microsecond, isUtc); + if (value == null) return null; + return DateTime._withValue(value, isUtc: isUtc); + } + static int _weekDay(y) { // 1/1/1970 was a Thursday. return (_dayFromYear(y) + 4) % 7; } - /** - * Returns a year in the range 2008-2035 matching - * * leap year, and - * * week day of first day. - * - * Leap seconds are ignored. - * Adapted from V8's date implementation. See ECMA 262 - 15.9.1.9. - */ + /// Returns a year in the range 2008-2035 matching + /// * leap year, and + /// * week day of first day. + /// + /// Leap seconds are ignored. + /// Adapted from V8's date implementation. See ECMA 262 - 15.9.1.9. static int _equivalentYear(int year) { // Returns year y so that _weekDay(y) == _weekDay(year). // _weekDay returns the week day (in range 0 - 6). @@ -370,12 +399,10 @@ class DateTime { return 2008 + (recentYear - 2008) % 28; } - /** - * Returns the UTC year for the corresponding [secondsSinceEpoch]. - * It is relatively fast for values in the range 0 to year 2098. - * - * Code is adapted from V8. - */ + /// Returns the UTC year for the corresponding [secondsSinceEpoch]. + /// It is relatively fast for values in the range 0 to year 2098. + /// + /// Code is adapted from V8. static int _yearsFromSecondsSinceEpoch(int secondsSinceEpoch) { const int DAYS_IN_4_YEARS = 4 * 365 + 1; const int DAYS_IN_100_YEARS = 25 * DAYS_IN_4_YEARS - 1; @@ -390,18 +417,16 @@ class DateTime { return _computeUpperPart(micros)[_YEAR_INDEX]; } - /** - * Returns a date in seconds that is equivalent to the given - * date in microseconds [microsecondsSinceEpoch]. An equivalent - * date has the same fields (`month`, `day`, etc.) as the given - * date, but the `year` is in the range [1901..2038]. - * - * * The time since the beginning of the year is the same. - * * If the given date is in a leap year then the returned - * seconds are in a leap year, too. - * * The week day of given date is the same as the one for the - * returned date. - */ + /// Returns a date in seconds that is equivalent to the given + /// date in microseconds [microsecondsSinceEpoch]. An equivalent + /// date has the same fields (`month`, `day`, etc.) as the given + /// date, but the `year` is in the range [1901..2038]. + /// + /// * The time since the beginning of the year is the same. + /// * If the given date is in a leap year then the returned + /// seconds are in a leap year, too. + /// * The week day of given date is the same as the one for the + /// returned date. static int _equivalentSeconds(int microsecondsSinceEpoch) { const int CUT_OFF_SECONDS = 0x7FFFFFFF; diff --git a/sdk/lib/core/date_time.dart b/sdk/lib/core/date_time.dart index 366995cf813..6c53f7f4e99 100644 --- a/sdk/lib/core/date_time.dart +++ b/sdk/lib/core/date_time.dart @@ -159,13 +159,6 @@ class DateTime implements Comparable { static const int december = 12; static const int monthsPerYear = 12; - /// The value of this DateTime. - /// - /// The content of this field is implementation dependent. On JavaScript it is - /// equal to [millisecondsSinceEpoch]. On the VM it is equal to - /// [microsecondsSinceEpoch]. - final int _value; - /// True if this [DateTime] is set to UTC time. /// /// ```dart @@ -348,12 +341,12 @@ class DateTime implements Comparable { minute -= sign * minuteDifference; } } - int? value = _brokenDownDateToValue(years, month, day, hour, minute, - second, millisecond, microsecond, isUtc); - if (value == null) { + DateTime? result = _finishParse(years, month, day, hour, minute, second, + millisecond, microsecond, isUtc); + if (result == null) { throw FormatException("Time out of range", formattedString); } - return DateTime._withValue(value, isUtc: isUtc); + return result; } else { throw FormatException("Invalid date format", formattedString); } @@ -373,6 +366,8 @@ class DateTime implements Comparable { } static const int _maxMillisecondsSinceEpoch = 8640000000000000; + static const int _maxMicrosecondsSinceEpoch = + _maxMillisecondsSinceEpoch * Duration.microsecondsPerMillisecond; /// Constructs a new [DateTime] instance /// with the given [millisecondsSinceEpoch]. @@ -406,18 +401,33 @@ class DateTime implements Comparable { external DateTime.fromMicrosecondsSinceEpoch(int microsecondsSinceEpoch, {bool isUtc = false}); - /// Constructs a new [DateTime] instance with the given value. + /// Throws an error if the millisecondsSinceEpoch and microsecond components + /// are out of range. /// - /// If [isUtc] is false, then the date is in the local time zone. - DateTime._withValue(this._value, {required this.isUtc}) { - if (millisecondsSinceEpoch.abs() > _maxMillisecondsSinceEpoch || - (millisecondsSinceEpoch.abs() == _maxMillisecondsSinceEpoch && - microsecond != 0)) { - throw ArgumentError( - "DateTime is outside valid range: $millisecondsSinceEpoch"); + /// Returns the millisecondsSinceEpoch component. + static int _validate( + int millisecondsSinceEpoch, int microsecond, bool isUtc) { + if (microsecond < 0 || microsecond > 999) { + throw RangeError.range(microsecond, 0, 999, "microsecond"); } + if (millisecondsSinceEpoch < -_maxMillisecondsSinceEpoch || + millisecondsSinceEpoch > _maxMillisecondsSinceEpoch) { + throw RangeError.range( + millisecondsSinceEpoch, + -_maxMillisecondsSinceEpoch, + _maxMillisecondsSinceEpoch, + "millisecondsSinceEpoch"); + } + if (millisecondsSinceEpoch == _maxMillisecondsSinceEpoch && + microsecond != 0) { + throw ArgumentError.value(microsecond, "microsecond", + "Time including microseconds is outside valid range"); + } + // For backwards compatibility with legacy mode. checkNotNullable(isUtc, "isUtc"); + + return millisecondsSinceEpoch; } /// Whether [other] is a [DateTime] at the same moment and in the @@ -436,6 +446,8 @@ class DateTime implements Comparable { /// independently of their zones. external bool operator ==(Object other); + external int get hashCode; + /// Whether this [DateTime] occurs before [other]. /// /// The comparison is independent @@ -516,8 +528,6 @@ class DateTime implements Comparable { /// ``` external int compareTo(DateTime other); - int get hashCode => (_value ^ (_value >> 30)) & 0x3FFFFFFF; - /// Returns this DateTime value in the local time zone. /// /// Returns this [DateTime] if it is already in the local time zone. @@ -529,7 +539,7 @@ class DateTime implements Comparable { /// ``` DateTime toLocal() { if (isUtc) { - return DateTime._withValue(_value, isUtc: false); + return _withUtc(isUtc: false); } return this; } @@ -545,9 +555,11 @@ class DateTime implements Comparable { /// ``` DateTime toUtc() { if (isUtc) return this; - return DateTime._withValue(_value, isUtc: true); + return _withUtc(isUtc: true); } + external DateTime _withUtc({required bool isUtc}); + static String _fourDigits(int n) { int absN = n.abs(); String sign = n < 0 ? "-" : ""; @@ -712,18 +724,10 @@ class DateTime implements Comparable { external DateTime._now(); - /// Returns the time as value (millisecond or microsecond since epoch), or - /// null if the values are out of range. - external static int? _brokenDownDateToValue( - int year, - int month, - int day, - int hour, - int minute, - int second, - int millisecond, - int microsecond, - bool isUtc); + /// Returns the [DateTime] corresponding to the given components, or `null` if + /// the values are out of range. + external static DateTime? _finishParse(int year, int month, int day, int hour, + int minute, int second, int millisecond, int microsecond, bool isUtc); /// The number of milliseconds since /// the "Unix epoch" 1970-01-01T00:00:00Z (UTC). @@ -744,8 +748,10 @@ class DateTime implements Comparable { /// 8,640,000,000,000,000,000us (100,000,000 days) from the Unix epoch. /// In other words: `microsecondsSinceEpoch.abs() <= 8640000000000000000`. /// - /// Note that this value does not fit into 53 bits (the size of a IEEE double). - /// A JavaScript number is not able to hold this value. + /// Note that this value does not always fit into 53 bits (the size of a IEEE + /// double). On the web JavaScript platforms, there may be a rounding error + /// for DateTime values sufficiently far from the epoch. The year range close + /// to the epoch to avoid rounding is approximately 1685..2254. external int get microsecondsSinceEpoch; /// The time zone name. diff --git a/sdk/lib/libraries.json b/sdk/lib/libraries.json index 56265a24143..a00925f6058 100644 --- a/sdk/lib/libraries.json +++ b/sdk/lib/libraries.json @@ -390,7 +390,10 @@ }, "core": { "uri": "core/core.dart", - "patches": "_internal/js_runtime/lib/core_patch.dart" + "patches": [ + "_internal/js_shared/lib/date_time_patch.dart", + "_internal/js_runtime/lib/core_patch.dart" + ] }, "developer": { "uri": "developer/developer.dart", @@ -570,7 +573,10 @@ }, "core": { "uri": "core/core.dart", - "patches": "_internal/js_dev_runtime/patch/core_patch.dart" + "patches": [ + "_internal/js_shared/lib/date_time_patch.dart", + "_internal/js_dev_runtime/patch/core_patch.dart" + ] }, "developer": { "uri": "developer/developer.dart", diff --git a/sdk/lib/libraries.yaml b/sdk/lib/libraries.yaml index 068e1b287f4..e843722cc1a 100644 --- a/sdk/lib/libraries.yaml +++ b/sdk/lib/libraries.yaml @@ -319,7 +319,9 @@ _dart2js_common: core: uri: "core/core.dart" - patches: "_internal/js_runtime/lib/core_patch.dart" + patches: + - "_internal/js_shared/lib/date_time_patch.dart" + - "_internal/js_runtime/lib/core_patch.dart" developer: uri: "developer/developer.dart" @@ -495,7 +497,9 @@ dartdevc: core: uri: "core/core.dart" - patches: "_internal/js_dev_runtime/patch/core_patch.dart" + patches: + - "_internal/js_shared/lib/date_time_patch.dart" + - "_internal/js_dev_runtime/patch/core_patch.dart" developer: uri: "developer/developer.dart" diff --git a/tests/corelib/date_time_extremes_test.dart b/tests/corelib/date_time_extremes_test.dart index 57facb12402..ad5316fc30b 100644 --- a/tests/corelib/date_time_extremes_test.dart +++ b/tests/corelib/date_time_extremes_test.dart @@ -7,14 +7,13 @@ import "package:expect/expect.dart"; // Dart test program for DateTime, extreme values. bool get supportsMicroseconds => - new DateTime.fromMicrosecondsSinceEpoch(1).microsecondsSinceEpoch == 1; + DateTime.fromMicrosecondsSinceEpoch(1).microsecondsSinceEpoch == 1; // Identical to _maxMillisecondsSinceEpoch in date_time.dart const int _MAX_MILLISECONDS = 8640000000000000; void testExtremes() { - var dt = - new DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS, isUtc: true); + var dt = DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS, isUtc: true); Expect.equals(275760, dt.year); Expect.equals(9, dt.month); Expect.equals(13, dt.day); @@ -23,7 +22,7 @@ void testExtremes() { Expect.equals(0, dt.second); Expect.equals(0, dt.millisecond); Expect.equals(0, dt.microsecond); - dt = new DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS, isUtc: true); + dt = DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS, isUtc: true); Expect.equals(-271821, dt.year); Expect.equals(4, dt.month); Expect.equals(20, dt.day); @@ -33,71 +32,103 @@ void testExtremes() { Expect.equals(0, dt.millisecond); Expect.equals(0, dt.microsecond); // Make sure that we can build the extreme dates in local too. - dt = new DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS); - dt = new DateTime( + dt = DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS); + dt = DateTime( dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.millisecond); Expect.equals(_MAX_MILLISECONDS, dt.millisecondsSinceEpoch); - dt = new DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS); - dt = new DateTime( + dt = DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS); + dt = DateTime( dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.millisecond); Expect.equals(-_MAX_MILLISECONDS, dt.millisecondsSinceEpoch); - Expect.throws(() => new DateTime.fromMillisecondsSinceEpoch( - _MAX_MILLISECONDS + 1, - isUtc: true)); - Expect.throws(() => new DateTime.fromMillisecondsSinceEpoch( - -_MAX_MILLISECONDS - 1, - isUtc: true)); - Expect.throws( - () => new DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS + 1)); - Expect.throws( - () => new DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS - 1)); - dt = new DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS); - Expect.throws( - () => new DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 1)); - dt = new DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS, isUtc: true); Expect.throws(() => - new DateTime.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 1)); - dt = new DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS); - Expect.throws( - () => new DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, -1)); - dt = new DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS, isUtc: true); + DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS + 1, isUtc: true)); Expect.throws(() => - new DateTime.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, -1)); + DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS - 1, isUtc: true)); + Expect.throws( + () => DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS + 1)); + Expect.throws( + () => DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS - 1)); + dt = DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS); + Expect.throws( + () => DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 1)); + dt = DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS, isUtc: true); + Expect.throws( + () => DateTime.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 1)); + dt = DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS); + Expect.throws( + () => DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, -1)); + dt = DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS, isUtc: true); + Expect.throws( + () => DateTime.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, -1)); if (!supportsMicroseconds) return; - dt = new DateTime.fromMicrosecondsSinceEpoch(_MAX_MILLISECONDS * 1000); - dt = new DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute); + /// The nearest value to [base] in the direction [delta]. For native `int`s, + /// this is just `base + delta`. For web `int`s outside the safe range, the + /// next value might differ by some power of two. + int nearest(int base, int delta) { + for (int factor = 1;; factor *= 2) { + final next = base + delta * factor; + print(factor); + if (next != base) return next; + } + } + + dt = DateTime.fromMicrosecondsSinceEpoch(_MAX_MILLISECONDS * 1000); + dt = DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second); Expect.equals(_MAX_MILLISECONDS * 1000, dt.microsecondsSinceEpoch); - dt = new DateTime.fromMicrosecondsSinceEpoch(-_MAX_MILLISECONDS * 1000); - dt = new DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute); + print(-_MAX_MILLISECONDS * 1000); + dt = DateTime.fromMicrosecondsSinceEpoch(-_MAX_MILLISECONDS * 1000); + dt = DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second); Expect.equals(-_MAX_MILLISECONDS * 1000, dt.microsecondsSinceEpoch); - Expect.throws(() => new DateTime.fromMicrosecondsSinceEpoch( - _MAX_MILLISECONDS * 1000 + 1, + Expect.throws(() => DateTime.fromMicrosecondsSinceEpoch( + nearest(_MAX_MILLISECONDS * 1000, 1), isUtc: true)); - Expect.throws(() => new DateTime.fromMicrosecondsSinceEpoch( - -_MAX_MILLISECONDS * 1000 - 1, + Expect.throws(() => DateTime.fromMicrosecondsSinceEpoch( + nearest(-_MAX_MILLISECONDS * 1000, -1), isUtc: true)); + Expect.throws(() => DateTime.fromMicrosecondsSinceEpoch( + nearest(_MAX_MILLISECONDS * 1000, 1))); + Expect.throws(() => DateTime.fromMicrosecondsSinceEpoch( + nearest(-_MAX_MILLISECONDS * 1000, -1))); + // These should all succeed - stepping into the valid range rather than out: + DateTime.fromMicrosecondsSinceEpoch(nearest(-_MAX_MILLISECONDS * 1000, 1), + isUtc: true); + DateTime.fromMicrosecondsSinceEpoch(nearest(_MAX_MILLISECONDS * 1000, -1), + isUtc: true); + DateTime.fromMicrosecondsSinceEpoch(nearest(-_MAX_MILLISECONDS * 1000, 1)); + DateTime.fromMicrosecondsSinceEpoch(nearest(_MAX_MILLISECONDS * 1000, -1)); + + dt = DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS); + Expect.throws( + () => DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, 1)); + Expect.throws(() => dt.copyWith(microsecond: 1)); + Expect.isTrue(dt.copyWith(microsecond: -1).toString().endsWith('.999999')); + + dt = DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS, isUtc: true); Expect.throws(() => - new DateTime.fromMicrosecondsSinceEpoch(_MAX_MILLISECONDS * 1000 + 1)); + DateTime.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, 1)); + Expect.throws(() => dt.copyWith(microsecond: 1)); + Expect.isTrue(dt.copyWith(microsecond: -1).toString().endsWith('.999999Z')); + + dt = DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS); + Expect.throws( + () => DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, -1)); + Expect.throws(() => dt.copyWith(microsecond: -1)); + Expect.isTrue(dt.copyWith(microsecond: 1).toString().endsWith('.000001')); + + dt = DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS, isUtc: true); Expect.throws(() => - new DateTime.fromMicrosecondsSinceEpoch(-_MAX_MILLISECONDS * 1000 - 1)); - dt = new DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS); - Expect.throws(() => - new DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, 1)); - dt = new DateTime.fromMillisecondsSinceEpoch(_MAX_MILLISECONDS, isUtc: true); - Expect.throws(() => - new DateTime.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, 1)); - dt = new DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS); - Expect.throws(() => - new DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, -1)); - dt = new DateTime.fromMillisecondsSinceEpoch(-_MAX_MILLISECONDS, isUtc: true); - Expect.throws(() => new DateTime.utc( - dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, -1)); + DateTime.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0, 0, -1)); + Expect.throws(() => dt.copyWith(microsecond: -1)); + Expect.isTrue(dt.copyWith(microsecond: 1).toString().endsWith('.000001Z')); // Regression test for https://dartbug.com/55438 dt = DateTime.utc(1969, 12, 31, 23, 59, 59, 999, 999); Expect.equals(-1, dt.microsecondsSinceEpoch); + // The first fix confused millisecondsSinceEpoch and microsecondsSinceEpoch. + dt = DateTime.utc(1696, 3, 16, 23, 59, 59, 999, 999); + Expect.equals(-_MAX_MILLISECONDS - 1, dt.microsecondsSinceEpoch); } void main() {