| // 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)<br> | |
| $(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.getDefaultTZDatabaseDir(), "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)<br> | |
| $(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)<br> | |
| $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ Database)<br> | |
| $(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. If the TZDIR environment variable is set, it is consulted | |
| before this constant. | |
| +/ | |
| 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 = ""; | |
| } | |
| private static string getDefaultTZDatabaseDir() | |
| { | |
| import core.stdc.stdlib : getenv; | |
| import std.string : fromStringz; | |
| auto dir = getenv("TZDIR"); | |
| if (dir) | |
| return fromStringz(dir).idup; | |
| return 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)<br> | |
| $(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 = getDefaultTZDatabaseDir()) @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 = getDefaultTZDatabaseDir()) @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 | |
| { | |
| string tzDatabaseDir = getDefaultTZDatabaseDir(); | |
| foreach (DirEntry de; dirEntries(tzDatabaseDir, SpanMode.depth)) | |
| { | |
| if (de.isFile) | |
| { | |
| auto tzName = de.name[tzDatabaseDir.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)<br> | |
| $(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 github.com/unicode-org/cldr/blob/main/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 github.com/unicode-org/cldr/blob/main/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 = "https://raw.githubusercontent.com/unicode-org/cldr/main/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 an xml parsing module. | |
| 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 = `<mapZone other="`; | |
| immutable f2 = `type="`; | |
| foreach (line; windowsZonesXMLText.lineSplitter()) | |
| { | |
| import std.exception : enforce; | |
| // Sample line: | |
| // <mapZone other="Canada Central Standard Time" territory="CA" type="America/Regina America/Swift_Current"/> | |
| 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 https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml | |
| auto sampleFileText = | |
| `<?xml version="1.0" encoding="UTF-8" ?> | |
| <!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd"> | |
| <!-- | |
| Copyright © 1991-2013 Unicode, Inc. | |
| CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/) | |
| For terms of use, see http://www.unicode.org/copyright.html | |
| --> | |
| <supplementalData> | |
| <version number="$Revision$"/> | |
| <windowsZones> | |
| <mapTimezones otherVersion="7df0005" typeVersion="2015g"> | |
| <!-- (UTC-12:00) International Date Line West --> | |
| <mapZone other="Dateline Standard Time" territory="001" type="Etc/GMT+12"/> | |
| <mapZone other="Dateline Standard Time" territory="ZZ" type="Etc/GMT+12"/> | |
| <!-- (UTC-11:00) Coordinated Universal Time-11 --> | |
| <mapZone other="UTC-11" territory="001" type="Etc/GMT+11"/> | |
| <mapZone other="UTC-11" territory="AS" type="Pacific/Pago_Pago"/> | |
| <mapZone other="UTC-11" territory="NU" type="Pacific/Niue"/> | |
| <mapZone other="UTC-11" territory="UM" type="Pacific/Midway"/> | |
| <mapZone other="UTC-11" territory="ZZ" type="Etc/GMT+11"/> | |
| <!-- (UTC-10:00) Hawaii --> | |
| <mapZone other="Hawaiian Standard Time" territory="001" type="Pacific/Honolulu"/> | |
| <mapZone other="Hawaiian Standard Time" territory="CK" type="Pacific/Rarotonga"/> | |
| <mapZone other="Hawaiian Standard Time" territory="PF" type="Pacific/Tahiti"/> | |
| <mapZone other="Hawaiian Standard Time" territory="UM" type="Pacific/Johnston"/> | |
| <mapZone other="Hawaiian Standard Time" territory="US" type="Pacific/Honolulu"/> | |
| <mapZone other="Hawaiian Standard Time" territory="ZZ" type="Etc/GMT+10"/> | |
| <!-- (UTC-09:00) Alaska --> | |
| <mapZone other="Alaskan Standard Time" territory="001" type="America/Anchorage"/> | |
| <mapZone other="Alaskan Standard Time" territory="US" ` | |
| ~ `type="America/Anchorage America/Juneau America/Nome America/Sitka America/Yakutat"/> | |
| </mapTimezones> | |
| </windowsZones> | |
| </supplementalData>`; | |
| 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); | |
| } | |
| } |