| // 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. |
| |
| /// TimeZone Location Info. |
| /// |
| /// Most of this code were taken from the go standard library |
| /// [http://golang.org/src/pkg/time/zoneinfo.go](time/zoneinfo.go) |
| /// and ported to Dart. |
| library; |
| |
| /// Maximum value for time instants. |
| const int maxTime = 8640000000000000; |
| |
| /// Minimum value for time instants. |
| const int minTime = -maxTime; |
| |
| /// A [Location] maps time instants to the zone in use at that time. |
| /// Typically, the Location represents the collection of time offsets |
| /// in use in a geographical area, such as CEST and CET for central Europe. |
| class Location { |
| /// [Location] name. |
| final String name; |
| |
| /// Transition time, in milliseconds since 1970 UTC. |
| final List<int> transitionAt; |
| |
| /// The index of the zone that goes into effect at that time. |
| final List<int> transitionZone; |
| |
| /// [TimeZone]s at this [Location]. |
| final List<TimeZone> zones; |
| |
| /// [TimeZone] for the current time. |
| TimeZone get currentTimeZone => |
| timeZone(DateTime.now().millisecondsSinceEpoch); |
| |
| // Most lookups will be for the current time. |
| // To avoid the binary search through tx, keep a |
| // static one-element cache that gives the correct |
| // zone for the time when the Location was created. |
| // if cacheStart <= t <= cacheEnd, |
| // lookup can return cacheZone. |
| // The units for cacheStart and cacheEnd are milliseconds |
| // since January 1, 1970 UTC, to match the argument |
| // to lookup. |
| static final int _cacheNow = DateTime.now().millisecondsSinceEpoch; |
| int _cacheStart = 0; |
| int _cacheEnd = 0; |
| late TimeZone _cacheZone; |
| |
| Location(this.name, this.transitionAt, this.transitionZone, this.zones) { |
| // Fill in the cache with information about right now, |
| // since that will be the most common lookup. |
| for (var i = 0; i < transitionAt.length; i++) { |
| final tAt = transitionAt[i]; |
| |
| if ((tAt <= _cacheNow) && |
| ((i + 1 == transitionAt.length) || |
| (_cacheNow < transitionAt[i + 1]))) { |
| _cacheStart = tAt; |
| _cacheEnd = maxTime; |
| if (i + 1 < transitionAt.length) { |
| _cacheEnd = transitionAt[i + 1]; |
| } |
| _cacheZone = zones[transitionZone[i]]; |
| } |
| } |
| } |
| |
| /// translate instant in time expressed as milliseconds since |
| /// January 1, 1970 00:00:00 UTC to this [Location]. |
| int translate(int millisecondsSinceEpoch) { |
| return millisecondsSinceEpoch + |
| timeZone(millisecondsSinceEpoch).offset.inMilliseconds; |
| } |
| |
| /// translate instant in time expressed as milliseconds since |
| /// January 1, 1970 00:00:00 to UTC. |
| int translateToUtc(int millisecondsSinceEpoch) { |
| final t = lookupTimeZone(millisecondsSinceEpoch); |
| final tz = t.timeZone; |
| final start = t.start; |
| final end = t.end; |
| |
| var utc = millisecondsSinceEpoch; |
| |
| final offset = tz.offset.inMilliseconds; |
| if (offset != 0) { |
| utc -= offset; |
| |
| if (utc < start) { |
| utc = |
| millisecondsSinceEpoch - |
| lookupTimeZone(start - 1).timeZone.offset.inMilliseconds; |
| } else if (utc >= end) { |
| utc = |
| millisecondsSinceEpoch - |
| lookupTimeZone(end).timeZone.offset.inMilliseconds; |
| } |
| } |
| |
| return utc; |
| } |
| |
| /// lookup for [TimeZone] and its boundaries for an instant in time expressed |
| /// as milliseconds since January 1, 1970 00:00:00 UTC. |
| TzInstant lookupTimeZone(int millisecondsSinceEpoch) { |
| if (zones.isEmpty) { |
| return const TzInstant(TimeZone.UTC, minTime, maxTime); |
| } |
| |
| if (millisecondsSinceEpoch >= _cacheStart && |
| millisecondsSinceEpoch < _cacheEnd) { |
| return TzInstant(_cacheZone, _cacheStart, _cacheEnd); |
| } |
| |
| if (transitionAt.isEmpty || millisecondsSinceEpoch < transitionAt[0]) { |
| final zone = _firstZone(); |
| final start = minTime; |
| final end = transitionAt.isEmpty ? maxTime : transitionAt.first; |
| return TzInstant(zone, start, end); |
| } |
| |
| // Binary search for entry with largest millisecondsSinceEpoch <= sec. |
| var lo = 0; |
| var hi = transitionAt.length; |
| var end = maxTime; |
| |
| while (hi - lo > 1) { |
| final m = lo + (hi - lo) ~/ 2; |
| final at = transitionAt[m]; |
| |
| if (millisecondsSinceEpoch < at) { |
| end = at; |
| hi = m; |
| } else { |
| lo = m; |
| } |
| } |
| |
| return TzInstant(zones[transitionZone[lo]], transitionAt[lo], end); |
| } |
| |
| /// timeZone method returns [TimeZone] in use at an instant in time expressed |
| /// as milliseconds since January 1, 1970 00:00:00 UTC. |
| TimeZone timeZone(int millisecondsSinceEpoch) { |
| return lookupTimeZone(millisecondsSinceEpoch).timeZone; |
| } |
| |
| /// timeZoneFromLocal method returns [TimeZone] in use at an instant in time |
| /// expressed as milliseconds since January 1, 1970 00:00:00. |
| TimeZone timeZoneFromLocal(int millisecondsSinceEpoch) { |
| final t = lookupTimeZone(millisecondsSinceEpoch); |
| var tz = t.timeZone; |
| final start = t.start; |
| final end = t.end; |
| |
| final offset = tz.offset.inMilliseconds; |
| if (offset != 0) { |
| final utc = millisecondsSinceEpoch - offset; |
| |
| if (utc < start) { |
| tz = lookupTimeZone(start - 1).timeZone; |
| } else if (utc >= end) { |
| tz = lookupTimeZone(end).timeZone; |
| } |
| } |
| |
| return tz; |
| } |
| |
| /// This method returns the [TimeZone] to use for times before the first |
| /// transition time, or when there are no transition times. |
| /// |
| /// The reference implementation in localtime.c from |
| /// http://www.iana.org/time-zones/repository/releases/tzcode2013g.tar.gz |
| /// implements the following algorithm for these cases: |
| /// |
| /// 1. If the first zone is unused by the transitions, use it. |
| /// 2. Otherwise, if there are transition times, and the first |
| /// transition is to a zone in daylight time, find the first |
| /// non-daylight-time zone before and closest to the first transition |
| /// zone. |
| /// 3. Otherwise, use the first zone that is not daylight time, if |
| /// there is one. |
| /// 4. Otherwise, use the first zone. |
| /// |
| TimeZone _firstZone() { |
| // case 1 |
| if (!_firstZoneIsUsed()) { |
| return zones.first; |
| } |
| |
| // case 2 |
| if (transitionZone.isNotEmpty && zones[transitionZone.first].isDst) { |
| for (var zi = transitionZone.first - 1; zi >= 0; zi--) { |
| final z = zones[zi]; |
| if (!z.isDst) { |
| return z; |
| } |
| } |
| } |
| |
| // case 3 |
| for (final zi in transitionZone) { |
| final z = zones[zi]; |
| if (!z.isDst) { |
| return z; |
| } |
| } |
| |
| // case 4 |
| return zones.first; |
| } |
| |
| /// firstZoneUsed returns whether the first zone is used by some transition. |
| bool _firstZoneIsUsed() { |
| for (final i in transitionZone) { |
| if (i == 0) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| @override |
| String toString() => name; |
| |
| // Override equals and hashCode to support comparing |
| // Locations created in different isolates. |
| |
| @override |
| bool operator ==(Object other) { |
| return identical(this, other) || |
| other is Location && |
| runtimeType == other.runtimeType && |
| name == other.name; |
| } |
| |
| @override |
| int get hashCode { |
| return name.hashCode; |
| } |
| } |
| |
| /// A [TimeZone] represents a single time zone such as CEST or CET. |
| class TimeZone { |
| // ignore: constant_identifier_names |
| static const TimeZone UTC = TimeZone( |
| Duration.zero, |
| isDst: false, |
| abbreviation: 'UTC', |
| ); |
| |
| /// Milliseconds east of UTC. |
| final Duration offset; |
| |
| /// Is this [TimeZone] Daylight Savings Time? |
| final bool isDst; |
| |
| /// Abbreviated name, "CET". |
| final String abbreviation; |
| |
| const TimeZone( |
| this.offset, { |
| required this.isDst, |
| required this.abbreviation, |
| }); |
| |
| @override |
| bool operator ==(Object other) { |
| return identical(this, other) || |
| other is TimeZone && |
| offset == other.offset && |
| isDst == other.isDst && |
| abbreviation == other.abbreviation; |
| } |
| |
| @override |
| int get hashCode { |
| var result = 17; |
| result = 37 * result + offset.hashCode; |
| result = 37 * result + isDst.hashCode; |
| result = 37 * result + abbreviation.hashCode; |
| return result; |
| } |
| |
| @override |
| String toString() => '[$abbreviation offset=$offset dst=$isDst]'; |
| } |
| |
| /// A [TzInstant] represents a timezone and an instant in time. |
| class TzInstant { |
| final TimeZone timeZone; |
| final int start; |
| final int end; |
| |
| const TzInstant(this.timeZone, this.start, this.end); |
| } |