// Written in the D programming language /++ $(SCRIPT inhibitQuickIndex = 1;) $(DIVC quickindex, $(BOOKTABLE, $(TR $(TH Category) $(TH Functions)) $(TR $(TD Time zones) $(TD $(LREF TimeZone) $(LREF UTC) $(LREF LocalTime) $(LREF PosixTimeZone) $(LREF WindowsTimeZone) $(LREF SimpleTimeZone) )) $(TR $(TD Utilities) $(TD $(LREF clearTZEnvVar) $(LREF parseTZConversions) $(LREF setTZEnvVar) $(LREF TZConversions) )) )) License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). Authors: $(HTTP jmdavisprog.com, Jonathan M Davis) Source: $(PHOBOSSRC std/datetime/timezone.d) +/ module std.datetime.timezone; import core.time : abs, convert, dur, Duration, hours, minutes; import std.datetime.systime : Clock, stdTimeToUnixTime, SysTime; import std.range.primitives : back, empty, front, isOutputRange, popFront; import std.traits : isIntegral, isSomeString; version (OSX) version = Darwin; else version (iOS) version = Darwin; else version (TVOS) version = Darwin; else version (WatchOS) version = Darwin; version (Windows) { import core.stdc.time : time_t; import core.sys.windows.winbase; import core.sys.windows.winsock2; import std.windows.registry; // Uncomment and run unittests to print missing Windows TZ translations. // Please subscribe to Microsoft Daylight Saving Time & Time Zone Blog // (https://blogs.technet.microsoft.com/dst2007/) if you feel responsible // for updating the translations. // version = UpdateWindowsTZTranslations; } else version (Posix) { import core.sys.posix.signal : timespec; import core.sys.posix.sys.types : time_t; } version (StdUnittest) import std.exception : assertThrown; /++ Represents a time zone. It is used with $(REF SysTime,std,datetime,systime) to indicate the time zone of a $(REF SysTime,std,datetime,systime). +/ abstract class TimeZone { public: /++ The name of the time zone. Exactly how the time zone name is formatted depends on the derived class. In the case of $(LREF PosixTimeZone), it's the TZ Database name, whereas with $(LREF WindowsTimeZone), it's the name that Windows chose to give the registry key for that time zone (typically the name that they give $(LREF stdTime) if the OS is in English). For other time zone types, what it is depends on how they're implemented. See_Also: $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ Database)
$(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of Time Zones) +/ @property string name() @safe const nothrow { return _name; } /++ Typically, the abbreviation (generally 3 or 4 letters) for the time zone when DST is $(I not) in effect (e.g. PST). It is not necessarily unique. However, on Windows, it may be the unabbreviated name (e.g. Pacific Standard Time). Regardless, it is not the same as name. +/ @property string stdName() @safe const scope nothrow { return _stdName; } /++ Typically, the abbreviation (generally 3 or 4 letters) for the time zone when DST $(I is) in effect (e.g. PDT). It is not necessarily unique. However, on Windows, it may be the unabbreviated name (e.g. Pacific Daylight Time). Regardless, it is not the same as name. +/ @property string dstName() @safe const scope nothrow { return _dstName; } /++ Whether this time zone has Daylight Savings Time at any point in time. Note that for some time zone types it may not have DST for current dates but will still return true for `hasDST` because the time zone did at some point have DST. +/ @property abstract bool hasDST() @safe const nothrow; /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and returns whether DST is effect in this time zone at the given point in time. Params: stdTime = The UTC time that needs to be checked for DST in this time zone. +/ abstract bool dstInEffect(long stdTime) @safe const scope nothrow; /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and converts it to this time zone's time. Params: stdTime = The UTC time that needs to be adjusted to this time zone's time. +/ abstract long utcToTZ(long stdTime) @safe const scope nothrow; /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in this time zone's time and converts it to UTC (i.e. std time). Params: adjTime = The time in this time zone that needs to be adjusted to UTC time. +/ abstract long tzToUTC(long adjTime) @safe const scope nothrow; /++ Returns what the offset from UTC is at the given std time. It includes the DST offset in effect at that time (if any). Params: stdTime = The UTC time for which to get the offset from UTC for this time zone. +/ Duration utcOffsetAt(long stdTime) @safe const scope nothrow { return dur!"hnsecs"(utcToTZ(stdTime) - stdTime); } // The purpose of this is to handle the case where a Windows time zone is // new and exists on an up-to-date Windows box but does not exist on Windows // boxes which have not been properly updated. The "date added" is included // on the theory that we'll be able to remove them at some point in the // the future once enough time has passed, and that way, we know how much // time has passed. private static string _getOldName(string windowsTZName) @safe pure nothrow { switch (windowsTZName) { case "Belarus Standard Time": return "Kaliningrad Standard Time"; // Added 2014-10-08 case "Russia Time Zone 10": return "Magadan Standard Time"; // Added 2014-10-08 case "Russia Time Zone 11": return "Magadan Standard Time"; // Added 2014-10-08 case "Russia Time Zone 3": return "Russian Standard Time"; // Added 2014-10-08 default: return null; } } // Since reading in the time zone files could be expensive, most unit tests // are consolidated into this one unittest block which minimizes how often // it reads a time zone file. @system unittest { import core.exception : AssertError; import std.conv : to; import std.file : exists, isFile; import std.format : format; import std.path : chainPath; import std.stdio : writefln; import std.typecons : tuple; version (Posix) alias getTimeZone = PosixTimeZone.getTimeZone; else version (Windows) alias getTimeZone = WindowsTimeZone.getTimeZone; version (Posix) scope(exit) clearTZEnvVar(); static immutable(TimeZone) testTZ(string tzName, string stdName, string dstName, Duration utcOffset, Duration dstOffset, bool north = true) { scope(failure) writefln("Failed time zone: %s", tzName); version (Posix) { immutable tz = PosixTimeZone.getTimeZone(tzName); assert(tz.name == tzName); } else version (Windows) { immutable tz = WindowsTimeZone.getTimeZone(tzName); assert(tz.name == stdName); } immutable hasDST = dstOffset != Duration.zero; //assert(tz.stdName == stdName); //Locale-dependent //assert(tz.dstName == dstName); //Locale-dependent assert(tz.hasDST == hasDST); import std.datetime.date : DateTime; immutable stdDate = DateTime(2010, north ? 1 : 7, 1, 6, 0, 0); immutable dstDate = DateTime(2010, north ? 7 : 1, 1, 6, 0, 0); auto std = SysTime(stdDate, tz); auto dst = SysTime(dstDate, tz); auto stdUTC = SysTime(stdDate - utcOffset, UTC()); auto dstUTC = SysTime(stdDate - utcOffset + dstOffset, UTC()); assert(!std.dstInEffect); assert(dst.dstInEffect == hasDST); assert(tz.utcOffsetAt(std.stdTime) == utcOffset); assert(tz.utcOffsetAt(dst.stdTime) == utcOffset + dstOffset); assert(cast(DateTime) std == stdDate); assert(cast(DateTime) dst == dstDate); assert(std == stdUTC); version (Posix) { setTZEnvVar(tzName); static void testTM(scope const SysTime st) { import core.stdc.time : tm; import core.sys.posix.time : localtime_r; time_t unixTime = st.toUnixTime(); tm osTimeInfo = void; localtime_r(&unixTime, &osTimeInfo); tm ourTimeInfo = st.toTM(); assert(ourTimeInfo.tm_sec == osTimeInfo.tm_sec); assert(ourTimeInfo.tm_min == osTimeInfo.tm_min); assert(ourTimeInfo.tm_hour == osTimeInfo.tm_hour); assert(ourTimeInfo.tm_mday == osTimeInfo.tm_mday); assert(ourTimeInfo.tm_mon == osTimeInfo.tm_mon); assert(ourTimeInfo.tm_year == osTimeInfo.tm_year); assert(ourTimeInfo.tm_wday == osTimeInfo.tm_wday); assert(ourTimeInfo.tm_yday == osTimeInfo.tm_yday); assert(ourTimeInfo.tm_isdst == osTimeInfo.tm_isdst); assert(ourTimeInfo.tm_gmtoff == osTimeInfo.tm_gmtoff); assert(to!string(ourTimeInfo.tm_zone) == to!string(osTimeInfo.tm_zone)); } testTM(std); testTM(dst); // Apparently, right/ does not exist on Mac OS X. I don't know // whether or not it exists on FreeBSD. It's rather pointless // normally, since the Posix standard requires that leap seconds // be ignored, so it does make some sense that right/ wouldn't // be there, but since PosixTimeZone _does_ use leap seconds if // the time zone file does, we'll test that functionality if the // appropriate files exist. if (chainPath(PosixTimeZone.defaultTZDatabaseDir, "right", tzName).exists) { auto leapTZ = PosixTimeZone.getTimeZone("right/" ~ tzName); assert(leapTZ.name == "right/" ~ tzName); //assert(leapTZ.stdName == stdName); //Locale-dependent //assert(leapTZ.dstName == dstName); //Locale-dependent assert(leapTZ.hasDST == hasDST); auto leapSTD = SysTime(std.stdTime, leapTZ); auto leapDST = SysTime(dst.stdTime, leapTZ); assert(!leapSTD.dstInEffect); assert(leapDST.dstInEffect == hasDST); assert(leapSTD.stdTime == std.stdTime); assert(leapDST.stdTime == dst.stdTime); // Whenever a leap second is added/removed, // this will have to be adjusted. //enum leapDiff = convert!("seconds", "hnsecs")(25); //assert(leapSTD.adjTime - leapDiff == std.adjTime); //assert(leapDST.adjTime - leapDiff == dst.adjTime); } } return tz; } import std.datetime.date : DateTime; auto dstSwitches = [/+America/Los_Angeles+/ tuple(DateTime(2012, 3, 11), DateTime(2012, 11, 4), 2, 2), /+America/New_York+/ tuple(DateTime(2012, 3, 11), DateTime(2012, 11, 4), 2, 2), ///+America/Santiago+/ tuple(DateTime(2011, 8, 21), DateTime(2011, 5, 8), 0, 0), /+Europe/London+/ tuple(DateTime(2012, 3, 25), DateTime(2012, 10, 28), 1, 2), /+Europe/Paris+/ tuple(DateTime(2012, 3, 25), DateTime(2012, 10, 28), 2, 3), /+Australia/Adelaide+/ tuple(DateTime(2012, 10, 7), DateTime(2012, 4, 1), 2, 3)]; import std.datetime.date : DateTimeException; version (Posix) { version (FreeBSD) enum utcZone = "Etc/UTC"; else version (OpenBSD) enum utcZone = "UTC"; else version (NetBSD) enum utcZone = "UTC"; else version (DragonFlyBSD) enum utcZone = "UTC"; else version (linux) enum utcZone = "UTC"; else version (Darwin) enum utcZone = "UTC"; else version (Solaris) enum utcZone = "UTC"; else static assert(0, "The location of the UTC timezone file on this Posix platform must be set."); auto tzs = [testTZ("America/Los_Angeles", "PST", "PDT", dur!"hours"(-8), dur!"hours"(1)), testTZ("America/New_York", "EST", "EDT", dur!"hours"(-5), dur!"hours"(1)), //testTZ("America/Santiago", "CLT", "CLST", dur!"hours"(-4), dur!"hours"(1), false), testTZ("Europe/London", "GMT", "BST", dur!"hours"(0), dur!"hours"(1)), testTZ("Europe/Paris", "CET", "CEST", dur!"hours"(1), dur!"hours"(1)), // Per www.timeanddate.com, it should be "CST" and "CDT", // but the OS insists that it's "CST" for both. We should // probably figure out how to report an error in the TZ // database and report it. testTZ("Australia/Adelaide", "CST", "CST", dur!"hours"(9) + dur!"minutes"(30), dur!"hours"(1), false)]; testTZ(utcZone, "UTC", "UTC", dur!"hours"(0), dur!"hours"(0)); assertThrown!DateTimeException(PosixTimeZone.getTimeZone("hello_world")); } else version (Windows) { auto tzs = [testTZ("Pacific Standard Time", "Pacific Standard Time", "Pacific Daylight Time", dur!"hours"(-8), dur!"hours"(1)), testTZ("Eastern Standard Time", "Eastern Standard Time", "Eastern Daylight Time", dur!"hours"(-5), dur!"hours"(1)), //testTZ("Pacific SA Standard Time", "Pacific SA Standard Time", //"Pacific SA Daylight Time", dur!"hours"(-4), dur!"hours"(1), false), testTZ("GMT Standard Time", "GMT Standard Time", "GMT Daylight Time", dur!"hours"(0), dur!"hours"(1)), testTZ("Romance Standard Time", "Romance Standard Time", "Romance Daylight Time", dur!"hours"(1), dur!"hours"(1)), testTZ("Cen. Australia Standard Time", "Cen. Australia Standard Time", "Cen. Australia Daylight Time", dur!"hours"(9) + dur!"minutes"(30), dur!"hours"(1), false)]; testTZ("Greenwich Standard Time", "Greenwich Standard Time", "Greenwich Daylight Time", dur!"hours"(0), dur!"hours"(0)); assertThrown!DateTimeException(WindowsTimeZone.getTimeZone("hello_world")); } else assert(0, "OS not supported."); foreach (i; 0 .. tzs.length) { auto tz = tzs[i]; immutable spring = dstSwitches[i][2]; immutable fall = dstSwitches[i][3]; auto stdOffset = SysTime(dstSwitches[i][0] + dur!"days"(-1), tz).utcOffset; auto dstOffset = stdOffset + dur!"hours"(1); // Verify that creating a SysTime in the given time zone results // in a SysTime with the correct std time during and surrounding // a DST switch. foreach (hour; -12 .. 13) { import std.exception : enforce; auto st = SysTime(dstSwitches[i][0] + dur!"hours"(hour), tz); immutable targetHour = hour < 0 ? hour + 24 : hour; static void testHour(SysTime st, int hour, string tzName, size_t line = __LINE__) { enforce(st.hour == hour, new AssertError(format("[%s] [%s]: [%s] [%s]", st, tzName, st.hour, hour), __FILE__, line)); } void testOffset1(Duration offset, bool dstInEffect, size_t line = __LINE__) { AssertError msg(string tag) { return new AssertError(format("%s [%s] [%s]: [%s] [%s] [%s]", tag, st, tz.name, st.utcOffset, stdOffset, dstOffset), __FILE__, line); } enforce(st.dstInEffect == dstInEffect, msg("1")); enforce(st.utcOffset == offset, msg("2")); enforce((st + dur!"minutes"(1)).utcOffset == offset, msg("3")); } if (hour == spring) { testHour(st, spring + 1, tz.name); testHour(st + dur!"minutes"(1), spring + 1, tz.name); } else { testHour(st, targetHour, tz.name); testHour(st + dur!"minutes"(1), targetHour, tz.name); } if (hour < spring) testOffset1(stdOffset, false); else testOffset1(dstOffset, true); st = SysTime(dstSwitches[i][1] + dur!"hours"(hour), tz); testHour(st, targetHour, tz.name); // Verify that 01:00 is the first 01:00 (or whatever hour before the switch is). if (hour == fall - 1) testHour(st + dur!"hours"(1), targetHour, tz.name); if (hour < fall) testOffset1(dstOffset, true); else testOffset1(stdOffset, false); } // Verify that converting a time in UTC to a time in another // time zone results in the correct time during and surrounding // a DST switch. bool first = true; auto springSwitch = SysTime(dstSwitches[i][0] + dur!"hours"(spring), UTC()) - stdOffset; auto fallSwitch = SysTime(dstSwitches[i][1] + dur!"hours"(fall), UTC()) - dstOffset; // https://issues.dlang.org/show_bug.cgi?id=3659 makes this necessary. auto fallSwitchMinus1 = fallSwitch - dur!"hours"(1); foreach (hour; -24 .. 25) { auto utc = SysTime(dstSwitches[i][0] + dur!"hours"(hour), UTC()); auto local = utc.toOtherTZ(tz); void testOffset2(Duration offset, size_t line = __LINE__) { AssertError msg(string tag) { return new AssertError(format("%s [%s] [%s]: [%s] [%s]", tag, hour, tz.name, utc, local), __FILE__, line); } import std.exception : enforce; enforce((utc + offset).hour == local.hour, msg("1")); enforce((utc + offset + dur!"minutes"(1)).hour == local.hour, msg("2")); } if (utc < springSwitch) testOffset2(stdOffset); else testOffset2(dstOffset); utc = SysTime(dstSwitches[i][1] + dur!"hours"(hour), UTC()); local = utc.toOtherTZ(tz); if (utc == fallSwitch || utc == fallSwitchMinus1) { if (first) { testOffset2(dstOffset); first = false; } else testOffset2(stdOffset); } else if (utc > fallSwitch) testOffset2(stdOffset); else testOffset2(dstOffset); } } } protected: /++ Params: name = The name of the time zone. stdName = The abbreviation for the time zone during std time. dstName = The abbreviation for the time zone during DST. +/ this(string name, string stdName, string dstName) @safe immutable pure { _name = name; _stdName = stdName; _dstName = dstName; } private: immutable string _name; immutable string _stdName; immutable string _dstName; } /++ A TimeZone which represents the current local time zone on the system running your program. This uses the underlying C calls to adjust the time rather than using specific D code based off of system settings to calculate the time such as $(LREF PosixTimeZone) and $(LREF WindowsTimeZone) do. That also means that it will use whatever the current time zone is on the system, even if the system's time zone changes while the program is running. +/ final class LocalTime : TimeZone { public: /++ $(LREF LocalTime) is a singleton class. $(LREF LocalTime) returns its only instance. +/ static immutable(LocalTime) opCall() @trusted pure nothrow { alias FuncType = immutable(LocalTime) function() @safe pure nothrow; return (cast(FuncType)&singleton)(); } version (StdDdoc) { /++ In principle, this is the name of the local time zone. However, this always returns the empty string. This is because time zones cannot be uniquely identified by the attributes given by the OS (such as the `stdName` and `dstName`), and neither Posix systems nor Windows systems provide an easy way to get the TZ Database name of the local time zone. See_Also: $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ Database)
$(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of Time Zones) +/ @property override string name() @safe const nothrow; } /++ Typically, the abbreviation (generally 3 or 4 letters) for the time zone when DST is $(I not) in effect (e.g. PST). It is not necessarily unique. However, on Windows, it may be the unabbreviated name (e.g. Pacific Standard Time). Regardless, it is not the same as name. This property is overridden because the local time of the system could change while the program is running and we need to determine it dynamically rather than it being fixed like it would be with most time zones. +/ @property override string stdName() @trusted const scope nothrow { version (Posix) { import core.stdc.time : tzname; import std.conv : to; try return to!string(tzname[0]); catch (Exception e) assert(0, "to!string(tzname[0]) failed."); } else version (Windows) { TIME_ZONE_INFORMATION tzInfo; GetTimeZoneInformation(&tzInfo); // Cannot use to!string() like this should, probably due to bug // https://issues.dlang.org/show_bug.cgi?id=5016 //return to!string(tzInfo.StandardName); wchar[32] str; foreach (i, ref wchar c; str) c = tzInfo.StandardName[i]; string retval; try { foreach (dchar c; str) { if (c == '\0') break; retval ~= c; } return retval; } catch (Exception e) assert(0, "GetTimeZoneInformation() returned invalid UTF-16."); } } @safe unittest { version (FreeBSD) { // A bug on FreeBSD 9+ makes it so that this test fails. // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=168862 } else version (NetBSD) { // The same bug on NetBSD 7+ } else { assert(LocalTime().stdName !is null); version (Posix) { scope(exit) clearTZEnvVar(); setTZEnvVar("America/Los_Angeles"); assert(LocalTime().stdName == "PST"); setTZEnvVar("America/New_York"); assert(LocalTime().stdName == "EST"); } } } /++ Typically, the abbreviation (generally 3 or 4 letters) for the time zone when DST $(I is) in effect (e.g. PDT). It is not necessarily unique. However, on Windows, it may be the unabbreviated name (e.g. Pacific Daylight Time). Regardless, it is not the same as name. This property is overridden because the local time of the system could change while the program is running and we need to determine it dynamically rather than it being fixed like it would be with most time zones. +/ @property override string dstName() @trusted const scope nothrow { version (Posix) { import core.stdc.time : tzname; import std.conv : to; try return to!string(tzname[1]); catch (Exception e) assert(0, "to!string(tzname[1]) failed."); } else version (Windows) { TIME_ZONE_INFORMATION tzInfo; GetTimeZoneInformation(&tzInfo); // Cannot use to!string() like this should, probably due to bug // https://issues.dlang.org/show_bug.cgi?id=5016 //return to!string(tzInfo.DaylightName); wchar[32] str; foreach (i, ref wchar c; str) c = tzInfo.DaylightName[i]; string retval; try { foreach (dchar c; str) { if (c == '\0') break; retval ~= c; } return retval; } catch (Exception e) assert(0, "GetTimeZoneInformation() returned invalid UTF-16."); } } @safe unittest { // tzname, called from dstName, isn't set by default for Musl. version (CRuntime_Musl) assert(LocalTime().dstName is null); else assert(LocalTime().dstName !is null); version (Posix) { scope(exit) clearTZEnvVar(); version (FreeBSD) { // A bug on FreeBSD 9+ makes it so that this test fails. // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=168862 } else version (NetBSD) { // The same bug on NetBSD 7+ } else { setTZEnvVar("America/Los_Angeles"); assert(LocalTime().dstName == "PDT"); setTZEnvVar("America/New_York"); assert(LocalTime().dstName == "EDT"); } } } /++ Whether this time zone has Daylight Savings Time at any point in time. Note that for some time zone types it may not have DST for current dates but will still return true for `hasDST` because the time zone did at some point have DST. +/ @property override bool hasDST() @trusted const nothrow { version (Posix) { static if (is(typeof(daylight))) return cast(bool)(daylight); else { try { import std.datetime.date : Date; auto currYear = (cast(Date) Clock.currTime()).year; auto janOffset = SysTime(Date(currYear, 1, 4), cast(immutable) this).stdTime - SysTime(Date(currYear, 1, 4), UTC()).stdTime; auto julyOffset = SysTime(Date(currYear, 7, 4), cast(immutable) this).stdTime - SysTime(Date(currYear, 7, 4), UTC()).stdTime; return janOffset != julyOffset; } catch (Exception e) assert(0, "Clock.currTime() threw."); } } else version (Windows) { TIME_ZONE_INFORMATION tzInfo; GetTimeZoneInformation(&tzInfo); return tzInfo.DaylightDate.wMonth != 0; } } @safe unittest { LocalTime().hasDST; version (Posix) { scope(exit) clearTZEnvVar(); setTZEnvVar("America/Los_Angeles"); assert(LocalTime().hasDST); setTZEnvVar("America/New_York"); assert(LocalTime().hasDST); setTZEnvVar("UTC"); assert(!LocalTime().hasDST); } } /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and returns whether DST is in effect in this time zone at the given point in time. Params: stdTime = The UTC time that needs to be checked for DST in this time zone. +/ override bool dstInEffect(long stdTime) @trusted const scope nothrow { import core.stdc.time : tm; time_t unixTime = stdTimeToUnixTime(stdTime); version (Posix) { import core.sys.posix.time : localtime_r; tm timeInfo = void; localtime_r(&unixTime, &timeInfo); return cast(bool)(timeInfo.tm_isdst); } else version (Windows) { import core.stdc.time : localtime; // Apparently Windows isn't smart enough to deal with negative time_t. if (unixTime >= 0) { tm* timeInfo = localtime(&unixTime); if (timeInfo) return cast(bool)(timeInfo.tm_isdst); } TIME_ZONE_INFORMATION tzInfo; GetTimeZoneInformation(&tzInfo); return WindowsTimeZone._dstInEffect(&tzInfo, stdTime); } } @safe unittest { auto currTime = Clock.currStdTime; LocalTime().dstInEffect(currTime); } /++ Returns hnsecs in the local time zone using the standard C function calls on Posix systems and the standard Windows system calls on Windows systems to adjust the time to the appropriate time zone from std time. Params: stdTime = The UTC time that needs to be adjusted to this time zone's time. See_Also: `TimeZone.utcToTZ` +/ override long utcToTZ(long stdTime) @trusted const scope nothrow { version (Solaris) return stdTime + convert!("seconds", "hnsecs")(tm_gmtoff(stdTime)); else version (Posix) { import core.stdc.time : tm; import core.sys.posix.time : localtime_r; time_t unixTime = stdTimeToUnixTime(stdTime); tm timeInfo = void; localtime_r(&unixTime, &timeInfo); return stdTime + convert!("seconds", "hnsecs")(timeInfo.tm_gmtoff); } else version (Windows) { TIME_ZONE_INFORMATION tzInfo; GetTimeZoneInformation(&tzInfo); return WindowsTimeZone._utcToTZ(&tzInfo, stdTime, hasDST); } } @safe unittest { LocalTime().utcToTZ(0); } /++ Returns std time using the standard C function calls on Posix systems and the standard Windows system calls on Windows systems to adjust the time to UTC from the appropriate time zone. See_Also: `TimeZone.tzToUTC` Params: adjTime = The time in this time zone that needs to be adjusted to UTC time. +/ override long tzToUTC(long adjTime) @trusted const scope nothrow { version (Posix) { import core.stdc.time : tm; import core.sys.posix.time : localtime_r; time_t unixTime = stdTimeToUnixTime(adjTime); immutable past = unixTime - cast(time_t) convert!("days", "seconds")(1); tm timeInfo = void; localtime_r(past < unixTime ? &past : &unixTime, &timeInfo); immutable pastOffset = timeInfo.tm_gmtoff; immutable future = unixTime + cast(time_t) convert!("days", "seconds")(1); localtime_r(future > unixTime ? &future : &unixTime, &timeInfo); immutable futureOffset = timeInfo.tm_gmtoff; if (pastOffset == futureOffset) return adjTime - convert!("seconds", "hnsecs")(pastOffset); if (pastOffset < futureOffset) unixTime -= cast(time_t) convert!("hours", "seconds")(1); unixTime -= pastOffset; localtime_r(&unixTime, &timeInfo); return adjTime - convert!("seconds", "hnsecs")(timeInfo.tm_gmtoff); } else version (Windows) { TIME_ZONE_INFORMATION tzInfo; GetTimeZoneInformation(&tzInfo); return WindowsTimeZone._tzToUTC(&tzInfo, adjTime, hasDST); } } @safe unittest { import core.exception : AssertError; import std.format : format; import std.typecons : tuple; assert(LocalTime().tzToUTC(LocalTime().utcToTZ(0)) == 0); assert(LocalTime().utcToTZ(LocalTime().tzToUTC(0)) == 0); assert(LocalTime().tzToUTC(LocalTime().utcToTZ(0)) == 0); assert(LocalTime().utcToTZ(LocalTime().tzToUTC(0)) == 0); version (Posix) { scope(exit) clearTZEnvVar(); import std.datetime.date : DateTime; auto tzInfos = [tuple("America/Los_Angeles", DateTime(2012, 3, 11), DateTime(2012, 11, 4), 2, 2), tuple("America/New_York", DateTime(2012, 3, 11), DateTime(2012, 11, 4), 2, 2), //tuple("America/Santiago", DateTime(2011, 8, 21), DateTime(2011, 5, 8), 0, 0), tuple("Atlantic/Azores", DateTime(2011, 3, 27), DateTime(2011, 10, 30), 0, 1), tuple("Europe/London", DateTime(2012, 3, 25), DateTime(2012, 10, 28), 1, 2), tuple("Europe/Paris", DateTime(2012, 3, 25), DateTime(2012, 10, 28), 2, 3), tuple("Australia/Adelaide", DateTime(2012, 10, 7), DateTime(2012, 4, 1), 2, 3)]; foreach (i; 0 .. tzInfos.length) { import std.exception : enforce; auto tzName = tzInfos[i][0]; setTZEnvVar(tzName); immutable spring = tzInfos[i][3]; immutable fall = tzInfos[i][4]; auto stdOffset = SysTime(tzInfos[i][1] + dur!"hours"(-12)).utcOffset; auto dstOffset = stdOffset + dur!"hours"(1); // Verify that creating a SysTime in the given time zone results // in a SysTime with the correct std time during and surrounding // a DST switch. foreach (hour; -12 .. 13) { auto st = SysTime(tzInfos[i][1] + dur!"hours"(hour)); immutable targetHour = hour < 0 ? hour + 24 : hour; static void testHour(SysTime st, int hour, string tzName, size_t line = __LINE__) { enforce(st.hour == hour, new AssertError(format("[%s] [%s]: [%s] [%s]", st, tzName, st.hour, hour), __FILE__, line)); } void testOffset1(Duration offset, bool dstInEffect, size_t line = __LINE__) { AssertError msg(string tag) { return new AssertError(format("%s [%s] [%s]: [%s] [%s] [%s]", tag, st, tzName, st.utcOffset, stdOffset, dstOffset), __FILE__, line); } enforce(st.dstInEffect == dstInEffect, msg("1")); enforce(st.utcOffset == offset, msg("2")); enforce((st + dur!"minutes"(1)).utcOffset == offset, msg("3")); } if (hour == spring) { testHour(st, spring + 1, tzName); testHour(st + dur!"minutes"(1), spring + 1, tzName); } else { testHour(st, targetHour, tzName); testHour(st + dur!"minutes"(1), targetHour, tzName); } if (hour < spring) testOffset1(stdOffset, false); else testOffset1(dstOffset, true); st = SysTime(tzInfos[i][2] + dur!"hours"(hour)); testHour(st, targetHour, tzName); // Verify that 01:00 is the first 01:00 (or whatever hour before the switch is). if (hour == fall - 1) testHour(st + dur!"hours"(1), targetHour, tzName); if (hour < fall) testOffset1(dstOffset, true); else testOffset1(stdOffset, false); } // Verify that converting a time in UTC to a time in another // time zone results in the correct time during and surrounding // a DST switch. bool first = true; auto springSwitch = SysTime(tzInfos[i][1] + dur!"hours"(spring), UTC()) - stdOffset; auto fallSwitch = SysTime(tzInfos[i][2] + dur!"hours"(fall), UTC()) - dstOffset; // https://issues.dlang.org/show_bug.cgi?id=3659 makes this necessary. auto fallSwitchMinus1 = fallSwitch - dur!"hours"(1); foreach (hour; -24 .. 25) { auto utc = SysTime(tzInfos[i][1] + dur!"hours"(hour), UTC()); auto local = utc.toLocalTime(); void testOffset2(Duration offset, size_t line = __LINE__) { AssertError msg(string tag) { return new AssertError(format("%s [%s] [%s]: [%s] [%s]", tag, hour, tzName, utc, local), __FILE__, line); } enforce((utc + offset).hour == local.hour, msg("1")); enforce((utc + offset + dur!"minutes"(1)).hour == local.hour, msg("2")); } if (utc < springSwitch) testOffset2(stdOffset); else testOffset2(dstOffset); utc = SysTime(tzInfos[i][2] + dur!"hours"(hour), UTC()); local = utc.toLocalTime(); if (utc == fallSwitch || utc == fallSwitchMinus1) { if (first) { testOffset2(dstOffset); first = false; } else testOffset2(stdOffset); } else if (utc > fallSwitch) testOffset2(stdOffset); else testOffset2(dstOffset); } } } } private: this() @safe immutable pure { super("", "", ""); } // This is done so that we can maintain purity in spite of doing an impure // operation the first time that LocalTime() is called. static immutable(LocalTime) singleton() @trusted { import core.stdc.time : tzset; import std.concurrency : initOnce; static instance = new immutable(LocalTime)(); static shared bool guard; initOnce!guard({tzset(); return true;}()); return instance; } // The Solaris version of struct tm has no tm_gmtoff field, so do it here version (Solaris) { long tm_gmtoff(long stdTime) @trusted const nothrow { import core.stdc.time : tm; import core.sys.posix.time : localtime_r, gmtime_r; time_t unixTime = stdTimeToUnixTime(stdTime); tm timeInfo = void; localtime_r(&unixTime, &timeInfo); tm timeInfoGmt = void; gmtime_r(&unixTime, &timeInfoGmt); return timeInfo.tm_sec - timeInfoGmt.tm_sec + convert!("minutes", "seconds")(timeInfo.tm_min - timeInfoGmt.tm_min) + convert!("hours", "seconds")(timeInfo.tm_hour - timeInfoGmt.tm_hour); } } } /++ A $(LREF TimeZone) which represents UTC. +/ final class UTC : TimeZone { public: /++ `UTC` is a singleton class. `UTC` returns its only instance. +/ static immutable(UTC) opCall() @safe pure nothrow { return _utc; } /++ Always returns false. +/ @property override bool hasDST() @safe const nothrow { return false; } /++ Always returns false. +/ override bool dstInEffect(long stdTime) @safe const scope nothrow { return false; } /++ Returns the given hnsecs without changing them at all. Params: stdTime = The UTC time that needs to be adjusted to this time zone's time. See_Also: `TimeZone.utcToTZ` +/ override long utcToTZ(long stdTime) @safe const scope nothrow { return stdTime; } @safe unittest { assert(UTC().utcToTZ(0) == 0); version (Posix) { scope(exit) clearTZEnvVar(); setTZEnvVar("UTC"); import std.datetime.date : Date; auto std = SysTime(Date(2010, 1, 1)); auto dst = SysTime(Date(2010, 7, 1)); assert(UTC().utcToTZ(std.stdTime) == std.stdTime); assert(UTC().utcToTZ(dst.stdTime) == dst.stdTime); } } /++ Returns the given hnsecs without changing them at all. See_Also: `TimeZone.tzToUTC` Params: adjTime = The time in this time zone that needs to be adjusted to UTC time. +/ override long tzToUTC(long adjTime) @safe const scope nothrow { return adjTime; } @safe unittest { assert(UTC().tzToUTC(0) == 0); version (Posix) { scope(exit) clearTZEnvVar(); setTZEnvVar("UTC"); import std.datetime.date : Date; auto std = SysTime(Date(2010, 1, 1)); auto dst = SysTime(Date(2010, 7, 1)); assert(UTC().tzToUTC(std.stdTime) == std.stdTime); assert(UTC().tzToUTC(dst.stdTime) == dst.stdTime); } } /++ Returns a $(REF Duration, core,time) of 0. Params: stdTime = The UTC time for which to get the offset from UTC for this time zone. +/ override Duration utcOffsetAt(long stdTime) @safe const scope nothrow { return dur!"hnsecs"(0); } private: this() @safe immutable pure { super("UTC", "UTC", "UTC"); } static immutable UTC _utc = new immutable(UTC)(); } /++ Represents a time zone with an offset (in minutes, west is negative) from UTC but no DST. It's primarily used as the time zone in the result of $(REF SysTime,std,datetime,systime)'s `fromISOString`, `fromISOExtString`, and `fromSimpleString`. `name` and `dstName` are always the empty string since this time zone has no DST, and while it may be meant to represent a time zone which is in the TZ Database, obviously it's not likely to be following the exact rules of any of the time zones in the TZ Database, so it makes no sense to set it. +/ final class SimpleTimeZone : TimeZone { public: /++ Always returns false. +/ @property override bool hasDST() @safe const nothrow { return false; } /++ Always returns false. +/ override bool dstInEffect(long stdTime) @safe const scope nothrow { return false; } /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and converts it to this time zone's time. Params: stdTime = The UTC time that needs to be adjusted to this time zone's time. +/ override long utcToTZ(long stdTime) @safe const scope nothrow { return stdTime + _utcOffset.total!"hnsecs"; } @safe unittest { auto west = new immutable SimpleTimeZone(dur!"hours"(-8)); auto east = new immutable SimpleTimeZone(dur!"hours"(8)); assert(west.utcToTZ(0) == -288_000_000_000L); assert(east.utcToTZ(0) == 288_000_000_000L); assert(west.utcToTZ(54_321_234_567_890L) == 54_033_234_567_890L); const cstz = west; assert(cstz.utcToTZ(50002) == west.utcToTZ(50002)); } /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in this time zone's time and converts it to UTC (i.e. std time). Params: adjTime = The time in this time zone that needs to be adjusted to UTC time. +/ override long tzToUTC(long adjTime) @safe const scope nothrow { return adjTime - _utcOffset.total!"hnsecs"; } @safe unittest { auto west = new immutable SimpleTimeZone(dur!"hours"(-8)); auto east = new immutable SimpleTimeZone(dur!"hours"(8)); assert(west.tzToUTC(-288_000_000_000L) == 0); assert(east.tzToUTC(288_000_000_000L) == 0); assert(west.tzToUTC(54_033_234_567_890L) == 54_321_234_567_890L); const cstz = west; assert(cstz.tzToUTC(20005) == west.tzToUTC(20005)); } /++ Returns utcOffset as a $(REF Duration, core,time). Params: stdTime = The UTC time for which to get the offset from UTC for this time zone. +/ override Duration utcOffsetAt(long stdTime) @safe const scope nothrow { return _utcOffset; } /++ Params: utcOffset = This time zone's offset from UTC with west of UTC being negative (it is added to UTC to get the adjusted time). stdName = The `stdName` for this time zone. +/ this(Duration utcOffset, string stdName = "") @safe immutable pure { // FIXME This probably needs to be changed to something like (-12 - 13). import std.datetime.date : DateTimeException; import std.exception : enforce; enforce!DateTimeException(abs(utcOffset) < dur!"minutes"(1440), "Offset from UTC must be within range (-24:00 - 24:00)."); super("", stdName, ""); this._utcOffset = utcOffset; } @safe unittest { auto stz = new immutable SimpleTimeZone(dur!"hours"(-8), "PST"); assert(stz.name == ""); assert(stz.stdName == "PST"); assert(stz.dstName == ""); assert(stz.utcOffset == dur!"hours"(-8)); } /++ The amount of time the offset from UTC is (negative is west of UTC, positive is east). +/ @property Duration utcOffset() @safe const pure nothrow { return _utcOffset; } package: /+ Returns a time zone as a string with an offset from UTC. Time zone offsets will be in the form +HHMM or -HHMM. Params: utcOffset = The number of minutes offset from UTC (negative means west). +/ static string toISOString(Duration utcOffset) @safe pure { import std.array : appender; auto w = appender!string(); w.reserve(5); toISOString(w, utcOffset); return w.data; } // ditto static void toISOString(W)(ref W writer, Duration utcOffset) if (isOutputRange!(W, char)) { import std.datetime.date : DateTimeException; import std.exception : enforce; import std.format.write : formattedWrite; immutable absOffset = abs(utcOffset); enforce!DateTimeException(absOffset < dur!"minutes"(1440), "Offset from UTC must be within range (-24:00 - 24:00)."); int hours; int minutes; absOffset.split!("hours", "minutes")(hours, minutes); formattedWrite( writer, utcOffset < Duration.zero ? "-%02d%02d" : "+%02d%02d", hours, minutes ); } @safe unittest { static string testSTZInvalid(Duration offset) { return SimpleTimeZone.toISOString(offset); } import std.datetime.date : DateTimeException; assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(1440))); assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(-1440))); assert(toISOString(dur!"minutes"(0)) == "+0000"); assert(toISOString(dur!"minutes"(1)) == "+0001"); assert(toISOString(dur!"minutes"(10)) == "+0010"); assert(toISOString(dur!"minutes"(59)) == "+0059"); assert(toISOString(dur!"minutes"(60)) == "+0100"); assert(toISOString(dur!"minutes"(90)) == "+0130"); assert(toISOString(dur!"minutes"(120)) == "+0200"); assert(toISOString(dur!"minutes"(480)) == "+0800"); assert(toISOString(dur!"minutes"(1439)) == "+2359"); assert(toISOString(dur!"minutes"(-1)) == "-0001"); assert(toISOString(dur!"minutes"(-10)) == "-0010"); assert(toISOString(dur!"minutes"(-59)) == "-0059"); assert(toISOString(dur!"minutes"(-60)) == "-0100"); assert(toISOString(dur!"minutes"(-90)) == "-0130"); assert(toISOString(dur!"minutes"(-120)) == "-0200"); assert(toISOString(dur!"minutes"(-480)) == "-0800"); assert(toISOString(dur!"minutes"(-1439)) == "-2359"); } /+ Returns a time zone as a string with an offset from UTC. Time zone offsets will be in the form +HH:MM or -HH:MM. Params: utcOffset = The number of minutes offset from UTC (negative means west). +/ static string toISOExtString(Duration utcOffset) @safe pure { import std.array : appender; auto w = appender!string(); w.reserve(6); toISOExtString(w, utcOffset); return w.data; } // ditto static void toISOExtString(W)(ref W writer, Duration utcOffset) { import std.datetime.date : DateTimeException; import std.format.write : formattedWrite; import std.exception : enforce; immutable absOffset = abs(utcOffset); enforce!DateTimeException(absOffset < dur!"minutes"(1440), "Offset from UTC must be within range (-24:00 - 24:00)."); int hours; int minutes; absOffset.split!("hours", "minutes")(hours, minutes); formattedWrite( writer, utcOffset < Duration.zero ? "-%02d:%02d" : "+%02d:%02d", hours, minutes ); } @safe unittest { static string testSTZInvalid(Duration offset) { return SimpleTimeZone.toISOExtString(offset); } import std.datetime.date : DateTimeException; assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(1440))); assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(-1440))); assert(toISOExtString(dur!"minutes"(0)) == "+00:00"); assert(toISOExtString(dur!"minutes"(1)) == "+00:01"); assert(toISOExtString(dur!"minutes"(10)) == "+00:10"); assert(toISOExtString(dur!"minutes"(59)) == "+00:59"); assert(toISOExtString(dur!"minutes"(60)) == "+01:00"); assert(toISOExtString(dur!"minutes"(90)) == "+01:30"); assert(toISOExtString(dur!"minutes"(120)) == "+02:00"); assert(toISOExtString(dur!"minutes"(480)) == "+08:00"); assert(toISOExtString(dur!"minutes"(1439)) == "+23:59"); assert(toISOExtString(dur!"minutes"(-1)) == "-00:01"); assert(toISOExtString(dur!"minutes"(-10)) == "-00:10"); assert(toISOExtString(dur!"minutes"(-59)) == "-00:59"); assert(toISOExtString(dur!"minutes"(-60)) == "-01:00"); assert(toISOExtString(dur!"minutes"(-90)) == "-01:30"); assert(toISOExtString(dur!"minutes"(-120)) == "-02:00"); assert(toISOExtString(dur!"minutes"(-480)) == "-08:00"); assert(toISOExtString(dur!"minutes"(-1439)) == "-23:59"); } /+ Takes a time zone as a string with an offset from UTC and returns a $(LREF SimpleTimeZone) which matches. The accepted formats for time zone offsets are +HH, -HH, +HHMM, and -HHMM. Params: isoString = A string which represents a time zone in the ISO format. +/ static immutable(SimpleTimeZone) fromISOString(S)(S isoString) @safe pure if (isSomeString!S) { import std.algorithm.searching : startsWith; import std.conv : text, to, ConvException; import std.datetime.date : DateTimeException; import std.exception : enforce; auto whichSign = isoString.startsWith('-', '+'); enforce!DateTimeException(whichSign > 0, text("Invalid ISO String ", isoString)); isoString = isoString[1 .. $]; auto sign = whichSign == 1 ? -1 : 1; int hours; int minutes; try { // cast to int from uint is used because it checks for // non digits without extra loops if (isoString.length == 2) { hours = cast(int) to!uint(isoString); } else if (isoString.length == 4) { hours = cast(int) to!uint(isoString[0 .. 2]); minutes = cast(int) to!uint(isoString[2 .. 4]); } else { throw new DateTimeException(text("Invalid ISO String ", isoString)); } } catch (ConvException) { throw new DateTimeException(text("Invalid ISO String ", isoString)); } enforce!DateTimeException(hours < 24 && minutes < 60, text("Invalid ISO String ", isoString)); return new immutable SimpleTimeZone(sign * (dur!"hours"(hours) + dur!"minutes"(minutes))); } @safe unittest { import core.exception : AssertError; import std.format : format; foreach (str; ["", "Z", "-", "+", "-:", "+:", "-1:", "+1:", "+1", "-1", "-24:00", "+24:00", "-24", "+24", "-2400", "+2400", "1", "+1", "-1", "+9", "-9", "+1:0", "+01:0", "+1:00", "+01:000", "+01:60", "-1:0", "-01:0", "-1:00", "-01:000", "-01:60", "000", "00000", "0160", "-0160", " +08:00", "+ 08:00", "+08 :00", "+08: 00", "+08:00 ", " -08:00", "- 08:00", "-08 :00", "-08: 00", "-08:00 ", " +0800", "+ 0800", "+08 00", "+08 00", "+0800 ", " -0800", "- 0800", "-08 00", "-08 00", "-0800 ", "+ab:cd", "+abcd", "+0Z:00", "+Z", "+00Z", "-ab:cd", "+abcd", "-0Z:00", "-Z", "-00Z", "01:00", "12:00", "23:59"]) { import std.datetime.date : DateTimeException; assertThrown!DateTimeException(SimpleTimeZone.fromISOString(str), format("[%s]", str)); } static void test(string str, Duration utcOffset, size_t line = __LINE__) { if (SimpleTimeZone.fromISOString(str).utcOffset != (new immutable SimpleTimeZone(utcOffset)).utcOffset) throw new AssertError("unittest failure", __FILE__, line); } test("+0000", Duration.zero); test("+0001", minutes(1)); test("+0010", minutes(10)); test("+0059", minutes(59)); test("+0100", hours(1)); test("+0130", hours(1) + minutes(30)); test("+0200", hours(2)); test("+0800", hours(8)); test("+2359", hours(23) + minutes(59)); test("-0001", minutes(-1)); test("-0010", minutes(-10)); test("-0059", minutes(-59)); test("-0100", hours(-1)); test("-0130", hours(-1) - minutes(30)); test("-0200", hours(-2)); test("-0800", hours(-8)); test("-2359", hours(-23) - minutes(59)); test("+00", Duration.zero); test("+01", hours(1)); test("+02", hours(2)); test("+12", hours(12)); test("+23", hours(23)); test("-00", Duration.zero); test("-01", hours(-1)); test("-02", hours(-2)); test("-12", hours(-12)); test("-23", hours(-23)); } @safe unittest { import core.exception : AssertError; import std.format : format; static void test(scope const string isoString, int expectedOffset, size_t line = __LINE__) { auto stz = SimpleTimeZone.fromISOExtString(isoString); if (stz.utcOffset != dur!"minutes"(expectedOffset)) throw new AssertError(format("unittest failure: wrong offset [%s]", stz.utcOffset), __FILE__, line); auto result = SimpleTimeZone.toISOExtString(stz.utcOffset); if (result != isoString) throw new AssertError(format("unittest failure: [%s] != [%s]", result, isoString), __FILE__, line); } test("+00:00", 0); test("+00:01", 1); test("+00:10", 10); test("+00:59", 59); test("+01:00", 60); test("+01:30", 90); test("+02:00", 120); test("+08:00", 480); test("+08:00", 480); test("+23:59", 1439); test("-00:01", -1); test("-00:10", -10); test("-00:59", -59); test("-01:00", -60); test("-01:30", -90); test("-02:00", -120); test("-08:00", -480); test("-08:00", -480); test("-23:59", -1439); } /+ Takes a time zone as a string with an offset from UTC and returns a $(LREF SimpleTimeZone) which matches. The accepted formats for time zone offsets are +HH, -HH, +HH:MM, and -HH:MM. Params: isoExtString = A string which represents a time zone in the ISO format. +/ static immutable(SimpleTimeZone) fromISOExtString(S)(scope S isoExtString) @safe pure if (isSomeString!S) { import std.algorithm.searching : startsWith; import std.conv : ConvException, to; import std.datetime.date : DateTimeException; import std.exception : enforce; import std.format : format; import std.string : indexOf; auto whichSign = isoExtString.startsWith('-', '+'); enforce!DateTimeException(whichSign > 0, format("Invalid ISO String: %s", isoExtString)); auto sign = whichSign == 1 ? -1 : 1; isoExtString = isoExtString[1 .. $]; enforce!DateTimeException(!isoExtString.empty, format("Invalid ISO String: %s", isoExtString)); immutable colon = isoExtString.indexOf(':'); S hoursStr; S minutesStr; int hours, minutes; if (colon != -1) { hoursStr = isoExtString[0 .. colon]; minutesStr = isoExtString[colon + 1 .. $]; enforce!DateTimeException(minutesStr.length == 2, format("Invalid ISO String: %s", isoExtString)); } else { hoursStr = isoExtString; } enforce!DateTimeException(hoursStr.length == 2, format("Invalid ISO String: %s", isoExtString)); try { // cast to int from uint is used because it checks for // non digits without extra loops hours = cast(int) to!uint(hoursStr); minutes = cast(int) (minutesStr.empty ? 0 : to!uint(minutesStr)); } catch (ConvException) { throw new DateTimeException(format("Invalid ISO String: %s", isoExtString)); } enforce!DateTimeException(hours < 24 && minutes < 60, format("Invalid ISO String: %s", isoExtString)); return new immutable SimpleTimeZone(sign * (dur!"hours"(hours) + dur!"minutes"(minutes))); } @safe unittest { import core.exception : AssertError; import std.format : format; foreach (str; ["", "Z", "-", "+", "-:", "+:", "-1:", "+1:", "+1", "-1", "-24:00", "+24:00", "-24", "+24", "-2400", "-2400", "1", "+1", "-1", "+9", "-9", "+1:0", "+01:0", "+1:00", "+01:000", "+01:60", "-1:0", "-01:0", "-1:00", "-01:000", "-01:60", "000", "00000", "0160", "-0160", " +08:00", "+ 08:00", "+08 :00", "+08: 00", "+08:00 ", " -08:00", "- 08:00", "-08 :00", "-08: 00", "-08:00 ", " +0800", "+ 0800", "+08 00", "+08 00", "+0800 ", " -0800", "- 0800", "-08 00", "-08 00", "-0800 ", "+ab:cd", "abcd", "+0Z:00", "+Z", "+00Z", "-ab:cd", "abcd", "-0Z:00", "-Z", "-00Z", "0100", "1200", "2359"]) { import std.datetime.date : DateTimeException; assertThrown!DateTimeException(SimpleTimeZone.fromISOExtString(str), format("[%s]", str)); } static void test(string str, Duration utcOffset, size_t line = __LINE__) { if (SimpleTimeZone.fromISOExtString(str).utcOffset != (new immutable SimpleTimeZone(utcOffset)).utcOffset) throw new AssertError("unittest failure", __FILE__, line); } test("+00:00", Duration.zero); test("+00:01", minutes(1)); test("+00:10", minutes(10)); test("+00:59", minutes(59)); test("+01:00", hours(1)); test("+01:30", hours(1) + minutes(30)); test("+02:00", hours(2)); test("+08:00", hours(8)); test("+23:59", hours(23) + minutes(59)); test("-00:01", minutes(-1)); test("-00:10", minutes(-10)); test("-00:59", minutes(-59)); test("-01:00", hours(-1)); test("-01:30", hours(-1) - minutes(30)); test("-02:00", hours(-2)); test("-08:00", hours(-8)); test("-23:59", hours(-23) - minutes(59)); test("+00", Duration.zero); test("+01", hours(1)); test("+02", hours(2)); test("+12", hours(12)); test("+23", hours(23)); test("-00", Duration.zero); test("-01", hours(-1)); test("-02", hours(-2)); test("-12", hours(-12)); test("-23", hours(-23)); } @safe unittest { import core.exception : AssertError; import std.format : format; static void test(scope const string isoExtString, int expectedOffset, size_t line = __LINE__) { auto stz = SimpleTimeZone.fromISOExtString(isoExtString); if (stz.utcOffset != dur!"minutes"(expectedOffset)) throw new AssertError(format("unittest failure: wrong offset [%s]", stz.utcOffset), __FILE__, line); auto result = SimpleTimeZone.toISOExtString(stz.utcOffset); if (result != isoExtString) throw new AssertError(format("unittest failure: [%s] != [%s]", result, isoExtString), __FILE__, line); } test("+00:00", 0); test("+00:01", 1); test("+00:10", 10); test("+00:59", 59); test("+01:00", 60); test("+01:30", 90); test("+02:00", 120); test("+08:00", 480); test("+08:00", 480); test("+23:59", 1439); test("-00:01", -1); test("-00:10", -10); test("-00:59", -59); test("-01:00", -60); test("-01:30", -90); test("-02:00", -120); test("-08:00", -480); test("-08:00", -480); test("-23:59", -1439); } private: immutable Duration _utcOffset; } /++ Represents a time zone from a TZ Database time zone file. Files from the TZ Database are how Posix systems hold their time zone information. Unfortunately, Windows does not use the TZ Database. To use the TZ Database, use `PosixTimeZone` (which reads its information from the TZ Database files on disk) on Windows by providing the TZ Database files and telling `PosixTimeZone.getTimeZone` where the directory holding them is. To get a `PosixTimeZone`, call `PosixTimeZone.getTimeZone` (which allows specifying the location the time zone files). Note: Unless your system's local time zone deals with leap seconds (which is highly unlikely), then the only way to get a time zone which takes leap seconds into account is to use `PosixTimeZone` with a time zone whose name starts with "right/". Those time zone files do include leap seconds, and `PosixTimeZone` will take them into account (though posix systems which use a "right/" time zone as their local time zone will $(I not) take leap seconds into account even though they're in the file). See_Also: $(HTTP www.iana.org/time-zones, Home of the TZ Database files)
$(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ Database)
$(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of Time Zones) +/ final class PosixTimeZone : TimeZone { import std.algorithm.searching : countUntil, canFind, startsWith; import std.file : isDir, isFile, exists, dirEntries, SpanMode, DirEntry; import std.path : extension; import std.stdio : File; import std.string : strip, representation; import std.traits : isArray, isSomeChar; public: /++ Whether this time zone has Daylight Savings Time at any point in time. Note that for some time zone types it may not have DST for current dates but will still return true for `hasDST` because the time zone did at some point have DST. +/ @property override bool hasDST() @safe const nothrow { return _hasDST; } /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and returns whether DST is in effect in this time zone at the given point in time. Params: stdTime = The UTC time that needs to be checked for DST in this time zone. +/ override bool dstInEffect(long stdTime) @safe const scope nothrow { assert(!_transitions.empty); immutable unixTime = stdTimeToUnixTime(stdTime); immutable found = countUntil!"b < a.timeT"(_transitions, unixTime); if (found == -1) return _transitions.back.ttInfo.isDST; immutable transition = found == 0 ? _transitions[0] : _transitions[found - 1]; return transition.ttInfo.isDST; } /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and converts it to this time zone's time. Params: stdTime = The UTC time that needs to be adjusted to this time zone's time. +/ override long utcToTZ(long stdTime) @safe const scope nothrow { assert(!_transitions.empty); immutable leapSecs = calculateLeapSeconds(stdTime); immutable unixTime = stdTimeToUnixTime(stdTime); immutable found = countUntil!"b < a.timeT"(_transitions, unixTime); if (found == -1) return stdTime + convert!("seconds", "hnsecs")(_transitions.back.ttInfo.utcOffset + leapSecs); immutable transition = found == 0 ? _transitions[0] : _transitions[found - 1]; return stdTime + convert!("seconds", "hnsecs")(transition.ttInfo.utcOffset + leapSecs); } /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in this time zone's time and converts it to UTC (i.e. std time). Params: adjTime = The time in this time zone that needs to be adjusted to UTC time. +/ override long tzToUTC(long adjTime) @safe const scope nothrow { assert(!_transitions.empty, "UTC offset's not available"); immutable leapSecs = calculateLeapSeconds(adjTime); time_t unixTime = stdTimeToUnixTime(adjTime); immutable past = unixTime - convert!("days", "seconds")(1); immutable future = unixTime + convert!("days", "seconds")(1); immutable pastFound = countUntil!"b < a.timeT"(_transitions, past); if (pastFound == -1) return adjTime - convert!("seconds", "hnsecs")(_transitions.back.ttInfo.utcOffset + leapSecs); immutable futureFound = countUntil!"b < a.timeT"(_transitions[pastFound .. $], future); immutable pastTrans = pastFound == 0 ? _transitions[0] : _transitions[pastFound - 1]; if (futureFound == 0) return adjTime - convert!("seconds", "hnsecs")(pastTrans.ttInfo.utcOffset + leapSecs); immutable futureTrans = futureFound == -1 ? _transitions.back : _transitions[pastFound + futureFound - 1]; immutable pastOffset = pastTrans.ttInfo.utcOffset; if (pastOffset < futureTrans.ttInfo.utcOffset) unixTime -= convert!("hours", "seconds")(1); immutable found = countUntil!"b < a.timeT"(_transitions[pastFound .. $], unixTime - pastOffset); if (found == -1) return adjTime - convert!("seconds", "hnsecs")(_transitions.back.ttInfo.utcOffset + leapSecs); immutable transition = found == 0 ? pastTrans : _transitions[pastFound + found - 1]; return adjTime - convert!("seconds", "hnsecs")(transition.ttInfo.utcOffset + leapSecs); } version (StdDdoc) { /++ The default directory where the TZ Database files are stored. It's empty for Windows, since Windows doesn't have them. You can also use the TZDatabaseDir version to pass an arbitrary path at compile-time, rather than hard-coding it here. Android concatenates all time zone data into a single file called tzdata and stores it in the directory below. +/ enum defaultTZDatabaseDir = ""; } else version (TZDatabaseDir) { import std.string : strip; enum defaultTZDatabaseDir = strip(import("TZDatabaseDirFile")); } else version (Android) { enum defaultTZDatabaseDir = "/system/usr/share/zoneinfo/"; } else version (Solaris) { enum defaultTZDatabaseDir = "/usr/share/lib/zoneinfo/"; } else version (Posix) { enum defaultTZDatabaseDir = "/usr/share/zoneinfo/"; } else version (Windows) { enum defaultTZDatabaseDir = ""; } /++ Returns a $(LREF TimeZone) with the give name per the TZ Database. The time zone information is fetched from the TZ Database time zone files in the given directory. See_Also: $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ Database)
$(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of Time Zones) Params: name = The TZ Database name of the desired time zone tzDatabaseDir = The directory where the TZ Database files are located. Because these files are not located on Windows systems, provide them and give their location here to use $(LREF PosixTimeZone)s. Throws: $(REF DateTimeException,std,datetime,date) if the given time zone could not be found or `FileException` if the TZ Database file could not be opened. +/ // TODO make it possible for tzDatabaseDir to be gzipped tar file rather than an uncompressed // directory. static immutable(PosixTimeZone) getTimeZone(string name, string tzDatabaseDir = defaultTZDatabaseDir) @trusted { import std.algorithm.sorting : sort; import std.conv : to; import std.datetime.date : DateTimeException; import std.exception : enforce; import std.format : format; import std.path : asNormalizedPath, chainPath; import std.range : retro; name = strip(name); enforce(tzDatabaseDir.exists(), new DateTimeException(format("Directory %s does not exist.", tzDatabaseDir))); enforce(tzDatabaseDir.isDir, new DateTimeException(format("%s is not a directory.", tzDatabaseDir))); version (Android) { auto tzfileOffset = name in tzdataIndex(tzDatabaseDir); enforce(tzfileOffset, new DateTimeException(format("The time zone %s is not listed.", name))); string tzFilename = separate_index ? "zoneinfo.dat" : "tzdata"; const file = asNormalizedPath(chainPath(tzDatabaseDir, tzFilename)).to!string; } else const file = asNormalizedPath(chainPath(tzDatabaseDir, name)).to!string; enforce(file.exists(), new DateTimeException(format("File %s does not exist.", file))); enforce(file.isFile, new DateTimeException(format("%s is not a file.", file))); auto tzFile = File(file); version (Android) tzFile.seek(*tzfileOffset); immutable gmtZone = name.representation().canFind("GMT"); import std.datetime.date : DateTimeException; try { _enforceValidTZFile(readVal!(char[])(tzFile, 4) == "TZif"); immutable char tzFileVersion = readVal!char(tzFile); _enforceValidTZFile(tzFileVersion == '\0' || tzFileVersion == '2' || tzFileVersion == '3'); { auto zeroBlock = readVal!(ubyte[])(tzFile, 15); bool allZeroes = true; foreach (val; zeroBlock) { if (val != 0) { allZeroes = false; break; } } _enforceValidTZFile(allZeroes); } // The number of UTC/local indicators stored in the file. auto tzh_ttisgmtcnt = readVal!int(tzFile); // The number of standard/wall indicators stored in the file. auto tzh_ttisstdcnt = readVal!int(tzFile); // The number of leap seconds for which data is stored in the file. auto tzh_leapcnt = readVal!int(tzFile); // The number of "transition times" for which data is stored in the file. auto tzh_timecnt = readVal!int(tzFile); // The number of "local time types" for which data is stored in the file (must not be zero). auto tzh_typecnt = readVal!int(tzFile); _enforceValidTZFile(tzh_typecnt != 0); // The number of characters of "timezone abbreviation strings" stored in the file. auto tzh_charcnt = readVal!int(tzFile); // time_ts where DST transitions occur. auto transitionTimeTs = new long[](tzh_timecnt); foreach (ref transition; transitionTimeTs) transition = readVal!int(tzFile); // Indices into ttinfo structs indicating the changes // to be made at the corresponding DST transition. auto ttInfoIndices = new ubyte[](tzh_timecnt); foreach (ref ttInfoIndex; ttInfoIndices) ttInfoIndex = readVal!ubyte(tzFile); // ttinfos which give info on DST transitions. auto tempTTInfos = new TempTTInfo[](tzh_typecnt); foreach (ref ttInfo; tempTTInfos) ttInfo = readVal!TempTTInfo(tzFile); // The array of time zone abbreviation characters. auto tzAbbrevChars = readVal!(char[])(tzFile, tzh_charcnt); auto leapSeconds = new LeapSecond[](tzh_leapcnt); foreach (ref leapSecond; leapSeconds) { // The time_t when the leap second occurs. auto timeT = readVal!int(tzFile); // The total number of leap seconds to be applied after // the corresponding leap second. auto total = readVal!int(tzFile); leapSecond = LeapSecond(timeT, total); } // Indicate whether each corresponding DST transition were specified // in standard time or wall clock time. auto transitionIsStd = new bool[](tzh_ttisstdcnt); foreach (ref isStd; transitionIsStd) isStd = readVal!bool(tzFile); // Indicate whether each corresponding DST transition associated with // local time types are specified in UTC or local time. auto transitionInUTC = new bool[](tzh_ttisgmtcnt); foreach (ref inUTC; transitionInUTC) inUTC = readVal!bool(tzFile); _enforceValidTZFile(!tzFile.eof); // If version 2 or 3, the information is duplicated in 64-bit. if (tzFileVersion == '2' || tzFileVersion == '3') { _enforceValidTZFile(readVal!(char[])(tzFile, 4) == "TZif"); immutable char tzFileVersion2 = readVal!(char)(tzFile); _enforceValidTZFile(tzFileVersion2 == '2' || tzFileVersion2 == '3'); { auto zeroBlock = readVal!(ubyte[])(tzFile, 15); bool allZeroes = true; foreach (val; zeroBlock) { if (val != 0) { allZeroes = false; break; } } _enforceValidTZFile(allZeroes); } // The number of UTC/local indicators stored in the file. tzh_ttisgmtcnt = readVal!int(tzFile); // The number of standard/wall indicators stored in the file. tzh_ttisstdcnt = readVal!int(tzFile); // The number of leap seconds for which data is stored in the file. tzh_leapcnt = readVal!int(tzFile); // The number of "transition times" for which data is stored in the file. tzh_timecnt = readVal!int(tzFile); // The number of "local time types" for which data is stored in the file (must not be zero). tzh_typecnt = readVal!int(tzFile); _enforceValidTZFile(tzh_typecnt != 0); // The number of characters of "timezone abbreviation strings" stored in the file. tzh_charcnt = readVal!int(tzFile); // time_ts where DST transitions occur. transitionTimeTs = new long[](tzh_timecnt); foreach (ref transition; transitionTimeTs) transition = readVal!long(tzFile); // Indices into ttinfo structs indicating the changes // to be made at the corresponding DST transition. ttInfoIndices = new ubyte[](tzh_timecnt); foreach (ref ttInfoIndex; ttInfoIndices) ttInfoIndex = readVal!ubyte(tzFile); // ttinfos which give info on DST transitions. tempTTInfos = new TempTTInfo[](tzh_typecnt); foreach (ref ttInfo; tempTTInfos) ttInfo = readVal!TempTTInfo(tzFile); // The array of time zone abbreviation characters. tzAbbrevChars = readVal!(char[])(tzFile, tzh_charcnt); leapSeconds = new LeapSecond[](tzh_leapcnt); foreach (ref leapSecond; leapSeconds) { // The time_t when the leap second occurs. auto timeT = readVal!long(tzFile); // The total number of leap seconds to be applied after // the corresponding leap second. auto total = readVal!int(tzFile); leapSecond = LeapSecond(timeT, total); } // Indicate whether each corresponding DST transition were specified // in standard time or wall clock time. transitionIsStd = new bool[](tzh_ttisstdcnt); foreach (ref isStd; transitionIsStd) isStd = readVal!bool(tzFile); // Indicate whether each corresponding DST transition associated with // local time types are specified in UTC or local time. transitionInUTC = new bool[](tzh_ttisgmtcnt); foreach (ref inUTC; transitionInUTC) inUTC = readVal!bool(tzFile); } _enforceValidTZFile(tzFile.readln().strip().empty); cast(void) tzFile.readln(); version (Android) { // Android uses a single file for all timezone data, so the file // doesn't end here. } else { _enforceValidTZFile(tzFile.readln().strip().empty); _enforceValidTZFile(tzFile.eof); } auto transitionTypes = new TransitionType*[](tempTTInfos.length); foreach (i, ref ttype; transitionTypes) { bool isStd = false; if (i < transitionIsStd.length && !transitionIsStd.empty) isStd = transitionIsStd[i]; bool inUTC = false; if (i < transitionInUTC.length && !transitionInUTC.empty) inUTC = transitionInUTC[i]; ttype = new TransitionType(isStd, inUTC); } auto ttInfos = new immutable(TTInfo)*[](tempTTInfos.length); foreach (i, ref ttInfo; ttInfos) { auto tempTTInfo = tempTTInfos[i]; if (gmtZone) tempTTInfo.tt_gmtoff = -tempTTInfo.tt_gmtoff; auto abbrevChars = tzAbbrevChars[tempTTInfo.tt_abbrind .. $]; string abbrev = abbrevChars[0 .. abbrevChars.countUntil('\0')].idup; ttInfo = new immutable(TTInfo)(tempTTInfos[i], abbrev); } auto tempTransitions = new TempTransition[](transitionTimeTs.length); foreach (i, ref tempTransition; tempTransitions) { immutable ttiIndex = ttInfoIndices[i]; auto transitionTimeT = transitionTimeTs[i]; auto ttype = transitionTypes[ttiIndex]; auto ttInfo = ttInfos[ttiIndex]; tempTransition = TempTransition(transitionTimeT, ttInfo, ttype); } if (tempTransitions.empty) { _enforceValidTZFile(ttInfos.length == 1 && transitionTypes.length == 1); tempTransitions ~= TempTransition(0, ttInfos[0], transitionTypes[0]); } sort!"a.timeT < b.timeT"(tempTransitions); sort!"a.timeT < b.timeT"(leapSeconds); auto transitions = new Transition[](tempTransitions.length); foreach (i, ref transition; transitions) { auto tempTransition = tempTransitions[i]; auto transitionTimeT = tempTransition.timeT; auto ttInfo = tempTransition.ttInfo; _enforceValidTZFile(i == 0 || transitionTimeT > tempTransitions[i - 1].timeT); transition = Transition(transitionTimeT, ttInfo); } string stdName; string dstName; bool hasDST = false; foreach (transition; retro(transitions)) { auto ttInfo = transition.ttInfo; if (ttInfo.isDST) { if (dstName.empty) dstName = ttInfo.abbrev; hasDST = true; } else { if (stdName.empty) stdName = ttInfo.abbrev; } if (!stdName.empty && !dstName.empty) break; } return new immutable PosixTimeZone(transitions.idup, leapSeconds.idup, name, stdName, dstName, hasDST); } catch (DateTimeException dte) throw dte; catch (Exception e) throw new DateTimeException("Not a valid TZ data file", __FILE__, __LINE__, e); } /// @safe unittest { version (Posix) { auto tz = PosixTimeZone.getTimeZone("America/Los_Angeles"); assert(tz.name == "America/Los_Angeles"); assert(tz.stdName == "PST"); assert(tz.dstName == "PDT"); } } /++ Returns a list of the names of the time zones installed on the system. Providing a sub-name narrows down the list of time zones (which can number in the thousands). For example, passing in "America" as the sub-name returns only the time zones which begin with "America". Params: subName = The first part of the desired time zones. tzDatabaseDir = The directory where the TZ Database files are located. Throws: `FileException` if it fails to read from disk. +/ static string[] getInstalledTZNames(string subName = "", string tzDatabaseDir = defaultTZDatabaseDir) @safe { import std.algorithm.sorting : sort; import std.array : appender; import std.exception : enforce; import std.format : format; version (Posix) subName = strip(subName); else version (Windows) { import std.array : replace; import std.path : dirSeparator; subName = replace(strip(subName), "/", dirSeparator); } import std.datetime.date : DateTimeException; enforce(tzDatabaseDir.exists(), new DateTimeException(format("Directory %s does not exist.", tzDatabaseDir))); enforce(tzDatabaseDir.isDir, new DateTimeException(format("%s is not a directory.", tzDatabaseDir))); auto timezones = appender!(string[])(); version (Android) { import std.algorithm.iteration : filter; import std.algorithm.mutation : copy; const index = () @trusted { return tzdataIndex(tzDatabaseDir); }(); index.byKey.filter!(a => a.startsWith(subName)).copy(timezones); } else { import std.path : baseName; // dirEntries is @system because it uses a DirIterator with a // RefCounted variable, but here, no references to the payload is // escaped to the outside, so this should be @trusted () @trusted { foreach (DirEntry de; dirEntries(tzDatabaseDir, SpanMode.depth)) { if (de.isFile) { auto tzName = de.name[tzDatabaseDir.length .. $]; if (!tzName.extension().empty || !tzName.startsWith(subName) || baseName(tzName) == "leapseconds" || tzName == "+VERSION" || tzName == "SECURITY") { continue; } timezones.put(tzName); } } }(); } sort(timezones.data); return timezones.data; } version (Posix) @system unittest { import std.exception : assertNotThrown; import std.stdio : writefln; static void testPTZSuccess(string tzName) { scope(failure) writefln("TZName which threw: %s", tzName); PosixTimeZone.getTimeZone(tzName); } static void testPTZFailure(string tzName) { scope(success) writefln("TZName which was supposed to throw: %s", tzName); PosixTimeZone.getTimeZone(tzName); } auto tzNames = getInstalledTZNames(); import std.datetime.date : DateTimeException; foreach (tzName; tzNames) assertNotThrown!DateTimeException(testPTZSuccess(tzName)); // No timezone directories on Android, just a single tzdata file version (Android) {} else { foreach (DirEntry de; dirEntries(defaultTZDatabaseDir, SpanMode.depth)) { if (de.isFile) { auto tzName = de.name[defaultTZDatabaseDir.length .. $]; if (!canFind(tzNames, tzName)) assertThrown!DateTimeException(testPTZFailure(tzName)); } } } } private: /+ Holds information on when a time transition occures (usually a transition to or from DST) as well as a pointer to the `TTInfo` which holds information on the utc offset past the transition. +/ struct Transition { this(long timeT, immutable (TTInfo)* ttInfo) @safe pure { this.timeT = timeT; this.ttInfo = ttInfo; } long timeT; immutable (TTInfo)* ttInfo; } /+ Holds information on when a leap second occurs. +/ struct LeapSecond { this(long timeT, int total) @safe pure { this.timeT = timeT; this.total = total; } long timeT; int total; } /+ Holds information on the utc offset after a transition as well as whether DST is in effect after that transition. +/ struct TTInfo { this(scope const TempTTInfo tempTTInfo, string abbrev) @safe immutable pure { utcOffset = tempTTInfo.tt_gmtoff; isDST = tempTTInfo.tt_isdst; this.abbrev = abbrev; } immutable int utcOffset; // Offset from UTC. immutable bool isDST; // Whether DST is in effect. immutable string abbrev; // The current abbreviation for the time zone. } /+ Struct used to hold information relating to `TTInfo` while organizing the time zone information prior to putting it in its final form. +/ struct TempTTInfo { this(int gmtOff, bool isDST, ubyte abbrInd) @safe pure { tt_gmtoff = gmtOff; tt_isdst = isDST; tt_abbrind = abbrInd; } int tt_gmtoff; bool tt_isdst; ubyte tt_abbrind; } /+ Struct used to hold information relating to `Transition` while organizing the time zone information prior to putting it in its final form. +/ struct TempTransition { this(long timeT, immutable (TTInfo)* ttInfo, TransitionType* ttype) @safe pure { this.timeT = timeT; this.ttInfo = ttInfo; this.ttype = ttype; } long timeT; immutable (TTInfo)* ttInfo; TransitionType* ttype; } /+ Struct used to hold information relating to `Transition` and `TTInfo` while organizing the time zone information prior to putting it in its final form. +/ struct TransitionType { this(bool isStd, bool inUTC) @safe pure { this.isStd = isStd; this.inUTC = inUTC; } // Whether the transition is in std time (as opposed to wall clock time). bool isStd; // Whether the transition is in UTC (as opposed to local time). bool inUTC; } /+ Reads an int from a TZ file. +/ static T readVal(T)(ref File tzFile) @trusted if ((isIntegral!T || isSomeChar!T) || is(immutable T == immutable bool)) { import std.bitmanip : bigEndianToNative; T[1] buff; _enforceValidTZFile(!tzFile.eof); tzFile.rawRead(buff); return bigEndianToNative!T(cast(ubyte[T.sizeof]) buff); } /+ Reads an array of values from a TZ file. +/ static T readVal(T)(ref File tzFile, size_t length) @trusted if (isArray!T) { auto buff = new T(length); _enforceValidTZFile(!tzFile.eof); tzFile.rawRead(buff); return buff; } /+ Reads a `TempTTInfo` from a TZ file. +/ static T readVal(T)(ref File tzFile) @safe if (is(T == TempTTInfo)) { return TempTTInfo(readVal!int(tzFile), readVal!bool(tzFile), readVal!ubyte(tzFile)); } /+ Throws: $(REF DateTimeException,std,datetime,date) if `result` is false. +/ static void _enforceValidTZFile(bool result, size_t line = __LINE__) @safe pure { import std.datetime.date : DateTimeException; if (!result) throw new DateTimeException("Not a valid tzdata file.", __FILE__, line); } int calculateLeapSeconds(long stdTime) @safe const scope pure nothrow { if (_leapSeconds.empty) return 0; immutable unixTime = stdTimeToUnixTime(stdTime); if (_leapSeconds.front.timeT >= unixTime) return 0; immutable found = countUntil!"b < a.timeT"(_leapSeconds, unixTime); if (found == -1) return _leapSeconds.back.total; immutable leapSecond = found == 0 ? _leapSeconds[0] : _leapSeconds[found - 1]; return leapSecond.total; } this(immutable Transition[] transitions, immutable LeapSecond[] leapSeconds, string name, string stdName, string dstName, bool hasDST) @safe immutable pure { if (dstName.empty && !stdName.empty) dstName = stdName; else if (stdName.empty && !dstName.empty) stdName = dstName; super(name, stdName, dstName); if (!transitions.empty) { foreach (i, transition; transitions[0 .. $-1]) _enforceValidTZFile(transition.timeT < transitions[i + 1].timeT); } foreach (i, leapSecond; leapSeconds) _enforceValidTZFile(i == leapSeconds.length - 1 || leapSecond.timeT < leapSeconds[i + 1].timeT); _transitions = transitions; _leapSeconds = leapSeconds; _hasDST = hasDST; } // Android concatenates the usual timezone directories into a single file, // tzdata, along with an index to jump to each timezone's offset. In older // versions of Android, the index was stored in a separate file, zoneinfo.idx, // whereas now it's stored at the beginning of tzdata. version (Android) { // Keep track of whether there's a separate index, zoneinfo.idx. Only // check this after calling tzdataIndex, as it's initialized there. static shared bool separate_index; // Extracts the name of each time zone and the offset where its data is // located in the tzdata file from the index and caches it for later. static const(uint[string]) tzdataIndex(string tzDir) { import std.concurrency : initOnce; __gshared uint[string] _tzIndex; // _tzIndex is initialized once and then shared across all threads. initOnce!_tzIndex( { import std.conv : to; import std.datetime.date : DateTimeException; import std.format : format; import std.path : asNormalizedPath, chainPath; enum indexEntrySize = 52; const combinedFile = asNormalizedPath(chainPath(tzDir, "tzdata")).to!string; const indexFile = asNormalizedPath(chainPath(tzDir, "zoneinfo.idx")).to!string; File tzFile; uint indexEntries, dataOffset; uint[string] initIndex; // Check for the combined file tzdata, which stores the index // and the time zone data together. if (combinedFile.exists() && combinedFile.isFile) { tzFile = File(combinedFile); _enforceValidTZFile(readVal!(char[])(tzFile, 6) == "tzdata"); auto tzDataVersion = readVal!(char[])(tzFile, 6); _enforceValidTZFile(tzDataVersion[5] == '\0'); uint indexOffset = readVal!uint(tzFile); dataOffset = readVal!uint(tzFile); readVal!uint(tzFile); indexEntries = (dataOffset - indexOffset) / indexEntrySize; separate_index = false; } else if (indexFile.exists() && indexFile.isFile) { tzFile = File(indexFile); indexEntries = to!uint(tzFile.size/indexEntrySize); separate_index = true; } else { throw new DateTimeException(format("Both timezone files %s and %s do not exist.", combinedFile, indexFile)); } foreach (_; 0 .. indexEntries) { string tzName = to!string(readVal!(char[])(tzFile, 40).ptr); uint tzOffset = readVal!uint(tzFile); readVal!(uint[])(tzFile, 2); initIndex[tzName] = dataOffset + tzOffset; } initIndex.rehash; return initIndex; }()); return _tzIndex; } } // List of times when the utc offset changes. immutable Transition[] _transitions; // List of leap second occurrences. immutable LeapSecond[] _leapSeconds; // Whether DST is in effect for this time zone at any point in time. immutable bool _hasDST; } version (StdDdoc) { /++ $(BLUE This class is Windows-Only.) Represents a time zone from the Windows registry. Unfortunately, Windows does not use the TZ Database. To use the TZ Database, use $(LREF PosixTimeZone) (which reads its information from the TZ Database files on disk) on Windows by providing the TZ Database files and telling `PosixTimeZone.getTimeZone` where the directory holding them is. The TZ Database files and Windows' time zone information frequently do not match. Windows has many errors with regards to when DST switches occur (especially for historical dates). Also, the TZ Database files include far more time zones than Windows does. So, for accurate time zone information, use the TZ Database files with $(LREF PosixTimeZone) rather than `WindowsTimeZone`. However, because `WindowsTimeZone` uses Windows system calls to deal with the time, it's far more likely to match the behavior of other Windows programs. Be aware of the differences when selecting a method. `WindowsTimeZone` does not exist on Posix systems. To get a `WindowsTimeZone`, call `WindowsTimeZone.getTimeZone`. See_Also: $(HTTP www.iana.org/time-zones, Home of the TZ Database files) +/ final class WindowsTimeZone : TimeZone { public: /++ Whether this time zone has Daylight Savings Time at any point in time. Note that for some time zone types it may not have DST for current dates but will still return true for `hasDST` because the time zone did at some point have DST. +/ @property override bool hasDST() @safe const scope nothrow; /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and returns whether DST is in effect in this time zone at the given point in time. Params: stdTime = The UTC time that needs to be checked for DST in this time zone. +/ override bool dstInEffect(long stdTime) @safe const scope nothrow; /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in UTC time (i.e. std time) and converts it to this time zone's time. Params: stdTime = The UTC time that needs to be adjusted to this time zone's time. +/ override long utcToTZ(long stdTime) @safe const scope nothrow; /++ Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D. in this time zone's time and converts it to UTC (i.e. std time). Params: adjTime = The time in this time zone that needs to be adjusted to UTC time. +/ override long tzToUTC(long adjTime) @safe const scope nothrow; /++ Returns a $(LREF TimeZone) with the given name per the Windows time zone names. The time zone information is fetched from the Windows registry. See_Also: $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ Database)
$(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of Time Zones) Params: name = The TZ Database name of the desired time zone. Throws: $(REF DateTimeException,std,datetime,date) if the given time zone could not be found. Example: -------------------- auto tz = WindowsTimeZone.getTimeZone("Pacific Standard Time"); -------------------- +/ static immutable(WindowsTimeZone) getTimeZone(string name) @safe; /++ Returns a list of the names of the time zones installed on the system. The list returned by WindowsTimeZone contains the Windows TZ names, not the TZ Database names. However, `TimeZone.getinstalledTZNames` will return the TZ Database names which are equivalent to the Windows TZ names. +/ static string[] getInstalledTZNames() @safe; private: version (Windows) {} else alias TIME_ZONE_INFORMATION = void*; static bool _dstInEffect(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime) nothrow; static long _utcToTZ(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime, bool hasDST) nothrow; static long _tzToUTC(const scope TIME_ZONE_INFORMATION* tzInfo, long adjTime, bool hasDST) nothrow; this() immutable pure { super("", "", ""); } } } else version (Windows) { final class WindowsTimeZone : TimeZone { import std.algorithm.sorting : sort; import std.array : appender; import std.conv : to; import std.format : format; public: @property override bool hasDST() @safe const scope nothrow { return _tzInfo.DaylightDate.wMonth != 0; } override bool dstInEffect(long stdTime) @safe const scope nothrow { return _dstInEffect(&_tzInfo, stdTime); } override long utcToTZ(long stdTime) @safe const scope nothrow { return _utcToTZ(&_tzInfo, stdTime, hasDST); } override long tzToUTC(long adjTime) @safe const scope nothrow { return _tzToUTC(&_tzInfo, adjTime, hasDST); } static immutable(WindowsTimeZone) getTimeZone(string name) @trusted { scope baseKey = Registry.localMachine.getKey(`Software\Microsoft\Windows NT\CurrentVersion\Time Zones`); foreach (tzKeyName; baseKey.keyNames) { if (tzKeyName != name) continue; scope tzKey = baseKey.getKey(tzKeyName); scope stdVal = tzKey.getValue("Std"); auto stdName = stdVal.value_SZ; scope dstVal = tzKey.getValue("Dlt"); auto dstName = dstVal.value_SZ; scope tziVal = tzKey.getValue("TZI"); auto binVal = tziVal.value_BINARY; assert(binVal.length == REG_TZI_FORMAT.sizeof, "Unexpected size while getTimeZone with name " ~ name); auto tziFmt = cast(REG_TZI_FORMAT*) binVal.ptr; TIME_ZONE_INFORMATION tzInfo; auto wstdName = stdName.to!wstring; auto wdstName = dstName.to!wstring; auto wstdNameLen = wstdName.length > 32 ? 32 : wstdName.length; auto wdstNameLen = wdstName.length > 32 ? 32 : wdstName.length; tzInfo.Bias = tziFmt.Bias; tzInfo.StandardName[0 .. wstdNameLen] = wstdName[0 .. wstdNameLen]; tzInfo.StandardName[wstdNameLen .. $] = '\0'; tzInfo.StandardDate = tziFmt.StandardDate; tzInfo.StandardBias = tziFmt.StandardBias; tzInfo.DaylightName[0 .. wdstNameLen] = wdstName[0 .. wdstNameLen]; tzInfo.DaylightName[wdstNameLen .. $] = '\0'; tzInfo.DaylightDate = tziFmt.DaylightDate; tzInfo.DaylightBias = tziFmt.DaylightBias; return new immutable WindowsTimeZone(name, tzInfo); } import std.datetime.date : DateTimeException; throw new DateTimeException(format("Failed to find time zone: %s", name)); } static string[] getInstalledTZNames() @trusted { auto timezones = appender!(string[])(); scope baseKey = Registry.localMachine.getKey(`Software\Microsoft\Windows NT\CurrentVersion\Time Zones`); foreach (tzKeyName; baseKey.keyNames) timezones.put(tzKeyName); sort(timezones.data); return timezones.data; } @safe unittest { import std.exception : assertNotThrown; import std.stdio : writefln; static void testWTZSuccess(string tzName) { scope(failure) writefln("TZName which threw: %s", tzName); WindowsTimeZone.getTimeZone(tzName); } auto tzNames = getInstalledTZNames(); import std.datetime.date : DateTimeException; foreach (tzName; tzNames) assertNotThrown!DateTimeException(testWTZSuccess(tzName)); } private: static bool _dstInEffect(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime) @trusted nothrow { try { if (tzInfo.DaylightDate.wMonth == 0) return false; import std.datetime.date : DateTime, Month; auto utcDateTime = cast(DateTime) SysTime(stdTime, UTC()); //The limits of what SystemTimeToTzSpecificLocalTime will accept. if (utcDateTime.year < 1601) { import std.datetime.date : Month; if (utcDateTime.month == Month.feb && utcDateTime.day == 29) utcDateTime.day = 28; utcDateTime.year = 1601; } else if (utcDateTime.year > 30_827) { if (utcDateTime.month == Month.feb && utcDateTime.day == 29) utcDateTime.day = 28; utcDateTime.year = 30_827; } //SystemTimeToTzSpecificLocalTime doesn't act correctly at the //beginning or end of the year (bleh). Unless some bizarre time //zone changes DST on January 1st or December 31st, this should //fix the problem. if (utcDateTime.month == Month.jan) { if (utcDateTime.day == 1) utcDateTime.day = 2; } else if (utcDateTime.month == Month.dec && utcDateTime.day == 31) utcDateTime.day = 30; SYSTEMTIME utcTime = void; SYSTEMTIME otherTime = void; utcTime.wYear = utcDateTime.year; utcTime.wMonth = utcDateTime.month; utcTime.wDay = utcDateTime.day; utcTime.wHour = utcDateTime.hour; utcTime.wMinute = utcDateTime.minute; utcTime.wSecond = utcDateTime.second; utcTime.wMilliseconds = 0; immutable result = SystemTimeToTzSpecificLocalTime(cast(TIME_ZONE_INFORMATION*) tzInfo, &utcTime, &otherTime); assert(result, "Failed to create SystemTimeToTzSpecificLocalTime"); immutable otherDateTime = DateTime(otherTime.wYear, otherTime.wMonth, otherTime.wDay, otherTime.wHour, otherTime.wMinute, otherTime.wSecond); immutable diff = utcDateTime - otherDateTime; immutable minutes = diff.total!"minutes" - tzInfo.Bias; if (minutes == tzInfo.DaylightBias) return true; assert(minutes == tzInfo.StandardBias, "Unexpected difference"); return false; } catch (Exception e) assert(0, "DateTime's constructor threw."); } @system unittest { TIME_ZONE_INFORMATION tzInfo; GetTimeZoneInformation(&tzInfo); import std.datetime.date : DateTime; foreach (year; [1600, 1601, 30_827, 30_828]) WindowsTimeZone._dstInEffect(&tzInfo, SysTime(DateTime(year, 1, 1)).stdTime); } static long _utcToTZ(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime, bool hasDST) @safe nothrow { if (hasDST && WindowsTimeZone._dstInEffect(tzInfo, stdTime)) return stdTime - convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.DaylightBias); return stdTime - convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.StandardBias); } static long _tzToUTC(const scope TIME_ZONE_INFORMATION* tzInfo, long adjTime, bool hasDST) @trusted nothrow { if (hasDST) { try { import std.datetime.date : DateTime, Month; bool dstInEffectForLocalDateTime(DateTime localDateTime) { // The limits of what SystemTimeToTzSpecificLocalTime will accept. if (localDateTime.year < 1601) { if (localDateTime.month == Month.feb && localDateTime.day == 29) localDateTime.day = 28; localDateTime.year = 1601; } else if (localDateTime.year > 30_827) { if (localDateTime.month == Month.feb && localDateTime.day == 29) localDateTime.day = 28; localDateTime.year = 30_827; } // SystemTimeToTzSpecificLocalTime doesn't act correctly at the // beginning or end of the year (bleh). Unless some bizarre time // zone changes DST on January 1st or December 31st, this should // fix the problem. if (localDateTime.month == Month.jan) { if (localDateTime.day == 1) localDateTime.day = 2; } else if (localDateTime.month == Month.dec && localDateTime.day == 31) localDateTime.day = 30; SYSTEMTIME utcTime = void; SYSTEMTIME localTime = void; localTime.wYear = localDateTime.year; localTime.wMonth = localDateTime.month; localTime.wDay = localDateTime.day; localTime.wHour = localDateTime.hour; localTime.wMinute = localDateTime.minute; localTime.wSecond = localDateTime.second; localTime.wMilliseconds = 0; immutable result = TzSpecificLocalTimeToSystemTime(cast(TIME_ZONE_INFORMATION*) tzInfo, &localTime, &utcTime); assert(result); assert(result, "Failed to create _tzToUTC"); immutable utcDateTime = DateTime(utcTime.wYear, utcTime.wMonth, utcTime.wDay, utcTime.wHour, utcTime.wMinute, utcTime.wSecond); immutable diff = localDateTime - utcDateTime; immutable minutes = -tzInfo.Bias - diff.total!"minutes"; if (minutes == tzInfo.DaylightBias) return true; assert(minutes == tzInfo.StandardBias, "Unexpected difference"); return false; } import std.datetime.date : DateTime; auto localDateTime = cast(DateTime) SysTime(adjTime, UTC()); auto localDateTimeBefore = localDateTime - dur!"hours"(1); auto localDateTimeAfter = localDateTime + dur!"hours"(1); auto dstInEffectNow = dstInEffectForLocalDateTime(localDateTime); auto dstInEffectBefore = dstInEffectForLocalDateTime(localDateTimeBefore); auto dstInEffectAfter = dstInEffectForLocalDateTime(localDateTimeAfter); bool isDST; if (dstInEffectBefore && dstInEffectNow && dstInEffectAfter) isDST = true; else if (!dstInEffectBefore && !dstInEffectNow && !dstInEffectAfter) isDST = false; else if (!dstInEffectBefore && dstInEffectAfter) isDST = false; else if (dstInEffectBefore && !dstInEffectAfter) isDST = dstInEffectNow; else assert(0, "Bad Logic."); if (isDST) return adjTime + convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.DaylightBias); } catch (Exception e) assert(0, "SysTime's constructor threw."); } return adjTime + convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.StandardBias); } this(string name, TIME_ZONE_INFORMATION tzInfo) @trusted immutable pure { super(name, to!string(tzInfo.StandardName.ptr), to!string(tzInfo.DaylightName.ptr)); _tzInfo = tzInfo; } TIME_ZONE_INFORMATION _tzInfo; } } version (StdDdoc) { /++ $(BLUE This function is Posix-Only.) Sets the local time zone on Posix systems with the TZ Database name by setting the TZ environment variable. Unfortunately, there is no way to do it on Windows using the TZ Database name, so this function only exists on Posix systems. +/ void setTZEnvVar(string tzDatabaseName) @safe nothrow; /++ $(BLUE This function is Posix-Only.) Clears the TZ environment variable. +/ void clearTZEnvVar() @safe nothrow; } else version (Posix) { void setTZEnvVar(string tzDatabaseName) @trusted nothrow { import core.stdc.time : tzset; import core.sys.posix.stdlib : setenv; import std.internal.cstring : tempCString; import std.path : asNormalizedPath, chainPath; version (Android) auto value = asNormalizedPath(tzDatabaseName); else auto value = asNormalizedPath(chainPath(PosixTimeZone.defaultTZDatabaseDir, tzDatabaseName)); setenv("TZ", value.tempCString(), 1); tzset(); } void clearTZEnvVar() @trusted nothrow { import core.stdc.time : tzset; import core.sys.posix.stdlib : unsetenv; unsetenv("TZ"); tzset(); } } /++ Provides the conversions between the IANA time zone database time zone names (which POSIX systems use) and the time zone names that Windows uses. Windows uses a different set of time zone names than the IANA time zone database does, and how they correspond to one another changes over time (particularly when Microsoft updates Windows). $(HTTP unicode.org/cldr/data/common/supplemental/windowsZones.xml, windowsZones.xml) provides the current conversions (which may or may not match up with what's on a particular Windows box depending on how up-to-date it is), and parseTZConversions reads in those conversions from windowsZones.xml so that a D program can use those conversions. However, it should be noted that the time zone information on Windows is frequently less accurate than that in the IANA time zone database, and if someone really wants accurate time zone information, they should use the IANA time zone database files with $(LREF PosixTimeZone) on Windows rather than $(LREF WindowsTimeZone), whereas $(LREF WindowsTimeZone) makes more sense when trying to match what Windows will think the time is in a specific time zone. Also, the IANA time zone database has a lot more time zones than Windows does. Params: windowsZonesXMLText = The text from $(HTTP unicode.org/cldr/data/common/supplemental/windowsZones.xml, windowsZones.xml) Throws: Exception if there is an error while parsing the given XML. -------------------- // Parse the conversions from a local file. auto text = std.file.readText("path/to/windowsZones.xml"); auto conversions = parseTZConversions(text); // Alternatively, grab the XML file from the web at runtime // and parse it so that it's guaranteed to be up-to-date, though // that has the downside that the code needs to worry about the // site being down or unicode.org changing the URL. auto url = "http://unicode.org/cldr/data/common/supplemental/windowsZones.xml"; auto conversions2 = parseTZConversions(std.net.curl.get(url)); -------------------- +/ struct TZConversions { /++ The key is the Windows time zone name, and the value is a list of IANA TZ database names which are close (currently only ever one, but it allows for multiple in case it's ever necessary). +/ string[][string] toWindows; /++ The key is the IANA time zone database name, and the value is a list of Windows time zone names which are close (usually only one, but it could be multiple). +/ string[][string] fromWindows; } /++ ditto +/ TZConversions parseTZConversions(string windowsZonesXMLText) @safe pure { // This is a bit hacky, since it doesn't properly read XML, but it avoids // needing to pull in std.xml (which we're theoretically replacing at some // point anyway). import std.algorithm.iteration : uniq; import std.algorithm.searching : find; import std.algorithm.sorting : sort; import std.array : array, split; import std.string : lineSplitter; string[][string] win2Nix; string[][string] nix2Win; immutable f1 = ` line = line.find(f1); if (line.empty) continue; line = line[f1.length .. $]; auto next = line.find('"'); enforce(!next.empty, "Error parsing. Text does not appear to be from windowsZones.xml"); auto win = line[0 .. $ - next.length]; line = next.find(f2); enforce(!line.empty, "Error parsing. Text does not appear to be from windowsZones.xml"); line = line[f2.length .. $]; next = line.find('"'); enforce(!next.empty, "Error parsing. Text does not appear to be from windowsZones.xml"); auto nixes = line[0 .. $ - next.length].split(); if (auto n = win in win2Nix) *n ~= nixes; else win2Nix[win] = nixes; foreach (nix; nixes) { if (auto w = nix in nix2Win) *w ~= win; else nix2Win[nix] = [win]; } } foreach (key, ref value; nix2Win) value = value.sort().uniq().array(); foreach (key, ref value; win2Nix) value = value.sort().uniq().array(); return TZConversions(nix2Win, win2Nix); } @safe unittest { import std.algorithm.comparison : equal; import std.algorithm.iteration : uniq; import std.algorithm.sorting : isSorted; // Reduced text from http://unicode.org/cldr/data/common/supplemental/windowsZones.xml auto sampleFileText = ` `; auto tzConversions = parseTZConversions(sampleFileText); assert(tzConversions.toWindows.length == 15); assert(tzConversions.toWindows["America/Anchorage"] == ["Alaskan Standard Time"]); assert(tzConversions.toWindows["America/Juneau"] == ["Alaskan Standard Time"]); assert(tzConversions.toWindows["America/Nome"] == ["Alaskan Standard Time"]); assert(tzConversions.toWindows["America/Sitka"] == ["Alaskan Standard Time"]); assert(tzConversions.toWindows["America/Yakutat"] == ["Alaskan Standard Time"]); assert(tzConversions.toWindows["Etc/GMT+10"] == ["Hawaiian Standard Time"]); assert(tzConversions.toWindows["Etc/GMT+11"] == ["UTC-11"]); assert(tzConversions.toWindows["Etc/GMT+12"] == ["Dateline Standard Time"]); assert(tzConversions.toWindows["Pacific/Honolulu"] == ["Hawaiian Standard Time"]); assert(tzConversions.toWindows["Pacific/Johnston"] == ["Hawaiian Standard Time"]); assert(tzConversions.toWindows["Pacific/Midway"] == ["UTC-11"]); assert(tzConversions.toWindows["Pacific/Niue"] == ["UTC-11"]); assert(tzConversions.toWindows["Pacific/Pago_Pago"] == ["UTC-11"]); assert(tzConversions.toWindows["Pacific/Rarotonga"] == ["Hawaiian Standard Time"]); assert(tzConversions.toWindows["Pacific/Tahiti"] == ["Hawaiian Standard Time"]); assert(tzConversions.fromWindows.length == 4); assert(tzConversions.fromWindows["Alaskan Standard Time"] == ["America/Anchorage", "America/Juneau", "America/Nome", "America/Sitka", "America/Yakutat"]); assert(tzConversions.fromWindows["Dateline Standard Time"] == ["Etc/GMT+12"]); assert(tzConversions.fromWindows["Hawaiian Standard Time"] == ["Etc/GMT+10", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Rarotonga", "Pacific/Tahiti"]); assert(tzConversions.fromWindows["UTC-11"] == ["Etc/GMT+11", "Pacific/Midway", "Pacific/Niue", "Pacific/Pago_Pago"]); foreach (key, value; tzConversions.fromWindows) { assert(value.isSorted, key); assert(equal(value.uniq(), value), key); } }