blob: 9b13de4d97328a00c0896ebb7cd8a5ae76a016eb [file] [edit]
// Copyright (c) 2014, the timezone 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 'env.dart';
import 'location.dart';
/// TimeZone aware DateTime.
class TZDateTime implements DateTime {
/// Maximum value for time instants.
static const int maxMillisecondsSinceEpoch = 8640000000000000;
/// Minimum value for time instants.
static const int minMillisecondsSinceEpoch = -maxMillisecondsSinceEpoch;
/// Returns the native [DateTime] object.
static DateTime _toNative(DateTime t) => t is TZDateTime ? t.native : t;
/// Converts a [_localDateTime] into a correct [DateTime].
static DateTime _utcFromLocalDateTime(DateTime local, Location location) {
// Adapted from https://github.com/JodaOrg/joda-time/blob/main/src/main/java/org/joda/time/DateTimeZone.java#L951
// Get the offset at local (first estimate).
final localInstant = local.millisecondsSinceEpoch;
final localTimezone = location.lookupTimeZone(localInstant);
final localOffset = localTimezone.timeZone.offset.inMilliseconds;
// Adjust localInstant using the estimate and recalculate the offset.
final adjustedInstant = localInstant - localOffset;
final adjustedTimezone = location.lookupTimeZone(adjustedInstant);
final adjustedOffset = adjustedTimezone.timeZone.offset.inMilliseconds;
var milliseconds = localInstant - adjustedOffset;
// If the offsets differ, we must be near a DST boundary
if (localOffset != adjustedOffset) {
// We need to ensure that time is always after the DST gap
// this happens naturally for positive offsets, but not for negative.
// If we just use adjustedOffset then the time is pushed back before the
// transition, whereas it should be on or after the transition
if (localOffset - adjustedOffset < 0 &&
adjustedOffset !=
location
.lookupTimeZone(localInstant - adjustedOffset)
.timeZone
.offset
.inMilliseconds) {
milliseconds = adjustedInstant;
}
}
// Ensure original microseconds are preserved regardless of TZ shift.
final microsecondsSinceEpoch = Duration(
milliseconds: milliseconds,
microseconds: local.microsecond,
).inMicroseconds;
return DateTime.fromMicrosecondsSinceEpoch(
microsecondsSinceEpoch,
isUtc: true,
);
}
/// Native [DateTime] used as a Calendar object.
///
/// Represents the same date and time as this [TZDateTime], but in the UTC
/// time zone. For example, for a [TZDateTime] representing
/// 2000-03-17T12:00:00-0700, this will store the [DateTime] representing
/// 2000-03-17T12:00:00Z.
final DateTime _localDateTime;
/// Native [DateTime] used as canonical, UTC representation.
///
/// Represents the same moment as this [TZDateTime].
final DateTime native;
/// The number of milliseconds since
/// the "Unix epoch" 1970-01-01T00:00:00Z (UTC).
///
/// This value is independent of the time zone.
///
/// This value is at most
/// 8,640,000,000,000,000ms (100,000,000 days) from the Unix epoch.
/// In other words: [:millisecondsSinceEpoch.abs() <= 8640000000000000:].
@override
int get millisecondsSinceEpoch => native.millisecondsSinceEpoch;
/// The number of microseconds since the "Unix epoch"
/// 1970-01-01T00:00:00Z (UTC).
///
/// This value is independent of the time zone.
///
/// This value is at most 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.
@override
int get microsecondsSinceEpoch => native.microsecondsSinceEpoch;
/// [Location]
final Location location;
/// [TimeZone]
final TimeZone timeZone;
/// True if this [TZDateTime] is set to UTC time.
///
/// ```dart
/// final dDay = TZDateTime.utc(1944, 6, 6);
/// assert(dDay.isUtc);
/// ```
///
@override
bool get isUtc => _isUtc(location);
static bool _isUtc(Location l) => identical(l, UTC);
/// True if this [TZDateTime] is set to Local time.
///
/// ```dart
/// final dDay = TZDateTime.local(1944, 6, 6);
/// assert(dDay.isLocal);
/// ```
///
bool get isLocal => identical(location, local);
/// Constructs a [TZDateTime] instance specified at [location] time zone.
///
/// For example,
/// to create a new TZDateTime object representing April 29, 2014, 6:04am
/// in America/Detroit:
///
/// ```dart
/// final detroit = getLocation('America/Detroit');
///
/// final annularEclipse = TZDateTime(location,
/// 2014, DateTime.APRIL, 29, 6, 4);
/// ```
TZDateTime(
Location location,
int year, [
int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0,
]) : this.from(
_utcFromLocalDateTime(
DateTime.utc(
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
),
location,
),
location,
);
/// Constructs a [TZDateTime] instance specified in the UTC time zone.
///
/// ```dart
/// final dDay = TZDateTime.utc(1944, TZDateTime.JUNE, 6);
/// ```
TZDateTime.utc(
int year, [
int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0,
]) : this(
UTC,
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
);
/// Constructs a [TZDateTime] instance specified in the local time zone.
///
/// ```dart
/// final dDay = TZDateTime.local(1944, TZDateTime.JUNE, 6);
/// ```
TZDateTime.local(
int year, [
int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0,
]) : this(
local,
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
);
/// Constructs a [TZDateTime] instance with current date and time in the
/// [location] time zone.
///
/// ```dart
/// final detroit = getLocation('America/Detroit');
///
/// final thisInstant = TZDateTime.now(detroit);
/// ```
TZDateTime.now(Location location) : this.from(DateTime.now(), location);
/// Constructs a new [TZDateTime] instance with the given
/// [millisecondsSinceEpoch].
///
/// The constructed [TZDateTime] represents
/// 1970-01-01T00:00:00Z + [millisecondsSinceEpoch] ms in the given
/// time zone [location].
TZDateTime.fromMillisecondsSinceEpoch(
Location location,
int millisecondsSinceEpoch,
) : this.from(
DateTime.fromMillisecondsSinceEpoch(
millisecondsSinceEpoch,
isUtc: true,
),
location,
);
TZDateTime.fromMicrosecondsSinceEpoch(
Location location,
int microsecondsSinceEpoch,
) : this.from(
DateTime.fromMicrosecondsSinceEpoch(
microsecondsSinceEpoch,
isUtc: true,
),
location,
);
/// Constructs a new [TZDateTime] instance from the given [DateTime]
/// in the specified [location].
///
/// ```dart
/// final laTime = TZDateTime(la, 2010, 1, 1);
/// final detroitTime = TZDateTime.from(laTime, detroit);
/// ```
TZDateTime.from(DateTime other, Location location)
: this._(
_toNative(other).toUtc(),
location,
_isUtc(location)
? TimeZone.UTC
: location.timeZone(other.millisecondsSinceEpoch),
);
TZDateTime._(this.native, this.location, this.timeZone)
: _localDateTime = _isUtc(location) ? native : native.add(timeZone.offset);
/// Constructs a new [TZDateTime] instance based on [formattedString].
///
/// Throws a [FormatException] if the input cannot be parsed.
///
/// The function parses a subset of ISO 8601
/// which includes the subset accepted by RFC 3339.
///
/// The result is always in the time zone of the provided location.
///
/// Examples of accepted strings:
///
/// * `"2012-02-27 13:27:00"`
/// * `"2012-02-27 13:27:00.123456z"`
/// * `"20120227 13:27:00"`
/// * `"20120227T132700"`
/// * `"20120227"`
/// * `"+20120227"`
/// * `"2012-02-27T14Z"`
/// * `"2012-02-27T14+00:00"`
/// * `"-123450101 00:00:00 Z"`: in the year -12345.
/// * `"2002-02-27T14:00:00-0500"`: Same as `"2002-02-27T19:00:00Z"`
static TZDateTime parse(Location location, String formattedString) {
return TZDateTime.from(DateTime.parse(formattedString), location);
}
/// Returns this DateTime value in the UTC time zone.
///
/// Returns `this` if it is already in UTC.
@override
TZDateTime toUtc() => isUtc ? this : TZDateTime.from(native, UTC);
/// Returns this DateTime value in the local time zone.
///
/// Returns `this` if it is already in the local time zone.
@override
TZDateTime toLocal() => isLocal ? this : TZDateTime.from(native, local);
static String _fourDigits(int n) {
final absN = n.abs();
final sign = n < 0 ? '-' : '';
if (absN >= 1000) return '$n';
if (absN >= 100) return '${sign}0$absN';
if (absN >= 10) return '${sign}00$absN';
return '${sign}000$absN';
}
static String _threeDigits(int n) {
if (n >= 100) return '$n';
if (n >= 10) return '0$n';
return '00$n';
}
static String _twoDigits(int n) {
if (n >= 10) return '$n';
return '0$n';
}
/// Returns a human-readable string for this instance.
///
/// The returned string is constructed for the time zone of this instance.
/// The `toString()` method provides a simply formatted string.
/// It does not support internationalized strings.
/// Use the [intl](http://pub.dartlang.org/packages/intl) package
/// at the pub shared packages repo.
@override
String toString() => _toString(iso8601: false);
/// Returns an ISO-8601 full-precision extended format representation.
///
/// The format is yyyy-MM-ddTHH:mm:ss.mmmuuuZ for UTC time, and
/// yyyy-MM-ddTHH:mm:ss.mmmuuu±hhmm for local/non-UTC time, where:
///
/// * yyyy is a, possibly negative, four digit representation of the year,
/// if the year is in the range -9999 to 9999, otherwise it is a signed
/// six digit representation of the year.
/// * MM is the month in the range 01 to 12,
/// * dd is the day of the month in the range 01 to 31,
/// * HH are hours in the range 00 to 23,
/// * mm are minutes in the range 00 to 59,
/// * ss are seconds in the range 00 to 59 (no leap seconds),
/// * mmm are milliseconds in the range 000 to 999, and
/// * uuu are microseconds in the range 001 to 999. If microsecond equals 0,
/// then this part is omitted.
///
///The resulting string can be parsed back using parse.
@override
String toIso8601String() => _toString(iso8601: true);
String _toString({bool iso8601 = true}) {
final offset = timeZone.offset;
final y = _fourDigits(year);
final m = _twoDigits(month);
final d = _twoDigits(day);
final sep = iso8601 ? 'T' : ' ';
final h = _twoDigits(hour);
final min = _twoDigits(minute);
final sec = _twoDigits(second);
final ms = _threeDigits(millisecond);
final us = microsecond == 0 ? '' : _threeDigits(microsecond);
if (isUtc) {
return '$y-$m-$d$sep$h:$min:$sec.$ms${us}Z';
} else {
final offSign = offset.isNegative ? '-' : '+';
final offsetSeconds = offset.abs().inSeconds;
final offH = _twoDigits(offsetSeconds ~/ 3600);
final offM = _twoDigits((offsetSeconds % 3600) ~/ 60);
return '$y-$m-$d$sep$h:$min:$sec.$ms$us$offSign$offH$offM';
}
}
/// Returns a new [TZDateTime] instance with [duration] added to `this`.
@override
TZDateTime add(Duration duration) =>
TZDateTime.from(native.add(duration), location);
/// Returns a new [TZDateTime] instance with [duration] subtracted from
/// `this`.
@override
TZDateTime subtract(Duration duration) =>
TZDateTime.from(native.subtract(duration), location);
/// Returns a [Duration] with the difference between `this` and [other].
@override
Duration difference(DateTime other) => native.difference(_toNative(other));
/// Returns true if [other] is a [TZDateTime] at the same moment and in the
/// same [Location].
///
/// ```dart
/// final detroit = getLocation('America/Detroit');
/// final dDayUtc = TZDateTime.utc(1944, DateTime.JUNE, 6);
/// final dDayLocal = TZDateTime(detroit, 1944, DateTime.JUNE, 6);
///
/// assert(dDayUtc.isAtSameMomentAs(dDayLocal) == false);
/// ````
///
/// See [isAtSameMomentAs] for a comparison that adjusts for time zone.
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is TZDateTime &&
native.isAtSameMomentAs(other.native) &&
location == other.location;
}
/// Returns true if `this` occurs before [other].
///
/// The comparison is independent of whether the time is in UTC or in other
/// time zone.
///
/// ```dart
/// final berlinWallFell = TZDateTime(UTC, 1989, 11, 9);
/// final moonLanding = TZDateTime(UTC, 1969, 7, 20);
///
/// assert(berlinWallFell.isBefore(moonLanding) == false);
/// ```
@override
bool isBefore(DateTime other) => native.isBefore(_toNative(other));
/// Returns true if `this` occurs after [other].
///
/// The comparison is independent of whether the time is in UTC or in other
/// time zone.
///
/// ```dart
/// final berlinWallFell = TZDateTime(UTC, 1989, 11, 9);
/// final moonLanding = TZDateTime(UTC, 1969, 7, 20);
///
/// assert(berlinWallFell.isAfter(moonLanding) == true);
/// ```
@override
bool isAfter(DateTime other) => native.isAfter(_toNative(other));
/// Returns true if `this` occurs at the same moment as [other].
///
/// The comparison is independent of whether the time is in UTC or in other
/// time zone.
///
/// ```dart
/// final berlinWallFell = TZDateTime(UTC, 1989, 11, 9);
/// final moonLanding = TZDateTime(UTC, 1969, 7, 20);
///
/// assert(berlinWallFell.isAtSameMomentAs(moonLanding) == false);
/// ```
@override
bool isAtSameMomentAs(DateTime other) =>
native.isAtSameMomentAs(_toNative(other));
/// Compares this [TZDateTime] object to [other],
/// returning zero if the values occur at the same moment.
///
/// This function returns a negative integer
/// if this [TZDateTime] is smaller (earlier) than [other],
/// or a positive integer if it is greater (later).
@override
int compareTo(DateTime other) => native.compareTo(_toNative(other));
@override
int get hashCode => native.hashCode;
/// The abbreviated time zone name&mdash;for example,
/// [:"CET":] or [:"CEST":].
@override
String get timeZoneName => timeZone.abbreviation;
/// The time zone offset, which is the difference between time at [location]
/// and UTC.
///
/// The offset is positive for time zones east of UTC.
///
/// Note, that JavaScript, Python and C return the difference between UTC and
/// local time. Java, C# and Ruby return the difference between local time and
/// UTC.
@override
Duration get timeZoneOffset => timeZone.offset;
/// The year.
@override
int get year => _localDateTime.year;
/// The month [1..12].
@override
int get month => _localDateTime.month;
/// The day of the month [1..31].
@override
int get day => _localDateTime.day;
/// The hour of the day, expressed as in a 24-hour clock [0..23].
@override
int get hour => _localDateTime.hour;
/// The minute [0...59].
@override
int get minute => _localDateTime.minute;
/// The second [0...59].
@override
int get second => _localDateTime.second;
/// The millisecond [0...999].
@override
int get millisecond => _localDateTime.millisecond;
/// The microsecond [0...999].
@override
int get microsecond => _localDateTime.microsecond;
/// The day of the week.
///
/// In accordance with ISO 8601
/// a week starts with Monday, which has the value 1.
@override
int get weekday => _localDateTime.weekday;
}