blob: 58f8899343ca850e48d9ec4bb2803e071b747dcc [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.
/// 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);
}