1 // Written in the D programming language
2 
3 /++
4     License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
5     Authors:   Jonathan M Davis
6     Source:    $(PHOBOSSRC std/datetime/_timezone.d)
7 +/
8 module std.datetime.timezone;
9 
10 import core.time;
11 import std.datetime.date;
12 import std.datetime.systime;
13 import std.exception : enforce;
14 import std.range.primitives;
15 import std.traits : isIntegral, isSomeString, Unqual;
16 
17 version (OSX)
18     version = Darwin;
19 else version (iOS)
20     version = Darwin;
21 else version (TVOS)
22     version = Darwin;
23 else version (WatchOS)
24     version = Darwin;
25 
version(Windows)26 version (Windows)
27 {
28     import core.stdc.time : time_t;
29     import core.sys.windows.windows;
30     import core.sys.windows.winsock2;
31     import std.windows.registry;
32 
33     // Uncomment and run unittests to print missing Windows TZ translations.
34     // Please subscribe to Microsoft Daylight Saving Time & Time Zone Blog
35     // (https://blogs.technet.microsoft.com/dst2007/) if you feel responsible
36     // for updating the translations.
37     // version = UpdateWindowsTZTranslations;
38 }
version(Posix)39 else version (Posix)
40 {
41     import core.sys.posix.signal : timespec;
42     import core.sys.posix.sys.types : time_t;
43 }
44 
45 version (unittest) import std.exception : assertThrown;
46 
47 
48 /++
49     Represents a time zone. It is used with $(REF SysTime,std,datetime,systime)
50     to indicate the time zone of a $(REF SysTime,std,datetime,systime).
51   +/
52 abstract class TimeZone
53 {
54 public:
55 
56     /++
57         The name of the time zone per the TZ Database. This is the name used to
58         get a $(LREF TimeZone) by name with $(D TimeZone.getTimeZone).
59 
60         See_Also:
61             $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ
62               Database)<br>
63             $(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of
64               Time Zones)
65       +/
name()66     @property string name() @safe const nothrow
67     {
68         return _name;
69     }
70 
71 
72     /++
73         Typically, the abbreviation (generally 3 or 4 letters) for the time zone
74         when DST is $(I not) in effect (e.g. PST). It is not necessarily unique.
75 
76         However, on Windows, it may be the unabbreviated name (e.g. Pacific
77         Standard Time). Regardless, it is not the same as name.
78       +/
stdName()79     @property string stdName() @safe const nothrow
80     {
81         return _stdName;
82     }
83 
84 
85     /++
86         Typically, the abbreviation (generally 3 or 4 letters) for the time zone
87         when DST $(I is) in effect (e.g. PDT). It is not necessarily unique.
88 
89         However, on Windows, it may be the unabbreviated name (e.g. Pacific
90         Daylight Time). Regardless, it is not the same as name.
91       +/
dstName()92     @property string dstName() @safe const nothrow
93     {
94         return _dstName;
95     }
96 
97 
98     /++
99         Whether this time zone has Daylight Savings Time at any point in time.
100         Note that for some time zone types it may not have DST for current dates
101         but will still return true for $(D hasDST) because the time zone did at
102         some point have DST.
103       +/
104     @property abstract bool hasDST() @safe const nothrow;
105 
106 
107     /++
108         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
109         in UTC time (i.e. std time) and returns whether DST is effect in this
110         time zone at the given point in time.
111 
112         Params:
113             stdTime = The UTC time that needs to be checked for DST in this time
114                       zone.
115       +/
116     abstract bool dstInEffect(long stdTime) @safe const nothrow;
117 
118 
119     /++
120         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
121         in UTC time (i.e. std time) and converts it to this time zone's time.
122 
123         Params:
124             stdTime = The UTC time that needs to be adjusted to this time zone's
125                       time.
126       +/
127     abstract long utcToTZ(long stdTime) @safe const nothrow;
128 
129 
130     /++
131         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
132         in this time zone's time and converts it to UTC (i.e. std time).
133 
134         Params:
135             adjTime = The time in this time zone that needs to be adjusted to
136                       UTC time.
137       +/
138     abstract long tzToUTC(long adjTime) @safe const nothrow;
139 
140 
141     /++
142         Returns what the offset from UTC is at the given std time.
143         It includes the DST offset in effect at that time (if any).
144 
145         Params:
146             stdTime = The UTC time for which to get the offset from UTC for this
147                       time zone.
148       +/
utcOffsetAt(long stdTime)149     Duration utcOffsetAt(long stdTime) @safe const nothrow
150     {
151         return dur!"hnsecs"(utcToTZ(stdTime) - stdTime);
152     }
153 
154     // The purpose of this is to handle the case where a Windows time zone is
155     // new and exists on an up-to-date Windows box but does not exist on Windows
156     // boxes which have not been properly updated. The "date added" is included
157     // on the theory that we'll be able to remove them at some point in the
158     // the future once enough time has passed, and that way, we know how much
159     // time has passed.
160     private static string _getOldName(string windowsTZName) @safe pure nothrow
161     {
162         switch (windowsTZName)
163         {
164             case "Belarus Standard Time": return "Kaliningrad Standard Time"; // Added 2014-10-08
165             case "Russia Time Zone 10": return "Magadan Standard Time"; // Added 2014-10-08
166             case "Russia Time Zone 11": return "Magadan Standard Time"; // Added 2014-10-08
167             case "Russia Time Zone 3": return "Russian Standard Time"; // Added 2014-10-08
168             default: return null;
169         }
170     }
171 
172     // Since reading in the time zone files could be expensive, most unit tests
173     // are consolidated into this one unittest block which minimizes how often
174     // it reads a time zone file.
175     @system unittest
176     {
177         import core.exception : AssertError;
178         import std.conv : to;
179         import std.file : exists, isFile;
180         import std.format : format;
181         import std.path : chainPath;
182         import std.stdio : writefln;
183         import std.typecons : tuple;
184 
185         version (Posix) alias getTimeZone = PosixTimeZone.getTimeZone;
186         else version (Windows) alias getTimeZone = WindowsTimeZone.getTimeZone;
187 
188         version (Posix) scope(exit) clearTZEnvVar();
189 
190         static immutable(TimeZone) testTZ(string tzName,
191                                           string stdName,
192                                           string dstName,
193                                           Duration utcOffset,
194                                           Duration dstOffset,
195                                           bool north = true)
196         {
197             scope(failure) writefln("Failed time zone: %s", tzName);
198 
version(Posix)199             version (Posix)
200             {
201                 immutable tz = PosixTimeZone.getTimeZone(tzName);
202                 assert(tz.name == tzName);
203             }
version(Windows)204             else version (Windows)
205             {
206                 immutable tz = WindowsTimeZone.getTimeZone(tzName);
207                 assert(tz.name == stdName);
208             }
209 
210             immutable hasDST = dstOffset != Duration.zero;
211 
212             //assert(tz.stdName == stdName);  //Locale-dependent
213             //assert(tz.dstName == dstName);  //Locale-dependent
214             assert(tz.hasDST == hasDST);
215 
216             immutable stdDate = DateTime(2010, north ? 1 : 7, 1, 6, 0, 0);
217             immutable dstDate = DateTime(2010, north ? 7 : 1, 1, 6, 0, 0);
218             auto std = SysTime(stdDate, tz);
219             auto dst = SysTime(dstDate, tz);
220             auto stdUTC = SysTime(stdDate - utcOffset, UTC());
221             auto dstUTC = SysTime(stdDate - utcOffset + dstOffset, UTC());
222 
223             assert(!std.dstInEffect);
224             assert(dst.dstInEffect == hasDST);
225             assert(tz.utcOffsetAt(std.stdTime) == utcOffset);
226             assert(tz.utcOffsetAt(dst.stdTime) == utcOffset + dstOffset);
227 
228             assert(cast(DateTime) std == stdDate);
229             assert(cast(DateTime) dst == dstDate);
230             assert(std == stdUTC);
231 
version(Posix)232             version (Posix)
233             {
234                 setTZEnvVar(tzName);
235 
236                 static void testTM(in SysTime st)
237                 {
238                     import core.stdc.time : localtime, tm;
239                     time_t unixTime = st.toUnixTime();
240                     tm* osTimeInfo = localtime(&unixTime);
241                     tm ourTimeInfo = st.toTM();
242 
243                     assert(ourTimeInfo.tm_sec == osTimeInfo.tm_sec);
244                     assert(ourTimeInfo.tm_min == osTimeInfo.tm_min);
245                     assert(ourTimeInfo.tm_hour == osTimeInfo.tm_hour);
246                     assert(ourTimeInfo.tm_mday == osTimeInfo.tm_mday);
247                     assert(ourTimeInfo.tm_mon == osTimeInfo.tm_mon);
248                     assert(ourTimeInfo.tm_year == osTimeInfo.tm_year);
249                     assert(ourTimeInfo.tm_wday == osTimeInfo.tm_wday);
250                     assert(ourTimeInfo.tm_yday == osTimeInfo.tm_yday);
251                     assert(ourTimeInfo.tm_isdst == osTimeInfo.tm_isdst);
252                     assert(ourTimeInfo.tm_gmtoff == osTimeInfo.tm_gmtoff);
253                     assert(to!string(ourTimeInfo.tm_zone) == to!string(osTimeInfo.tm_zone));
254                 }
255 
256                 testTM(std);
257                 testTM(dst);
258 
259                 // Apparently, right/ does not exist on Mac OS X. I don't know
260                 // whether or not it exists on FreeBSD. It's rather pointless
261                 // normally, since the Posix standard requires that leap seconds
262                 // be ignored, so it does make some sense that right/ wouldn't
263                 // be there, but since PosixTimeZone _does_ use leap seconds if
264                 // the time zone file does, we'll test that functionality if the
265                 // appropriate files exist.
266                 if (chainPath(PosixTimeZone.defaultTZDatabaseDir, "right", tzName).exists)
267                 {
268                     auto leapTZ = PosixTimeZone.getTimeZone("right/" ~ tzName);
269 
270                     assert(leapTZ.name == "right/" ~ tzName);
271                     //assert(leapTZ.stdName == stdName);  //Locale-dependent
272                     //assert(leapTZ.dstName == dstName);  //Locale-dependent
273                     assert(leapTZ.hasDST == hasDST);
274 
275                     auto leapSTD = SysTime(std.stdTime, leapTZ);
276                     auto leapDST = SysTime(dst.stdTime, leapTZ);
277 
278                     assert(!leapSTD.dstInEffect);
279                     assert(leapDST.dstInEffect == hasDST);
280 
281                     assert(leapSTD.stdTime == std.stdTime);
282                     assert(leapDST.stdTime == dst.stdTime);
283 
284                     // Whenever a leap second is added/removed,
285                     // this will have to be adjusted.
286                     //enum leapDiff = convert!("seconds", "hnsecs")(25);
287                     //assert(leapSTD.adjTime - leapDiff == std.adjTime);
288                     //assert(leapDST.adjTime - leapDiff == dst.adjTime);
289                 }
290             }
291 
292             return tz;
293         }
294 
295         auto dstSwitches = [/+America/Los_Angeles+/ tuple(DateTime(2012, 3, 11),  DateTime(2012, 11, 4), 2, 2),
296                             /+America/New_York+/    tuple(DateTime(2012, 3, 11),  DateTime(2012, 11, 4), 2, 2),
297                             ///+America/Santiago+/    tuple(DateTime(2011, 8, 21),  DateTime(2011, 5, 8), 0, 0),
298                             /+Europe/London+/       tuple(DateTime(2012, 3, 25),  DateTime(2012, 10, 28), 1, 2),
299                             /+Europe/Paris+/        tuple(DateTime(2012, 3, 25),  DateTime(2012, 10, 28), 2, 3),
300                             /+Australia/Adelaide+/  tuple(DateTime(2012, 10, 7),  DateTime(2012, 4, 1), 2, 3)];
301 
version(Posix)302         version (Posix)
303         {
304             version (FreeBSD)            enum utcZone = "Etc/UTC";
305             else version (NetBSD)        enum utcZone = "UTC";
306             else version (DragonFlyBSD)  enum utcZone = "UTC";
307             else version (linux)         enum utcZone = "UTC";
308             else version (Darwin)        enum utcZone = "UTC";
309             else version (Solaris)       enum utcZone = "UTC";
310             else static assert(0, "The location of the UTC timezone file on this Posix platform must be set.");
311 
312             auto tzs = [testTZ("America/Los_Angeles", "PST", "PDT", dur!"hours"(-8), dur!"hours"(1)),
313                         testTZ("America/New_York", "EST", "EDT", dur!"hours"(-5), dur!"hours"(1)),
314                         //testTZ("America/Santiago", "CLT", "CLST", dur!"hours"(-4), dur!"hours"(1), false),
315                         testTZ("Europe/London", "GMT", "BST", dur!"hours"(0), dur!"hours"(1)),
316                         testTZ("Europe/Paris", "CET", "CEST", dur!"hours"(1), dur!"hours"(1)),
317                         // Per www.timeanddate.com, it should be "CST" and "CDT",
318                         // but the OS insists that it's "CST" for both. We should
319                         // probably figure out how to report an error in the TZ
320                         // database and report it.
321                         testTZ("Australia/Adelaide", "CST", "CST",
322                                dur!"hours"(9) + dur!"minutes"(30), dur!"hours"(1), false)];
323 
324             testTZ(utcZone, "UTC", "UTC", dur!"hours"(0), dur!"hours"(0));
325             assertThrown!DateTimeException(PosixTimeZone.getTimeZone("hello_world"));
326         }
version(Windows)327         else version (Windows)
328         {
329             auto tzs = [testTZ("Pacific Standard Time", "Pacific Standard Time",
330                                "Pacific Daylight Time", dur!"hours"(-8), dur!"hours"(1)),
331                         testTZ("Eastern Standard Time", "Eastern Standard Time",
332                                "Eastern Daylight Time", dur!"hours"(-5), dur!"hours"(1)),
333                         //testTZ("Pacific SA Standard Time", "Pacific SA Standard Time",
334                                //"Pacific SA Daylight Time", dur!"hours"(-4), dur!"hours"(1), false),
335                         testTZ("GMT Standard Time", "GMT Standard Time",
336                                "GMT Daylight Time", dur!"hours"(0), dur!"hours"(1)),
337                         testTZ("Romance Standard Time", "Romance Standard Time",
338                                "Romance Daylight Time", dur!"hours"(1), dur!"hours"(1)),
339                         testTZ("Cen. Australia Standard Time", "Cen. Australia Standard Time",
340                                "Cen. Australia Daylight Time",
341                                dur!"hours"(9) + dur!"minutes"(30), dur!"hours"(1), false)];
342 
343             testTZ("Greenwich Standard Time", "Greenwich Standard Time",
344                    "Greenwich Daylight Time", dur!"hours"(0), dur!"hours"(0));
345             assertThrown!DateTimeException(WindowsTimeZone.getTimeZone("hello_world"));
346         }
347         else
348             assert(0, "OS not supported.");
349 
350         foreach (i; 0 .. tzs.length)
351         {
352             auto tz = tzs[i];
353             immutable spring = dstSwitches[i][2];
354             immutable fall = dstSwitches[i][3];
355             auto stdOffset = SysTime(dstSwitches[i][0] + dur!"days"(-1), tz).utcOffset;
356             auto dstOffset = stdOffset + dur!"hours"(1);
357 
358             // Verify that creating a SysTime in the given time zone results
359             // in a SysTime with the correct std time during and surrounding
360             // a DST switch.
361             foreach (hour; -12 .. 13)
362             {
363                 auto st = SysTime(dstSwitches[i][0] + dur!"hours"(hour), tz);
364                 immutable targetHour = hour < 0 ? hour + 24 : hour;
365 
366                 static void testHour(SysTime st, int hour, string tzName, size_t line = __LINE__)
367                 {
368                     enforce(st.hour == hour,
369                             new AssertError(format("[%s] [%s]: [%s] [%s]", st, tzName, st.hour, hour),
370                                             __FILE__, line));
371                 }
372 
373                 void testOffset1(Duration offset, bool dstInEffect, size_t line = __LINE__)
374                 {
msg(string tag)375                     AssertError msg(string tag)
376                     {
377                         return new AssertError(format("%s [%s] [%s]: [%s] [%s] [%s]",
378                                                       tag, st, tz.name, st.utcOffset, stdOffset, dstOffset),
379                                                __FILE__, line);
380                     }
381 
382                     enforce(st.dstInEffect == dstInEffect, msg("1"));
383                     enforce(st.utcOffset == offset, msg("2"));
384                     enforce((st + dur!"minutes"(1)).utcOffset == offset, msg("3"));
385                 }
386 
387                 if (hour == spring)
388                 {
389                     testHour(st, spring + 1, tz.name);
390                     testHour(st + dur!"minutes"(1), spring + 1, tz.name);
391                 }
392                 else
393                 {
394                     testHour(st, targetHour, tz.name);
395                     testHour(st + dur!"minutes"(1), targetHour, tz.name);
396                 }
397 
398                 if (hour < spring)
399                     testOffset1(stdOffset, false);
400                 else
401                     testOffset1(dstOffset, true);
402 
403                 st = SysTime(dstSwitches[i][1] + dur!"hours"(hour), tz);
404                 testHour(st, targetHour, tz.name);
405 
406                 // Verify that 01:00 is the first 01:00 (or whatever hour before the switch is).
407                 if (hour == fall - 1)
408                     testHour(st + dur!"hours"(1), targetHour, tz.name);
409 
410                 if (hour < fall)
411                     testOffset1(dstOffset, true);
412                 else
413                     testOffset1(stdOffset, false);
414             }
415 
416             // Verify that converting a time in UTC to a time in another
417             // time zone results in the correct time during and surrounding
418             // a DST switch.
419             bool first = true;
420             auto springSwitch = SysTime(dstSwitches[i][0] + dur!"hours"(spring), UTC()) - stdOffset;
421             auto fallSwitch = SysTime(dstSwitches[i][1] + dur!"hours"(fall), UTC()) - dstOffset;
422             // @@@BUG@@@ 3659 makes this necessary.
423             auto fallSwitchMinus1 = fallSwitch - dur!"hours"(1);
424 
425             foreach (hour; -24 .. 25)
426             {
427                 auto utc = SysTime(dstSwitches[i][0] + dur!"hours"(hour), UTC());
428                 auto local = utc.toOtherTZ(tz);
429 
430                 void testOffset2(Duration offset, size_t line = __LINE__)
431                 {
msg(string tag)432                     AssertError msg(string tag)
433                     {
434                         return new AssertError(format("%s [%s] [%s]: [%s] [%s]", tag, hour, tz.name, utc, local),
435                                                __FILE__, line);
436                     }
437 
438                     enforce((utc + offset).hour == local.hour, msg("1"));
439                     enforce((utc + offset + dur!"minutes"(1)).hour == local.hour, msg("2"));
440                 }
441 
442                 if (utc < springSwitch)
443                     testOffset2(stdOffset);
444                 else
445                     testOffset2(dstOffset);
446 
447                 utc = SysTime(dstSwitches[i][1] + dur!"hours"(hour), UTC());
448                 local = utc.toOtherTZ(tz);
449 
450                 if (utc == fallSwitch || utc == fallSwitchMinus1)
451                 {
452                     if (first)
453                     {
454                         testOffset2(dstOffset);
455                         first = false;
456                     }
457                     else
458                         testOffset2(stdOffset);
459                 }
460                 else if (utc > fallSwitch)
461                     testOffset2(stdOffset);
462                 else
463                     testOffset2(dstOffset);
464             }
465         }
466     }
467 
468 
469 protected:
470 
471     /++
472         Params:
473             name    = The name of the time zone.
474             stdName = The abbreviation for the time zone during std time.
475             dstName = The abbreviation for the time zone during DST.
476       +/
this(string name,string stdName,string dstName)477     this(string name, string stdName, string dstName) @safe immutable pure
478     {
479         _name = name;
480         _stdName = stdName;
481         _dstName = dstName;
482     }
483 
484 
485 private:
486 
487     immutable string _name;
488     immutable string _stdName;
489     immutable string _dstName;
490 }
491 
492 
493 /++
494     A TimeZone which represents the current local time zone on
495     the system running your program.
496 
497     This uses the underlying C calls to adjust the time rather than using
498     specific D code based off of system settings to calculate the time such as
499     $(LREF PosixTimeZone) and $(LREF WindowsTimeZone) do. That also means that
500     it will use whatever the current time zone is on the system, even if the
501     system's time zone changes while the program is running.
502   +/
503 final class LocalTime : TimeZone
504 {
505 public:
506 
507     /++
508         $(LREF LocalTime) is a singleton class. $(LREF LocalTime) returns its
509         only instance.
510       +/
immutable(LocalTime)511     static immutable(LocalTime) opCall() @trusted pure nothrow
512     {
513         alias FuncType = @safe pure nothrow immutable(LocalTime) function();
514         return (cast(FuncType)&singleton)();
515     }
516 
517 
518     version (StdDdoc)
519     {
520         /++
521             The name of the time zone per the TZ Database. This is the name used
522             to get a $(LREF TimeZone) by name with $(D TimeZone.getTimeZone).
523 
524             Note that this always returns the empty string. This is because time
525             zones cannot be uniquely identified by the attributes given by the
526             OS (such as the $(D stdName) and $(D dstName)), and neither Posix
527             systems nor Windows systems provide an easy way to get the TZ
528             Database name of the local time zone.
529 
530             See_Also:
531                 $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ
532                   Database)<br>
533                 $(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List
534                   of Time Zones)
535           +/
536         @property override string name() @safe const nothrow;
537     }
538 
539 
540     /++
541         Typically, the abbreviation (generally 3 or 4 letters) for the time zone
542         when DST is $(I not) in effect (e.g. PST). It is not necessarily unique.
543 
544         However, on Windows, it may be the unabbreviated name (e.g. Pacific
545         Standard Time). Regardless, it is not the same as name.
546 
547         This property is overridden because the local time of the system could
548         change while the program is running and we need to determine it
549         dynamically rather than it being fixed like it would be with most time
550         zones.
551       +/
552     @property override string stdName() @trusted const nothrow
553     {
554         version (Posix)
555         {
556             import core.stdc.time : tzname;
557             import std.conv : to;
558             try
559                 return to!string(tzname[0]);
560             catch (Exception e)
561                 assert(0, "to!string(tzname[0]) failed.");
562         }
563         else version (Windows)
564         {
565             TIME_ZONE_INFORMATION tzInfo;
566             GetTimeZoneInformation(&tzInfo);
567 
568             // Cannot use to!string() like this should, probably due to bug
569             // http://d.puremagic.com/issues/show_bug.cgi?id=5016
570             //return to!string(tzInfo.StandardName);
571 
572             wchar[32] str;
573 
574             foreach (i, ref wchar c; str)
575                 c = tzInfo.StandardName[i];
576 
577             string retval;
578 
579             try
580             {
581                 foreach (dchar c; str)
582                 {
583                     if (c == '\0')
584                         break;
585 
586                     retval ~= c;
587                 }
588 
589                 return retval;
590             }
591             catch (Exception e)
592                 assert(0, "GetTimeZoneInformation() returned invalid UTF-16.");
593         }
594     }
595 
596     @safe unittest
597     {
598         version (FreeBSD)
599         {
600             // A bug on FreeBSD 9+ makes it so that this test fails.
601             // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=168862
602         }
603         else version (NetBSD)
604         {
605             // The same bug on NetBSD 7+
606         }
607         else
608         {
609             assert(LocalTime().stdName !is null);
610 
611             version (Posix)
612             {
613                 scope(exit) clearTZEnvVar();
614 
615                 setTZEnvVar("America/Los_Angeles");
616                 assert(LocalTime().stdName == "PST");
617 
618                 setTZEnvVar("America/New_York");
619                 assert(LocalTime().stdName == "EST");
620             }
621         }
622     }
623 
624 
625     /++
626         Typically, the abbreviation (generally 3 or 4 letters) for the time zone
627         when DST $(I is) in effect (e.g. PDT). It is not necessarily unique.
628 
629         However, on Windows, it may be the unabbreviated name (e.g. Pacific
630         Daylight Time). Regardless, it is not the same as name.
631 
632         This property is overridden because the local time of the system could
633         change while the program is running and we need to determine it
634         dynamically rather than it being fixed like it would be with most time
635         zones.
636       +/
637     @property override string dstName() @trusted const nothrow
638     {
639         version (Posix)
640         {
641             import core.stdc.time : tzname;
642             import std.conv : to;
643             try
644                 return to!string(tzname[1]);
645             catch (Exception e)
646                 assert(0, "to!string(tzname[1]) failed.");
647         }
648         else version (Windows)
649         {
650             TIME_ZONE_INFORMATION tzInfo;
651             GetTimeZoneInformation(&tzInfo);
652 
653             // Cannot use to!string() like this should, probably due to bug
654             // http://d.puremagic.com/issues/show_bug.cgi?id=5016
655             //return to!string(tzInfo.DaylightName);
656 
657             wchar[32] str;
658 
659             foreach (i, ref wchar c; str)
660                 c = tzInfo.DaylightName[i];
661 
662             string retval;
663 
664             try
665             {
666                 foreach (dchar c; str)
667                 {
668                     if (c == '\0')
669                         break;
670 
671                     retval ~= c;
672                 }
673 
674                 return retval;
675             }
676             catch (Exception e)
677                 assert(0, "GetTimeZoneInformation() returned invalid UTF-16.");
678         }
679     }
680 
681     @safe unittest
682     {
683         // tzname, called from dstName, isn't set by default for Musl.
684         version (CRuntime_Musl)
685             assert(LocalTime().dstName is null);
686         else
687             assert(LocalTime().dstName !is null);
688 
version(Posix)689         version (Posix)
690         {
691             scope(exit) clearTZEnvVar();
692 
693             version (FreeBSD)
694             {
695                 // A bug on FreeBSD 9+ makes it so that this test fails.
696                 // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=168862
697             }
698             else version (NetBSD)
699             {
700                 // The same bug on NetBSD 7+
701             }
702             else
703             {
704                 setTZEnvVar("America/Los_Angeles");
705                 assert(LocalTime().dstName == "PDT");
706 
707                 setTZEnvVar("America/New_York");
708                 assert(LocalTime().dstName == "EDT");
709             }
710         }
711     }
712 
713 
714     /++
715         Whether this time zone has Daylight Savings Time at any point in time.
716         Note that for some time zone types it may not have DST for current
717         dates but will still return true for $(D hasDST) because the time zone
718         did at some point have DST.
719       +/
hasDST()720     @property override bool hasDST() @trusted const nothrow
721     {
722         version (Posix)
723         {
724             static if (is(typeof(daylight)))
725                 return cast(bool)(daylight);
726             else
727             {
728                 try
729                 {
730                     auto currYear = (cast(Date) Clock.currTime()).year;
731                     auto janOffset = SysTime(Date(currYear, 1, 4), cast(immutable) this).stdTime -
732                                      SysTime(Date(currYear, 1, 4), UTC()).stdTime;
733                     auto julyOffset = SysTime(Date(currYear, 7, 4), cast(immutable) this).stdTime -
734                                       SysTime(Date(currYear, 7, 4), UTC()).stdTime;
735 
736                     return janOffset != julyOffset;
737                 }
738                 catch (Exception e)
739                     assert(0, "Clock.currTime() threw.");
740             }
741         }
742         else version (Windows)
743         {
744             TIME_ZONE_INFORMATION tzInfo;
745             GetTimeZoneInformation(&tzInfo);
746 
747             return tzInfo.DaylightDate.wMonth != 0;
748         }
749     }
750 
751     @safe unittest
752     {
753         LocalTime().hasDST;
754 
version(Posix)755         version (Posix)
756         {
757             scope(exit) clearTZEnvVar();
758 
759             setTZEnvVar("America/Los_Angeles");
760             assert(LocalTime().hasDST);
761 
762             setTZEnvVar("America/New_York");
763             assert(LocalTime().hasDST);
764 
765             setTZEnvVar("UTC");
766             assert(!LocalTime().hasDST);
767         }
768     }
769 
770 
771     /++
772         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
773         in UTC time (i.e. std time) and returns whether DST is in effect in this
774         time zone at the given point in time.
775 
776         Params:
777             stdTime = The UTC time that needs to be checked for DST in this time
778                       zone.
779       +/
dstInEffect(long stdTime)780     override bool dstInEffect(long stdTime) @trusted const nothrow
781     {
782         import core.stdc.time : localtime, tm;
783         time_t unixTime = stdTimeToUnixTime(stdTime);
784 
785         version (Posix)
786         {
787             tm* timeInfo = localtime(&unixTime);
788 
789             return cast(bool)(timeInfo.tm_isdst);
790         }
791         else version (Windows)
792         {
793             // Apparently Windows isn't smart enough to deal with negative time_t.
794             if (unixTime >= 0)
795             {
796                 tm* timeInfo = localtime(&unixTime);
797 
798                 if (timeInfo)
799                     return cast(bool)(timeInfo.tm_isdst);
800             }
801 
802             TIME_ZONE_INFORMATION tzInfo;
803             GetTimeZoneInformation(&tzInfo);
804 
805             return WindowsTimeZone._dstInEffect(&tzInfo, stdTime);
806         }
807     }
808 
809     @safe unittest
810     {
811         auto currTime = Clock.currStdTime;
812         LocalTime().dstInEffect(currTime);
813     }
814 
815 
816     /++
817         Returns hnsecs in the local time zone using the standard C function
818         calls on Posix systems and the standard Windows system calls on Windows
819         systems to adjust the time to the appropriate time zone from std time.
820 
821         Params:
822             stdTime = The UTC time that needs to be adjusted to this time zone's
823                       time.
824 
825         See_Also:
826             $(D TimeZone.utcToTZ)
827       +/
utcToTZ(long stdTime)828     override long utcToTZ(long stdTime) @trusted const nothrow
829     {
830         version (Solaris)
831             return stdTime + convert!("seconds", "hnsecs")(tm_gmtoff(stdTime));
832         else version (Posix)
833         {
834             import core.stdc.time : localtime, tm;
835             time_t unixTime = stdTimeToUnixTime(stdTime);
836             tm* timeInfo = localtime(&unixTime);
837 
838             return stdTime + convert!("seconds", "hnsecs")(timeInfo.tm_gmtoff);
839         }
840         else version (Windows)
841         {
842             TIME_ZONE_INFORMATION tzInfo;
843             GetTimeZoneInformation(&tzInfo);
844 
845             return WindowsTimeZone._utcToTZ(&tzInfo, stdTime, hasDST);
846         }
847     }
848 
849     @safe unittest
850     {
851         LocalTime().utcToTZ(0);
852     }
853 
854 
855     /++
856         Returns std time using the standard C function calls on Posix systems
857         and the standard Windows system calls on Windows systems to adjust the
858         time to UTC from the appropriate time zone.
859 
860         See_Also:
861             $(D TimeZone.tzToUTC)
862 
863         Params:
864             adjTime = The time in this time zone that needs to be adjusted to
865                       UTC time.
866       +/
867     override long tzToUTC(long adjTime) @trusted const nothrow
868     {
869         version (Posix)
870         {
871             import core.stdc.time : localtime, tm;
872             time_t unixTime = stdTimeToUnixTime(adjTime);
873 
874             immutable past = unixTime - cast(time_t) convert!("days", "seconds")(1);
875             tm* timeInfo = localtime(past < unixTime ? &past : &unixTime);
876             immutable pastOffset = timeInfo.tm_gmtoff;
877 
878             immutable future = unixTime + cast(time_t) convert!("days", "seconds")(1);
879             timeInfo = localtime(future > unixTime ? &future : &unixTime);
880             immutable futureOffset = timeInfo.tm_gmtoff;
881 
882             if (pastOffset == futureOffset)
883                 return adjTime - convert!("seconds", "hnsecs")(pastOffset);
884 
885             if (pastOffset < futureOffset)
886                 unixTime -= cast(time_t) convert!("hours", "seconds")(1);
887 
888             unixTime -= pastOffset;
889             timeInfo = localtime(&unixTime);
890 
891             return adjTime - convert!("seconds", "hnsecs")(timeInfo.tm_gmtoff);
892         }
893         else version (Windows)
894         {
895             TIME_ZONE_INFORMATION tzInfo;
896             GetTimeZoneInformation(&tzInfo);
897 
898             return WindowsTimeZone._tzToUTC(&tzInfo, adjTime, hasDST);
899         }
900     }
901 
902     @safe unittest
903     {
904         import core.exception : AssertError;
905         import std.format : format;
906         import std.typecons : tuple;
907 
908         assert(LocalTime().tzToUTC(LocalTime().utcToTZ(0)) == 0);
909         assert(LocalTime().utcToTZ(LocalTime().tzToUTC(0)) == 0);
910 
911         assert(LocalTime().tzToUTC(LocalTime().utcToTZ(0)) == 0);
912         assert(LocalTime().utcToTZ(LocalTime().tzToUTC(0)) == 0);
913 
914         version (Posix)
915         {
916             scope(exit) clearTZEnvVar();
917 
918             auto tzInfos = [tuple("America/Los_Angeles", DateTime(2012, 3, 11), DateTime(2012, 11, 4), 2, 2),
919                             tuple("America/New_York",    DateTime(2012, 3, 11), DateTime(2012, 11, 4), 2, 2),
920                             //tuple("America/Santiago",    DateTime(2011, 8, 21), DateTime(2011, 5, 8), 0, 0),
921                             tuple("Atlantic/Azores",     DateTime(2011, 3, 27), DateTime(2011, 10, 30), 0, 1),
922                             tuple("Europe/London",       DateTime(2012, 3, 25), DateTime(2012, 10, 28), 1, 2),
923                             tuple("Europe/Paris",        DateTime(2012, 3, 25), DateTime(2012, 10, 28), 2, 3),
924                             tuple("Australia/Adelaide",  DateTime(2012, 10, 7), DateTime(2012, 4, 1), 2, 3)];
925 
926             foreach (i; 0 .. tzInfos.length)
927             {
928                 auto tzName = tzInfos[i][0];
929                 setTZEnvVar(tzName);
930                 immutable spring = tzInfos[i][3];
931                 immutable fall = tzInfos[i][4];
932                 auto stdOffset = SysTime(tzInfos[i][1] + dur!"hours"(-12)).utcOffset;
933                 auto dstOffset = stdOffset + dur!"hours"(1);
934 
935                 // Verify that creating a SysTime in the given time zone results
936                 // in a SysTime with the correct std time during and surrounding
937                 // a DST switch.
938                 foreach (hour; -12 .. 13)
939                 {
940                     auto st = SysTime(tzInfos[i][1] + dur!"hours"(hour));
941                     immutable targetHour = hour < 0 ? hour + 24 : hour;
942 
943                     static void testHour(SysTime st, int hour, string tzName, size_t line = __LINE__)
944                     {
945                         enforce(st.hour == hour,
946                                 new AssertError(format("[%s] [%s]: [%s] [%s]", st, tzName, st.hour, hour),
947                                                 __FILE__, line));
948                     }
949 
950                     void testOffset1(Duration offset, bool dstInEffect, size_t line = __LINE__)
951                     {
952                         AssertError msg(string tag)
953                         {
954                             return new AssertError(format("%s [%s] [%s]: [%s] [%s] [%s]",
955                                                           tag, st, tzName, st.utcOffset, stdOffset, dstOffset),
956                                                    __FILE__, line);
957                         }
958 
959                         enforce(st.dstInEffect == dstInEffect, msg("1"));
960                         enforce(st.utcOffset == offset, msg("2"));
961                         enforce((st + dur!"minutes"(1)).utcOffset == offset, msg("3"));
962                     }
963 
964                     if (hour == spring)
965                     {
966                         testHour(st, spring + 1, tzName);
967                         testHour(st + dur!"minutes"(1), spring + 1, tzName);
968                     }
969                     else
970                     {
971                         testHour(st, targetHour, tzName);
972                         testHour(st + dur!"minutes"(1), targetHour, tzName);
973                     }
974 
975                     if (hour < spring)
976                         testOffset1(stdOffset, false);
977                     else
978                         testOffset1(dstOffset, true);
979 
980                     st = SysTime(tzInfos[i][2] + dur!"hours"(hour));
981                     testHour(st, targetHour, tzName);
982 
983                     // Verify that 01:00 is the first 01:00 (or whatever hour before the switch is).
984                     if (hour == fall - 1)
985                         testHour(st + dur!"hours"(1), targetHour, tzName);
986 
987                     if (hour < fall)
988                         testOffset1(dstOffset, true);
989                     else
990                         testOffset1(stdOffset, false);
991                 }
992 
993                 // Verify that converting a time in UTC to a time in another
994                 // time zone results in the correct time during and surrounding
995                 // a DST switch.
996                 bool first = true;
997                 auto springSwitch = SysTime(tzInfos[i][1] + dur!"hours"(spring), UTC()) - stdOffset;
998                 auto fallSwitch = SysTime(tzInfos[i][2] + dur!"hours"(fall), UTC()) - dstOffset;
999                 // @@@BUG@@@ 3659 makes this necessary.
1000                 auto fallSwitchMinus1 = fallSwitch - dur!"hours"(1);
1001 
1002                 foreach (hour; -24 .. 25)
1003                 {
1004                     auto utc = SysTime(tzInfos[i][1] + dur!"hours"(hour), UTC());
1005                     auto local = utc.toLocalTime();
1006 
1007                     void testOffset2(Duration offset, size_t line = __LINE__)
1008                     {
1009                         AssertError msg(string tag)
1010                         {
1011                             return new AssertError(format("%s [%s] [%s]: [%s] [%s]", tag, hour, tzName, utc, local),
1012                                                    __FILE__, line);
1013                         }
1014 
1015                         enforce((utc + offset).hour == local.hour, msg("1"));
1016                         enforce((utc + offset + dur!"minutes"(1)).hour == local.hour, msg("2"));
1017                     }
1018 
1019                     if (utc < springSwitch)
1020                         testOffset2(stdOffset);
1021                     else
1022                         testOffset2(dstOffset);
1023 
1024                     utc = SysTime(tzInfos[i][2] + dur!"hours"(hour), UTC());
1025                     local = utc.toLocalTime();
1026 
1027                     if (utc == fallSwitch || utc == fallSwitchMinus1)
1028                     {
1029                         if (first)
1030                         {
1031                             testOffset2(dstOffset);
1032                             first = false;
1033                         }
1034                         else
1035                             testOffset2(stdOffset);
1036                     }
1037                     else if (utc > fallSwitch)
1038                         testOffset2(stdOffset);
1039                     else
1040                         testOffset2(dstOffset);
1041                 }
1042             }
1043         }
1044     }
1045 
1046 
1047 private:
1048 
1049     this() @safe immutable pure
1050     {
1051         super("", "", "");
1052     }
1053 
1054 
1055     // This is done so that we can maintain purity in spite of doing an impure
1056     // operation the first time that LocalTime() is called.
1057     static immutable(LocalTime) singleton() @trusted
1058     {
1059         import core.stdc.time : tzset;
1060         import std.concurrency : initOnce;
1061         static instance = new immutable(LocalTime)();
1062         static shared bool guard;
1063         initOnce!guard({tzset(); return true;}());
1064         return instance;
1065     }
1066 
1067 
1068     // The Solaris version of struct tm has no tm_gmtoff field, so do it here
1069     version (Solaris)
1070     {
1071         long tm_gmtoff(long stdTime) @trusted const nothrow
1072         {
1073             import core.stdc.time : localtime, gmtime, tm;
1074 
1075             time_t unixTime = stdTimeToUnixTime(stdTime);
1076             tm* buf = localtime(&unixTime);
1077             tm timeInfo = *buf;
1078             buf = gmtime(&unixTime);
1079             tm timeInfoGmt = *buf;
1080 
1081             return timeInfo.tm_sec - timeInfoGmt.tm_sec +
1082                    convert!("minutes", "seconds")(timeInfo.tm_min - timeInfoGmt.tm_min) +
1083                    convert!("hours", "seconds")(timeInfo.tm_hour - timeInfoGmt.tm_hour);
1084         }
1085     }
1086 }
1087 
1088 
1089 /++
1090     A $(LREF TimeZone) which represents UTC.
1091   +/
1092 final class UTC : TimeZone
1093 {
1094 public:
1095 
1096     /++
1097         $(D UTC) is a singleton class. $(D UTC) returns its only instance.
1098       +/
1099     static immutable(UTC) opCall() @safe pure nothrow
1100     {
1101         return _utc;
1102     }
1103 
1104 
1105     /++
1106         Always returns false.
1107       +/
1108     @property override bool hasDST() @safe const nothrow
1109     {
1110         return false;
1111     }
1112 
1113 
1114     /++
1115         Always returns false.
1116       +/
1117     override bool dstInEffect(long stdTime) @safe const nothrow
1118     {
1119         return false;
1120     }
1121 
1122 
1123     /++
1124         Returns the given hnsecs without changing them at all.
1125 
1126         Params:
1127             stdTime = The UTC time that needs to be adjusted to this time zone's
1128                       time.
1129 
1130         See_Also:
1131             $(D TimeZone.utcToTZ)
1132       +/
1133     override long utcToTZ(long stdTime) @safe const nothrow
1134     {
1135         return stdTime;
1136     }
1137 
1138     @safe unittest
1139     {
1140         assert(UTC().utcToTZ(0) == 0);
1141 
version(Posix)1142         version (Posix)
1143         {
1144             scope(exit) clearTZEnvVar();
1145 
1146             setTZEnvVar("UTC");
1147             auto std = SysTime(Date(2010, 1, 1));
1148             auto dst = SysTime(Date(2010, 7, 1));
1149             assert(UTC().utcToTZ(std.stdTime) == std.stdTime);
1150             assert(UTC().utcToTZ(dst.stdTime) == dst.stdTime);
1151         }
1152     }
1153 
1154 
1155     /++
1156         Returns the given hnsecs without changing them at all.
1157 
1158         See_Also:
1159             $(D TimeZone.tzToUTC)
1160 
1161         Params:
1162             adjTime = The time in this time zone that needs to be adjusted to
1163                       UTC time.
1164       +/
tzToUTC(long adjTime)1165     override long tzToUTC(long adjTime) @safe const nothrow
1166     {
1167         return adjTime;
1168     }
1169 
1170     @safe unittest
1171     {
1172         assert(UTC().tzToUTC(0) == 0);
1173 
version(Posix)1174         version (Posix)
1175         {
1176             scope(exit) clearTZEnvVar();
1177 
1178             setTZEnvVar("UTC");
1179             auto std = SysTime(Date(2010, 1, 1));
1180             auto dst = SysTime(Date(2010, 7, 1));
1181             assert(UTC().tzToUTC(std.stdTime) == std.stdTime);
1182             assert(UTC().tzToUTC(dst.stdTime) == dst.stdTime);
1183         }
1184     }
1185 
1186 
1187     /++
1188         Returns a $(REF Duration, core,time) of 0.
1189 
1190         Params:
1191             stdTime = The UTC time for which to get the offset from UTC for this
1192                       time zone.
1193       +/
utcOffsetAt(long stdTime)1194     override Duration utcOffsetAt(long stdTime) @safe const nothrow
1195     {
1196         return dur!"hnsecs"(0);
1197     }
1198 
1199 
1200 private:
1201 
this()1202     this() @safe immutable pure
1203     {
1204         super("UTC", "UTC", "UTC");
1205     }
1206 
1207 
1208     static immutable UTC _utc = new immutable(UTC)();
1209 }
1210 
1211 
1212 /++
1213     Represents a time zone with an offset (in minutes, west is negative) from
1214     UTC but no DST.
1215 
1216     It's primarily used as the time zone in the result of
1217     $(REF SysTime,std,datetime,systime)'s $(D fromISOString),
1218     $(D fromISOExtString), and $(D fromSimpleString).
1219 
1220     $(D name) and $(D dstName) are always the empty string since this time zone
1221     has no DST, and while it may be meant to represent a time zone which is in
1222     the TZ Database, obviously it's not likely to be following the exact rules
1223     of any of the time zones in the TZ Database, so it makes no sense to set it.
1224   +/
1225 final class SimpleTimeZone : TimeZone
1226 {
1227 public:
1228 
1229     /++
1230         Always returns false.
1231       +/
hasDST()1232     @property override bool hasDST() @safe const nothrow
1233     {
1234         return false;
1235     }
1236 
1237 
1238     /++
1239         Always returns false.
1240       +/
1241     override bool dstInEffect(long stdTime) @safe const nothrow
1242     {
1243         return false;
1244     }
1245 
1246 
1247     /++
1248         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
1249         in UTC time (i.e. std time) and converts it to this time zone's time.
1250 
1251         Params:
1252             stdTime = The UTC time that needs to be adjusted to this time zone's
1253                       time.
1254       +/
1255     override long utcToTZ(long stdTime) @safe const nothrow
1256     {
1257         return stdTime + _utcOffset.total!"hnsecs";
1258     }
1259 
1260     @safe unittest
1261     {
1262         auto west = new immutable SimpleTimeZone(dur!"hours"(-8));
1263         auto east = new immutable SimpleTimeZone(dur!"hours"(8));
1264 
1265         assert(west.utcToTZ(0) == -288_000_000_000L);
1266         assert(east.utcToTZ(0) == 288_000_000_000L);
1267         assert(west.utcToTZ(54_321_234_567_890L) == 54_033_234_567_890L);
1268 
1269         const cstz = west;
1270         assert(cstz.utcToTZ(50002) == west.utcToTZ(50002));
1271     }
1272 
1273 
1274     /++
1275         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
1276         in this time zone's time and converts it to UTC (i.e. std time).
1277 
1278         Params:
1279             adjTime = The time in this time zone that needs to be adjusted to
1280                       UTC time.
1281       +/
1282     override long tzToUTC(long adjTime) @safe const nothrow
1283     {
1284         return adjTime - _utcOffset.total!"hnsecs";
1285     }
1286 
1287     @safe unittest
1288     {
1289         auto west = new immutable SimpleTimeZone(dur!"hours"(-8));
1290         auto east = new immutable SimpleTimeZone(dur!"hours"(8));
1291 
1292         assert(west.tzToUTC(-288_000_000_000L) == 0);
1293         assert(east.tzToUTC(288_000_000_000L) == 0);
1294         assert(west.tzToUTC(54_033_234_567_890L) == 54_321_234_567_890L);
1295 
1296         const cstz = west;
1297         assert(cstz.tzToUTC(20005) == west.tzToUTC(20005));
1298     }
1299 
1300 
1301     /++
1302         Returns utcOffset as a $(REF Duration, core,time).
1303 
1304         Params:
1305             stdTime = The UTC time for which to get the offset from UTC for this
1306                       time zone.
1307       +/
utcOffsetAt(long stdTime)1308     override Duration utcOffsetAt(long stdTime) @safe const nothrow
1309     {
1310         return _utcOffset;
1311     }
1312 
1313 
1314     /++
1315         Params:
1316             utcOffset = This time zone's offset from UTC with west of UTC being
1317                         negative (it is added to UTC to get the adjusted time).
1318             stdName   = The $(D stdName) for this time zone.
1319       +/
1320     this(Duration utcOffset, string stdName = "") @safe immutable pure
1321     {
1322         // FIXME This probably needs to be changed to something like (-12 - 13).
1323         enforce!DateTimeException(abs(utcOffset) < dur!"minutes"(1440),
1324                                     "Offset from UTC must be within range (-24:00 - 24:00).");
1325         super("", stdName, "");
1326         this._utcOffset = utcOffset;
1327     }
1328 
1329     @safe unittest
1330     {
1331         auto stz = new immutable SimpleTimeZone(dur!"hours"(-8), "PST");
1332         assert(stz.name == "");
1333         assert(stz.stdName == "PST");
1334         assert(stz.dstName == "");
1335         assert(stz.utcOffset == dur!"hours"(-8));
1336     }
1337 
1338 
1339     /++
1340         The amount of time the offset from UTC is (negative is west of UTC,
1341         positive is east).
1342       +/
utcOffset()1343     @property Duration utcOffset() @safe const pure nothrow
1344     {
1345         return _utcOffset;
1346     }
1347 
1348 
1349 package:
1350 
1351     /+
1352         Returns a time zone as a string with an offset from UTC.
1353 
1354         Time zone offsets will be in the form +HHMM or -HHMM.
1355 
1356         Params:
1357             utcOffset = The number of minutes offset from UTC (negative means
1358                         west).
1359       +/
1360     static string toISOString(Duration utcOffset) @safe pure
1361     {
1362         import std.format : format;
1363         immutable absOffset = abs(utcOffset);
1364         enforce!DateTimeException(absOffset < dur!"minutes"(1440),
1365                                   "Offset from UTC must be within range (-24:00 - 24:00).");
1366         int hours;
1367         int minutes;
1368         absOffset.split!("hours", "minutes")(hours, minutes);
1369         return format(utcOffset < Duration.zero ? "-%02d%02d" : "+%02d%02d", hours, minutes);
1370     }
1371 
1372     @safe unittest
1373     {
1374         static string testSTZInvalid(Duration offset)
1375         {
1376             return SimpleTimeZone.toISOString(offset);
1377         }
1378 
1379         assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(1440)));
1380         assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(-1440)));
1381 
1382         assert(toISOString(dur!"minutes"(0)) == "+0000");
1383         assert(toISOString(dur!"minutes"(1)) == "+0001");
1384         assert(toISOString(dur!"minutes"(10)) == "+0010");
1385         assert(toISOString(dur!"minutes"(59)) == "+0059");
1386         assert(toISOString(dur!"minutes"(60)) == "+0100");
1387         assert(toISOString(dur!"minutes"(90)) == "+0130");
1388         assert(toISOString(dur!"minutes"(120)) == "+0200");
1389         assert(toISOString(dur!"minutes"(480)) == "+0800");
1390         assert(toISOString(dur!"minutes"(1439)) == "+2359");
1391 
1392         assert(toISOString(dur!"minutes"(-1)) == "-0001");
1393         assert(toISOString(dur!"minutes"(-10)) == "-0010");
1394         assert(toISOString(dur!"minutes"(-59)) == "-0059");
1395         assert(toISOString(dur!"minutes"(-60)) == "-0100");
1396         assert(toISOString(dur!"minutes"(-90)) == "-0130");
1397         assert(toISOString(dur!"minutes"(-120)) == "-0200");
1398         assert(toISOString(dur!"minutes"(-480)) == "-0800");
1399         assert(toISOString(dur!"minutes"(-1439)) == "-2359");
1400     }
1401 
1402 
1403     /+
1404         Returns a time zone as a string with an offset from UTC.
1405 
1406         Time zone offsets will be in the form +HH:MM or -HH:MM.
1407 
1408         Params:
1409             utcOffset = The number of minutes offset from UTC (negative means
1410                         west).
1411       +/
1412     static string toISOExtString(Duration utcOffset) @safe pure
1413     {
1414         import std.format : format;
1415 
1416         immutable absOffset = abs(utcOffset);
1417         enforce!DateTimeException(absOffset < dur!"minutes"(1440),
1418                                   "Offset from UTC must be within range (-24:00 - 24:00).");
1419         int hours;
1420         int minutes;
1421         absOffset.split!("hours", "minutes")(hours, minutes);
1422         return format(utcOffset < Duration.zero ? "-%02d:%02d" : "+%02d:%02d", hours, minutes);
1423     }
1424 
1425     @safe unittest
1426     {
1427         static string testSTZInvalid(Duration offset)
1428         {
1429             return SimpleTimeZone.toISOExtString(offset);
1430         }
1431 
1432         assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(1440)));
1433         assertThrown!DateTimeException(testSTZInvalid(dur!"minutes"(-1440)));
1434 
1435         assert(toISOExtString(dur!"minutes"(0)) == "+00:00");
1436         assert(toISOExtString(dur!"minutes"(1)) == "+00:01");
1437         assert(toISOExtString(dur!"minutes"(10)) == "+00:10");
1438         assert(toISOExtString(dur!"minutes"(59)) == "+00:59");
1439         assert(toISOExtString(dur!"minutes"(60)) == "+01:00");
1440         assert(toISOExtString(dur!"minutes"(90)) == "+01:30");
1441         assert(toISOExtString(dur!"minutes"(120)) == "+02:00");
1442         assert(toISOExtString(dur!"minutes"(480)) == "+08:00");
1443         assert(toISOExtString(dur!"minutes"(1439)) == "+23:59");
1444 
1445         assert(toISOExtString(dur!"minutes"(-1)) == "-00:01");
1446         assert(toISOExtString(dur!"minutes"(-10)) == "-00:10");
1447         assert(toISOExtString(dur!"minutes"(-59)) == "-00:59");
1448         assert(toISOExtString(dur!"minutes"(-60)) == "-01:00");
1449         assert(toISOExtString(dur!"minutes"(-90)) == "-01:30");
1450         assert(toISOExtString(dur!"minutes"(-120)) == "-02:00");
1451         assert(toISOExtString(dur!"minutes"(-480)) == "-08:00");
1452         assert(toISOExtString(dur!"minutes"(-1439)) == "-23:59");
1453     }
1454 
1455 
1456     /+
1457         Takes a time zone as a string with an offset from UTC and returns a
1458         $(LREF SimpleTimeZone) which matches.
1459 
1460         The accepted formats for time zone offsets are +HH, -HH, +HHMM, and
1461         -HHMM.
1462 
1463         Params:
1464             isoString = A string which represents a time zone in the ISO format.
1465       +/
1466     static immutable(SimpleTimeZone) fromISOString(S)(S isoString) @safe pure
1467         if (isSomeString!S)
1468     {
1469         import std.algorithm.searching : startsWith, countUntil, all;
1470         import std.ascii : isDigit;
1471         import std.conv : to;
1472         import std.format : format;
1473 
1474         auto dstr = to!dstring(isoString);
1475 
1476         enforce!DateTimeException(dstr.startsWith('-', '+'), "Invalid ISO String");
1477 
1478         auto sign = dstr.startsWith('-') ? -1 : 1;
1479 
1480         dstr.popFront();
1481         enforce!DateTimeException(all!isDigit(dstr), format("Invalid ISO String: %s", dstr));
1482 
1483         int hours;
1484         int minutes;
1485 
1486         if (dstr.length == 2)
1487             hours = to!int(dstr);
1488         else if (dstr.length == 4)
1489         {
1490             hours = to!int(dstr[0 .. 2]);
1491             minutes = to!int(dstr[2 .. 4]);
1492         }
1493         else
1494             throw new DateTimeException(format("Invalid ISO String: %s", dstr));
1495 
1496         enforce!DateTimeException(hours < 24 && minutes < 60, format("Invalid ISO String: %s", dstr));
1497 
1498         return new immutable SimpleTimeZone(sign * (dur!"hours"(hours) + dur!"minutes"(minutes)));
1499     }
1500 
1501     @safe unittest
1502     {
1503         import core.exception : AssertError;
1504         import std.format : format;
1505 
1506         foreach (str; ["", "Z", "-", "+", "-:", "+:", "-1:", "+1:", "+1", "-1",
1507                        "-24:00", "+24:00", "-24", "+24", "-2400", "+2400",
1508                        "1", "+1", "-1", "+9", "-9",
1509                        "+1:0", "+01:0", "+1:00", "+01:000", "+01:60",
1510                        "-1:0", "-01:0", "-1:00", "-01:000", "-01:60",
1511                        "000", "00000", "0160", "-0160",
1512                        " +08:00", "+ 08:00", "+08 :00", "+08: 00", "+08:00 ",
1513                        " -08:00", "- 08:00", "-08 :00", "-08: 00", "-08:00 ",
1514                        " +0800", "+ 0800", "+08 00", "+08 00", "+0800 ",
1515                        " -0800", "- 0800", "-08 00", "-08 00", "-0800 ",
1516                        "+ab:cd", "+abcd", "+0Z:00", "+Z", "+00Z",
1517                        "-ab:cd", "+abcd", "-0Z:00", "-Z", "-00Z",
1518                        "01:00", "12:00", "23:59"])
1519         {
1520             assertThrown!DateTimeException(SimpleTimeZone.fromISOString(str), format("[%s]", str));
1521         }
1522 
1523         static void test(string str, Duration utcOffset, size_t line = __LINE__)
1524         {
1525             if (SimpleTimeZone.fromISOString(str).utcOffset != (new immutable SimpleTimeZone(utcOffset)).utcOffset)
1526                 throw new AssertError("unittest failure", __FILE__, line);
1527         }
1528 
1529         test("+0000", Duration.zero);
1530         test("+0001", minutes(1));
1531         test("+0010", minutes(10));
1532         test("+0059", minutes(59));
1533         test("+0100", hours(1));
1534         test("+0130", hours(1) + minutes(30));
1535         test("+0200", hours(2));
1536         test("+0800", hours(8));
1537         test("+2359", hours(23) + minutes(59));
1538 
1539         test("-0001", minutes(-1));
1540         test("-0010", minutes(-10));
1541         test("-0059", minutes(-59));
1542         test("-0100", hours(-1));
1543         test("-0130", hours(-1) - minutes(30));
1544         test("-0200", hours(-2));
1545         test("-0800", hours(-8));
1546         test("-2359", hours(-23) - minutes(59));
1547 
1548         test("+00", Duration.zero);
1549         test("+01", hours(1));
1550         test("+02", hours(2));
1551         test("+12", hours(12));
1552         test("+23", hours(23));
1553 
1554         test("-00", Duration.zero);
1555         test("-01", hours(-1));
1556         test("-02", hours(-2));
1557         test("-12", hours(-12));
1558         test("-23", hours(-23));
1559     }
1560 
1561     @safe unittest
1562     {
1563         import core.exception : AssertError;
1564         import std.format : format;
1565 
1566         static void test(in string isoString, int expectedOffset, size_t line = __LINE__)
1567         {
1568             auto stz = SimpleTimeZone.fromISOExtString(isoString);
1569             if (stz.utcOffset != dur!"minutes"(expectedOffset))
1570                 throw new AssertError(format("unittest failure: wrong offset [%s]", stz.utcOffset), __FILE__, line);
1571 
1572             auto result = SimpleTimeZone.toISOExtString(stz.utcOffset);
1573             if (result != isoString)
1574                 throw new AssertError(format("unittest failure: [%s] != [%s]", result, isoString), __FILE__, line);
1575         }
1576 
1577         test("+00:00", 0);
1578         test("+00:01", 1);
1579         test("+00:10", 10);
1580         test("+00:59", 59);
1581         test("+01:00", 60);
1582         test("+01:30", 90);
1583         test("+02:00", 120);
1584         test("+08:00", 480);
1585         test("+08:00", 480);
1586         test("+23:59", 1439);
1587 
1588         test("-00:01", -1);
1589         test("-00:10", -10);
1590         test("-00:59", -59);
1591         test("-01:00", -60);
1592         test("-01:30", -90);
1593         test("-02:00", -120);
1594         test("-08:00", -480);
1595         test("-08:00", -480);
1596         test("-23:59", -1439);
1597     }
1598 
1599 
1600     /+
1601         Takes a time zone as a string with an offset from UTC and returns a
1602         $(LREF SimpleTimeZone) which matches.
1603 
1604         The accepted formats for time zone offsets are +HH, -HH, +HH:MM, and
1605         -HH:MM.
1606 
1607         Params:
1608             isoExtString = A string which represents a time zone in the ISO format.
1609       +/
1610     static immutable(SimpleTimeZone) fromISOExtString(S)(S isoExtString) @safe pure
1611         if (isSomeString!S)
1612     {
1613         import std.algorithm.searching : startsWith, countUntil, all;
1614         import std.ascii : isDigit;
1615         import std.conv : to;
1616         import std.format : format;
1617 
1618         auto dstr = to!dstring(isoExtString);
1619 
1620         enforce!DateTimeException(dstr.startsWith('-', '+'), "Invalid ISO String");
1621 
1622         auto sign = dstr.startsWith('-') ? -1 : 1;
1623 
1624         dstr.popFront();
1625         enforce!DateTimeException(!dstr.empty, "Invalid ISO String");
1626 
1627         immutable colon = dstr.countUntil(':');
1628 
1629         dstring hoursStr;
1630         dstring minutesStr;
1631 
1632         if (colon != -1)
1633         {
1634             hoursStr = dstr[0 .. colon];
1635             minutesStr = dstr[colon + 1 .. $];
1636             enforce!DateTimeException(minutesStr.length == 2, format("Invalid ISO String: %s", dstr));
1637         }
1638         else
1639             hoursStr = dstr;
1640 
1641         enforce!DateTimeException(hoursStr.length == 2, format("Invalid ISO String: %s", dstr));
1642         enforce!DateTimeException(all!isDigit(hoursStr), format("Invalid ISO String: %s", dstr));
1643         enforce!DateTimeException(all!isDigit(minutesStr), format("Invalid ISO String: %s", dstr));
1644 
1645         immutable hours = to!int(hoursStr);
1646         immutable minutes = minutesStr.empty ? 0 : to!int(minutesStr);
1647         enforce!DateTimeException(hours < 24 && minutes < 60, format("Invalid ISO String: %s", dstr));
1648 
1649         return new immutable SimpleTimeZone(sign * (dur!"hours"(hours) + dur!"minutes"(minutes)));
1650     }
1651 
1652     @safe unittest
1653     {
1654         import core.exception : AssertError;
1655         import std.format : format;
1656 
1657         foreach (str; ["", "Z", "-", "+", "-:", "+:", "-1:", "+1:", "+1", "-1",
1658                        "-24:00", "+24:00", "-24", "+24", "-2400", "-2400",
1659                        "1", "+1", "-1", "+9", "-9",
1660                        "+1:0", "+01:0", "+1:00", "+01:000", "+01:60",
1661                        "-1:0", "-01:0", "-1:00", "-01:000", "-01:60",
1662                        "000", "00000", "0160", "-0160",
1663                        " +08:00", "+ 08:00", "+08 :00", "+08: 00", "+08:00 ",
1664                        " -08:00", "- 08:00", "-08 :00", "-08: 00", "-08:00 ",
1665                        " +0800", "+ 0800", "+08 00", "+08 00", "+0800 ",
1666                        " -0800", "- 0800", "-08 00", "-08 00", "-0800 ",
1667                        "+ab:cd", "abcd", "+0Z:00", "+Z", "+00Z",
1668                        "-ab:cd", "abcd", "-0Z:00", "-Z", "-00Z",
1669                        "0100", "1200", "2359"])
1670         {
1671             assertThrown!DateTimeException(SimpleTimeZone.fromISOExtString(str), format("[%s]", str));
1672         }
1673 
1674         static void test(string str, Duration utcOffset, size_t line = __LINE__)
1675         {
1676             if (SimpleTimeZone.fromISOExtString(str).utcOffset != (new immutable SimpleTimeZone(utcOffset)).utcOffset)
1677                 throw new AssertError("unittest failure", __FILE__, line);
1678         }
1679 
1680         test("+00:00", Duration.zero);
1681         test("+00:01", minutes(1));
1682         test("+00:10", minutes(10));
1683         test("+00:59", minutes(59));
1684         test("+01:00", hours(1));
1685         test("+01:30", hours(1) + minutes(30));
1686         test("+02:00", hours(2));
1687         test("+08:00", hours(8));
1688         test("+23:59", hours(23) + minutes(59));
1689 
1690         test("-00:01", minutes(-1));
1691         test("-00:10", minutes(-10));
1692         test("-00:59", minutes(-59));
1693         test("-01:00", hours(-1));
1694         test("-01:30", hours(-1) - minutes(30));
1695         test("-02:00", hours(-2));
1696         test("-08:00", hours(-8));
1697         test("-23:59", hours(-23) - minutes(59));
1698 
1699         test("+00", Duration.zero);
1700         test("+01", hours(1));
1701         test("+02", hours(2));
1702         test("+12", hours(12));
1703         test("+23", hours(23));
1704 
1705         test("-00", Duration.zero);
1706         test("-01", hours(-1));
1707         test("-02", hours(-2));
1708         test("-12", hours(-12));
1709         test("-23", hours(-23));
1710     }
1711 
1712     @safe unittest
1713     {
1714         import core.exception : AssertError;
1715         import std.format : format;
1716 
1717         static void test(in string isoExtString, int expectedOffset, size_t line = __LINE__)
1718         {
1719             auto stz = SimpleTimeZone.fromISOExtString(isoExtString);
1720             if (stz.utcOffset != dur!"minutes"(expectedOffset))
1721                 throw new AssertError(format("unittest failure: wrong offset [%s]", stz.utcOffset), __FILE__, line);
1722 
1723             auto result = SimpleTimeZone.toISOExtString(stz.utcOffset);
1724             if (result != isoExtString)
1725                 throw new AssertError(format("unittest failure: [%s] != [%s]", result, isoExtString), __FILE__, line);
1726         }
1727 
1728         test("+00:00", 0);
1729         test("+00:01", 1);
1730         test("+00:10", 10);
1731         test("+00:59", 59);
1732         test("+01:00", 60);
1733         test("+01:30", 90);
1734         test("+02:00", 120);
1735         test("+08:00", 480);
1736         test("+08:00", 480);
1737         test("+23:59", 1439);
1738 
1739         test("-00:01", -1);
1740         test("-00:10", -10);
1741         test("-00:59", -59);
1742         test("-01:00", -60);
1743         test("-01:30", -90);
1744         test("-02:00", -120);
1745         test("-08:00", -480);
1746         test("-08:00", -480);
1747         test("-23:59", -1439);
1748     }
1749 
1750 
1751 private:
1752 
1753     immutable Duration _utcOffset;
1754 }
1755 
1756 
1757 /++
1758     Represents a time zone from a TZ Database time zone file. Files from the TZ
1759     Database are how Posix systems hold their time zone information.
1760     Unfortunately, Windows does not use the TZ Database. To use the TZ Database,
1761     use $(D PosixTimeZone) (which reads its information from the TZ Database
1762     files on disk) on Windows by providing the TZ Database files and telling
1763     $(D PosixTimeZone.getTimeZone) where the directory holding them is.
1764 
1765     To get a $(D PosixTimeZone), either call $(D PosixTimeZone.getTimeZone)
1766     (which allows specifying the location the time zone files) or call
1767     $(D TimeZone.getTimeZone) (which will give a $(D PosixTimeZone) on Posix
1768     systems and a $(LREF WindowsTimeZone) on Windows systems).
1769 
1770     Note:
1771         Unless your system's local time zone deals with leap seconds (which is
1772         highly unlikely), then the only way to get a time zone which
1773         takes leap seconds into account is to use $(D PosixTimeZone) with a
1774         time zone whose name starts with "right/". Those time zone files do
1775         include leap seconds, and $(D PosixTimeZone) will take them into account
1776         (though posix systems which use a "right/" time zone as their local time
1777         zone will $(I not) take leap seconds into account even though they're
1778         in the file).
1779 
1780     See_Also:
1781         $(HTTP www.iana.org/time-zones, Home of the TZ Database files)<br>
1782         $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ Database)<br>
1783         $(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of Time
1784           Zones)
1785   +/
1786 final class PosixTimeZone : TimeZone
1787 {
1788     import std.algorithm.searching : countUntil, canFind, startsWith;
1789     import std.file : isDir, isFile, exists, dirEntries, SpanMode, DirEntry;
1790     import std.path : extension;
1791     import std.stdio : File;
1792     import std.string : strip, representation;
1793     import std.traits : isArray, isSomeChar;
1794 public:
1795 
1796     /++
1797         Whether this time zone has Daylight Savings Time at any point in time.
1798         Note that for some time zone types it may not have DST for current
1799         dates but will still return true for $(D hasDST) because the time zone
1800         did at some point have DST.
1801       +/
1802     @property override bool hasDST() @safe const nothrow
1803     {
1804         return _hasDST;
1805     }
1806 
1807 
1808     /++
1809         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
1810         in UTC time (i.e. std time) and returns whether DST is in effect in this
1811         time zone at the given point in time.
1812 
1813         Params:
1814             stdTime = The UTC time that needs to be checked for DST in this time
1815                       zone.
1816       +/
1817     override bool dstInEffect(long stdTime) @safe const nothrow
1818     {
1819         assert(!_transitions.empty);
1820 
1821         immutable unixTime = stdTimeToUnixTime(stdTime);
1822         immutable found = countUntil!"b < a.timeT"(_transitions, unixTime);
1823 
1824         if (found == -1)
1825             return _transitions.back.ttInfo.isDST;
1826 
1827         immutable transition = found == 0 ? _transitions[0] : _transitions[found - 1];
1828 
1829         return transition.ttInfo.isDST;
1830     }
1831 
1832 
1833     /++
1834         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
1835         in UTC time (i.e. std time) and converts it to this time zone's time.
1836 
1837         Params:
1838             stdTime = The UTC time that needs to be adjusted to this time zone's
1839                       time.
1840       +/
1841     override long utcToTZ(long stdTime) @safe const nothrow
1842     {
1843         assert(!_transitions.empty);
1844 
1845         immutable leapSecs = calculateLeapSeconds(stdTime);
1846         immutable unixTime = stdTimeToUnixTime(stdTime);
1847         immutable found = countUntil!"b < a.timeT"(_transitions, unixTime);
1848 
1849         if (found == -1)
1850             return stdTime + convert!("seconds", "hnsecs")(_transitions.back.ttInfo.utcOffset + leapSecs);
1851 
1852         immutable transition = found == 0 ? _transitions[0] : _transitions[found - 1];
1853 
1854         return stdTime + convert!("seconds", "hnsecs")(transition.ttInfo.utcOffset + leapSecs);
1855     }
1856 
1857 
1858     /++
1859         Takes the number of hnsecs (100 ns) since midnight, January 1st, 1 A.D.
1860         in this time zone's time and converts it to UTC (i.e. std time).
1861 
1862         Params:
1863             adjTime = The time in this time zone that needs to be adjusted to
1864                       UTC time.
1865       +/
1866     override long tzToUTC(long adjTime) @safe const nothrow
1867     {
1868         assert(!_transitions.empty);
1869 
1870         immutable leapSecs = calculateLeapSeconds(adjTime);
1871         time_t unixTime = stdTimeToUnixTime(adjTime);
1872         immutable past = unixTime - convert!("days", "seconds")(1);
1873         immutable future = unixTime + convert!("days", "seconds")(1);
1874 
1875         immutable pastFound = countUntil!"b < a.timeT"(_transitions, past);
1876 
1877         if (pastFound == -1)
1878             return adjTime - convert!("seconds", "hnsecs")(_transitions.back.ttInfo.utcOffset + leapSecs);
1879 
1880         immutable futureFound = countUntil!"b < a.timeT"(_transitions[pastFound .. $], future);
1881         immutable pastTrans = pastFound == 0 ? _transitions[0] : _transitions[pastFound - 1];
1882 
1883         if (futureFound == 0)
1884             return adjTime - convert!("seconds", "hnsecs")(pastTrans.ttInfo.utcOffset + leapSecs);
1885 
1886         immutable futureTrans = futureFound == -1 ? _transitions.back
1887                                                   : _transitions[pastFound + futureFound - 1];
1888         immutable pastOffset = pastTrans.ttInfo.utcOffset;
1889 
1890         if (pastOffset < futureTrans.ttInfo.utcOffset)
1891             unixTime -= convert!("hours", "seconds")(1);
1892 
1893         immutable found = countUntil!"b < a.timeT"(_transitions[pastFound .. $], unixTime - pastOffset);
1894 
1895         if (found == -1)
1896             return adjTime - convert!("seconds", "hnsecs")(_transitions.back.ttInfo.utcOffset + leapSecs);
1897 
1898         immutable transition = found == 0 ? pastTrans : _transitions[pastFound + found - 1];
1899 
1900         return adjTime - convert!("seconds", "hnsecs")(transition.ttInfo.utcOffset + leapSecs);
1901     }
1902 
1903 
version(Android)1904     version (Android)
1905     {
1906         // Android concatenates all time zone data into a single file and stores it here.
1907         enum defaultTZDatabaseDir = "/system/usr/share/zoneinfo/";
1908     }
version(Solaris)1909     else version (Solaris)
1910     {
1911         /++
1912             The default directory where the TZ Database files are. It's empty
1913             for Windows, since Windows doesn't have them.
1914           +/
1915         enum defaultTZDatabaseDir = "/usr/share/lib/zoneinfo/";
1916     }
version(Posix)1917     else version (Posix)
1918     {
1919         /++
1920             The default directory where the TZ Database files are. It's empty
1921             for Windows, since Windows doesn't have them.
1922           +/
1923         enum defaultTZDatabaseDir = "/usr/share/zoneinfo/";
1924     }
version(Windows)1925     else version (Windows)
1926     {
1927         /++ The default directory where the TZ Database files are. It's empty
1928             for Windows, since Windows doesn't have them.
1929           +/
1930         enum defaultTZDatabaseDir = "";
1931     }
1932 
1933 
1934     /++
1935         Returns a $(LREF TimeZone) with the give name per the TZ Database. The
1936         time zone information is fetched from the TZ Database time zone files in
1937         the given directory.
1938 
1939         See_Also:
1940             $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ
1941               Database)<br>
1942             $(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of
1943               Time Zones)
1944 
1945         Params:
1946             name          = The TZ Database name of the desired time zone
1947             tzDatabaseDir = The directory where the TZ Database files are
1948                             located. Because these files are not located on
1949                             Windows systems, provide them
1950                             and give their location here to
1951                             use $(LREF PosixTimeZone)s.
1952 
1953         Throws:
1954             $(REF DateTimeException,std,datetime,date) if the given time zone
1955             could not be found or $(D FileException) if the TZ Database file
1956             could not be opened.
1957       +/
1958     // TODO make it possible for tzDatabaseDir to be gzipped tar file rather than an uncompressed
1959     //      directory.
1960     static immutable(PosixTimeZone) getTimeZone(string name, string tzDatabaseDir = defaultTZDatabaseDir) @trusted
1961     {
1962         import std.algorithm.sorting : sort;
1963         import std.conv : to;
1964         import std.format : format;
1965         import std.path : asNormalizedPath, chainPath;
1966         import std.range : retro;
1967 
1968         name = strip(name);
1969 
1970         enforce(tzDatabaseDir.exists(), new DateTimeException(format("Directory %s does not exist.", tzDatabaseDir)));
1971         enforce(tzDatabaseDir.isDir, new DateTimeException(format("%s is not a directory.", tzDatabaseDir)));
1972 
version(Android)1973         version (Android)
1974         {
1975             auto tzfileOffset = name in tzdataIndex(tzDatabaseDir);
1976             enforce(tzfileOffset, new DateTimeException(format("The time zone %s is not listed.", name)));
1977             string tzFilename = separate_index ? "zoneinfo.dat" : "tzdata";
1978             const file = asNormalizedPath(chainPath(tzDatabaseDir, tzFilename)).to!string;
1979         }
1980         else
1981             const file = asNormalizedPath(chainPath(tzDatabaseDir, name)).to!string;
1982 
1983         enforce(file.exists(), new DateTimeException(format("File %s does not exist.", file)));
1984         enforce(file.isFile, new DateTimeException(format("%s is not a file.", file)));
1985 
1986         auto tzFile = File(file);
1987         version (Android) tzFile.seek(*tzfileOffset);
1988         immutable gmtZone = name.representation().canFind("GMT");
1989 
1990         try
1991         {
1992             _enforceValidTZFile(readVal!(char[])(tzFile, 4) == "TZif");
1993 
1994             immutable char tzFileVersion = readVal!char(tzFile);
1995             _enforceValidTZFile(tzFileVersion == '\0' || tzFileVersion == '2' || tzFileVersion == '3');
1996 
1997             {
1998                 auto zeroBlock = readVal!(ubyte[])(tzFile, 15);
1999                 bool allZeroes = true;
2000 
foreach(val;zeroBlock)2001                 foreach (val; zeroBlock)
2002                 {
2003                     if (val != 0)
2004                     {
2005                         allZeroes = false;
2006                         break;
2007                     }
2008                 }
2009 
2010                 _enforceValidTZFile(allZeroes);
2011             }
2012 
2013 
2014             // The number of UTC/local indicators stored in the file.
2015             auto tzh_ttisgmtcnt = readVal!int(tzFile);
2016 
2017             // The number of standard/wall indicators stored in the file.
2018             auto tzh_ttisstdcnt = readVal!int(tzFile);
2019 
2020             // The number of leap seconds for which data is stored in the file.
2021             auto tzh_leapcnt = readVal!int(tzFile);
2022 
2023             // The number of "transition times" for which data is stored in the file.
2024             auto tzh_timecnt = readVal!int(tzFile);
2025 
2026             // The number of "local time types" for which data is stored in the file (must not be zero).
2027             auto tzh_typecnt = readVal!int(tzFile);
2028             _enforceValidTZFile(tzh_typecnt != 0);
2029 
2030             // The number of characters of "timezone abbreviation strings" stored in the file.
2031             auto tzh_charcnt = readVal!int(tzFile);
2032 
2033             // time_ts where DST transitions occur.
2034             auto transitionTimeTs = new long[](tzh_timecnt);
2035             foreach (ref transition; transitionTimeTs)
2036                 transition = readVal!int(tzFile);
2037 
2038             // Indices into ttinfo structs indicating the changes
2039             // to be made at the corresponding DST transition.
2040             auto ttInfoIndices = new ubyte[](tzh_timecnt);
2041             foreach (ref ttInfoIndex; ttInfoIndices)
2042                 ttInfoIndex = readVal!ubyte(tzFile);
2043 
2044             // ttinfos which give info on DST transitions.
2045             auto tempTTInfos = new TempTTInfo[](tzh_typecnt);
2046             foreach (ref ttInfo; tempTTInfos)
2047                 ttInfo = readVal!TempTTInfo(tzFile);
2048 
2049             // The array of time zone abbreviation characters.
2050             auto tzAbbrevChars = readVal!(char[])(tzFile, tzh_charcnt);
2051 
2052             auto leapSeconds = new LeapSecond[](tzh_leapcnt);
foreach(ref leapSecond;leapSeconds)2053             foreach (ref leapSecond; leapSeconds)
2054             {
2055                 // The time_t when the leap second occurs.
2056                 auto timeT = readVal!int(tzFile);
2057 
2058                 // The total number of leap seconds to be applied after
2059                 // the corresponding leap second.
2060                 auto total = readVal!int(tzFile);
2061 
2062                 leapSecond = LeapSecond(timeT, total);
2063             }
2064 
2065             // Indicate whether each corresponding DST transition were specified
2066             // in standard time or wall clock time.
2067             auto transitionIsStd = new bool[](tzh_ttisstdcnt);
2068             foreach (ref isStd; transitionIsStd)
2069                 isStd = readVal!bool(tzFile);
2070 
2071             // Indicate whether each corresponding DST transition associated with
2072             // local time types are specified in UTC or local time.
2073             auto transitionInUTC = new bool[](tzh_ttisgmtcnt);
2074             foreach (ref inUTC; transitionInUTC)
2075                 inUTC = readVal!bool(tzFile);
2076 
2077             _enforceValidTZFile(!tzFile.eof);
2078 
2079             // If version 2 or 3, the information is duplicated in 64-bit.
2080             if (tzFileVersion == '2' || tzFileVersion == '3')
2081             {
2082                 _enforceValidTZFile(readVal!(char[])(tzFile, 4) == "TZif");
2083 
2084                 immutable char tzFileVersion2 = readVal!(char)(tzFile);
2085                 _enforceValidTZFile(tzFileVersion2 == '2' || tzFileVersion2 == '3');
2086 
2087                 {
2088                     auto zeroBlock = readVal!(ubyte[])(tzFile, 15);
2089                     bool allZeroes = true;
2090 
foreach(val;zeroBlock)2091                     foreach (val; zeroBlock)
2092                     {
2093                         if (val != 0)
2094                         {
2095                             allZeroes = false;
2096                             break;
2097                         }
2098                     }
2099 
2100                     _enforceValidTZFile(allZeroes);
2101                 }
2102 
2103 
2104                 // The number of UTC/local indicators stored in the file.
2105                 tzh_ttisgmtcnt = readVal!int(tzFile);
2106 
2107                 // The number of standard/wall indicators stored in the file.
2108                 tzh_ttisstdcnt = readVal!int(tzFile);
2109 
2110                 // The number of leap seconds for which data is stored in the file.
2111                 tzh_leapcnt = readVal!int(tzFile);
2112 
2113                 // The number of "transition times" for which data is stored in the file.
2114                 tzh_timecnt = readVal!int(tzFile);
2115 
2116                 // The number of "local time types" for which data is stored in the file (must not be zero).
2117                 tzh_typecnt = readVal!int(tzFile);
2118                 _enforceValidTZFile(tzh_typecnt != 0);
2119 
2120                 // The number of characters of "timezone abbreviation strings" stored in the file.
2121                 tzh_charcnt = readVal!int(tzFile);
2122 
2123                 // time_ts where DST transitions occur.
2124                 transitionTimeTs = new long[](tzh_timecnt);
2125                 foreach (ref transition; transitionTimeTs)
2126                     transition = readVal!long(tzFile);
2127 
2128                 // Indices into ttinfo structs indicating the changes
2129                 // to be made at the corresponding DST transition.
2130                 ttInfoIndices = new ubyte[](tzh_timecnt);
2131                 foreach (ref ttInfoIndex; ttInfoIndices)
2132                     ttInfoIndex = readVal!ubyte(tzFile);
2133 
2134                 // ttinfos which give info on DST transitions.
2135                 tempTTInfos = new TempTTInfo[](tzh_typecnt);
2136                 foreach (ref ttInfo; tempTTInfos)
2137                     ttInfo = readVal!TempTTInfo(tzFile);
2138 
2139                 // The array of time zone abbreviation characters.
2140                 tzAbbrevChars = readVal!(char[])(tzFile, tzh_charcnt);
2141 
2142                 leapSeconds = new LeapSecond[](tzh_leapcnt);
foreach(ref leapSecond;leapSeconds)2143                 foreach (ref leapSecond; leapSeconds)
2144                 {
2145                     // The time_t when the leap second occurs.
2146                     auto timeT = readVal!long(tzFile);
2147 
2148                     // The total number of leap seconds to be applied after
2149                     // the corresponding leap second.
2150                     auto total = readVal!int(tzFile);
2151 
2152                     leapSecond = LeapSecond(timeT, total);
2153                 }
2154 
2155                 // Indicate whether each corresponding DST transition were specified
2156                 // in standard time or wall clock time.
2157                 transitionIsStd = new bool[](tzh_ttisstdcnt);
2158                 foreach (ref isStd; transitionIsStd)
2159                     isStd = readVal!bool(tzFile);
2160 
2161                 // Indicate whether each corresponding DST transition associated with
2162                 // local time types are specified in UTC or local time.
2163                 transitionInUTC = new bool[](tzh_ttisgmtcnt);
2164                 foreach (ref inUTC; transitionInUTC)
2165                     inUTC = readVal!bool(tzFile);
2166             }
2167 
2168             _enforceValidTZFile(tzFile.readln().strip().empty);
2169 
2170             cast(void) tzFile.readln();
2171 
version(Android)2172             version (Android)
2173             {
2174                 // Android uses a single file for all timezone data, so the file
2175                 // doesn't end here.
2176             }
2177             else
2178             {
2179                 _enforceValidTZFile(tzFile.readln().strip().empty);
2180                 _enforceValidTZFile(tzFile.eof);
2181             }
2182 
2183 
2184             auto transitionTypes = new TransitionType*[](tempTTInfos.length);
2185 
foreach(i,ref ttype;transitionTypes)2186             foreach (i, ref ttype; transitionTypes)
2187             {
2188                 bool isStd = false;
2189 
2190                 if (i < transitionIsStd.length && !transitionIsStd.empty)
2191                     isStd = transitionIsStd[i];
2192 
2193                 bool inUTC = false;
2194 
2195                 if (i < transitionInUTC.length && !transitionInUTC.empty)
2196                     inUTC = transitionInUTC[i];
2197 
2198                 ttype = new TransitionType(isStd, inUTC);
2199             }
2200 
2201             auto ttInfos = new immutable(TTInfo)*[](tempTTInfos.length);
foreach(i,ref ttInfo;ttInfos)2202             foreach (i, ref ttInfo; ttInfos)
2203             {
2204                 auto tempTTInfo = tempTTInfos[i];
2205 
2206                 if (gmtZone)
2207                     tempTTInfo.tt_gmtoff = -tempTTInfo.tt_gmtoff;
2208 
2209                 auto abbrevChars = tzAbbrevChars[tempTTInfo.tt_abbrind .. $];
2210                 string abbrev = abbrevChars[0 .. abbrevChars.countUntil('\0')].idup;
2211 
2212                 ttInfo = new immutable(TTInfo)(tempTTInfos[i], abbrev);
2213             }
2214 
2215             auto tempTransitions = new TempTransition[](transitionTimeTs.length);
foreach(i,ref tempTransition;tempTransitions)2216             foreach (i, ref tempTransition; tempTransitions)
2217             {
2218                 immutable ttiIndex = ttInfoIndices[i];
2219                 auto transitionTimeT = transitionTimeTs[i];
2220                 auto ttype = transitionTypes[ttiIndex];
2221                 auto ttInfo = ttInfos[ttiIndex];
2222 
2223                 tempTransition = TempTransition(transitionTimeT, ttInfo, ttype);
2224             }
2225 
2226             if (tempTransitions.empty)
2227             {
2228                 _enforceValidTZFile(ttInfos.length == 1 && transitionTypes.length == 1);
2229                 tempTransitions ~= TempTransition(0, ttInfos[0], transitionTypes[0]);
2230             }
2231 
2232             sort!"a.timeT < b.timeT"(tempTransitions);
2233             sort!"a.timeT < b.timeT"(leapSeconds);
2234 
2235             auto transitions = new Transition[](tempTransitions.length);
foreach(i,ref transition;transitions)2236             foreach (i, ref transition; transitions)
2237             {
2238                 auto tempTransition = tempTransitions[i];
2239                 auto transitionTimeT = tempTransition.timeT;
2240                 auto ttInfo = tempTransition.ttInfo;
2241 
2242                 _enforceValidTZFile(i == 0 || transitionTimeT > tempTransitions[i - 1].timeT);
2243 
2244                 transition = Transition(transitionTimeT, ttInfo);
2245             }
2246 
2247             string stdName;
2248             string dstName;
2249             bool hasDST = false;
2250 
foreach(transition;retro (transitions))2251             foreach (transition; retro(transitions))
2252             {
2253                 auto ttInfo = transition.ttInfo;
2254 
2255                 if (ttInfo.isDST)
2256                 {
2257                     if (dstName.empty)
2258                         dstName = ttInfo.abbrev;
2259                     hasDST = true;
2260                 }
2261                 else
2262                 {
2263                     if (stdName.empty)
2264                         stdName = ttInfo.abbrev;
2265                 }
2266 
2267                 if (!stdName.empty && !dstName.empty)
2268                     break;
2269             }
2270 
2271             return new immutable PosixTimeZone(transitions.idup, leapSeconds.idup, name, stdName, dstName, hasDST);
2272         }
2273         catch (DateTimeException dte)
2274             throw dte;
2275         catch (Exception e)
2276             throw new DateTimeException("Not a valid TZ data file", __FILE__, __LINE__, e);
2277     }
2278 
2279     ///
2280     @safe unittest
2281     {
version(Posix)2282         version (Posix)
2283         {
2284             auto tz = PosixTimeZone.getTimeZone("America/Los_Angeles");
2285 
2286             assert(tz.name == "America/Los_Angeles");
2287             assert(tz.stdName == "PST");
2288             assert(tz.dstName == "PDT");
2289         }
2290     }
2291 
2292     /++
2293         Returns a list of the names of the time zones installed on the system.
2294 
2295         Providing a sub-name narrows down the list of time zones (which
2296         can number in the thousands). For example,
2297         passing in "America" as the sub-name returns only the time zones which
2298         begin with "America".
2299 
2300         Params:
2301             subName       = The first part of the desired time zones.
2302             tzDatabaseDir = The directory where the TZ Database files are
2303                             located.
2304 
2305         Throws:
2306             $(D FileException) if it fails to read from disk.
2307       +/
2308     static string[] getInstalledTZNames(string subName = "", string tzDatabaseDir = defaultTZDatabaseDir) @trusted
2309     {
2310         import std.algorithm.sorting : sort;
2311         import std.array : appender;
2312         import std.format : format;
2313 
2314         version (Posix)
2315             subName = strip(subName);
version(Windows)2316         else version (Windows)
2317         {
2318             import std.array : replace;
2319             import std.path : dirSeparator;
2320             subName = replace(strip(subName), "/", dirSeparator);
2321         }
2322 
2323         enforce(tzDatabaseDir.exists(), new DateTimeException(format("Directory %s does not exist.", tzDatabaseDir)));
2324         enforce(tzDatabaseDir.isDir, new DateTimeException(format("%s is not a directory.", tzDatabaseDir)));
2325 
2326         auto timezones = appender!(string[])();
2327 
version(Android)2328         version (Android)
2329         {
2330             import std.algorithm.iteration : filter;
2331             import std.algorithm.mutation : copy;
2332             tzdataIndex(tzDatabaseDir).byKey.filter!(a => a.startsWith(subName)).copy(timezones);
2333         }
2334         else
2335         {
2336             foreach (DirEntry de; dirEntries(tzDatabaseDir, SpanMode.depth))
2337             {
2338                 if (de.isFile)
2339                 {
2340                     auto tzName = de.name[tzDatabaseDir.length .. $];
2341 
2342                     if (!tzName.extension().empty ||
2343                         !tzName.startsWith(subName) ||
2344                         tzName == "leapseconds" ||
2345                         tzName == "+VERSION")
2346                     {
2347                         continue;
2348                     }
2349 
2350                     timezones.put(tzName);
2351                 }
2352             }
2353         }
2354 
2355         sort(timezones.data);
2356 
2357         return timezones.data;
2358     }
2359 
version(Posix)2360     version (Posix) @system unittest
2361     {
2362         import std.exception : assertNotThrown;
2363         import std.stdio : writefln;
2364         static void testPTZSuccess(string tzName)
2365         {
2366             scope(failure) writefln("TZName which threw: %s", tzName);
2367 
2368             PosixTimeZone.getTimeZone(tzName);
2369         }
2370 
2371         static void testPTZFailure(string tzName)
2372         {
2373             scope(success) writefln("TZName which was supposed to throw: %s", tzName);
2374 
2375             PosixTimeZone.getTimeZone(tzName);
2376         }
2377 
2378         auto tzNames = getInstalledTZNames();
2379 
2380         foreach (tzName; tzNames)
2381             assertNotThrown!DateTimeException(testPTZSuccess(tzName));
2382 
2383         // No timezone directories on Android, just a single tzdata file
2384         version (Android)
2385         {}
2386         else
2387         {
2388             foreach (DirEntry de; dirEntries(defaultTZDatabaseDir, SpanMode.depth))
2389             {
2390                 if (de.isFile)
2391                 {
2392                     auto tzName = de.name[defaultTZDatabaseDir.length .. $];
2393 
2394                     if (!canFind(tzNames, tzName))
2395                         assertThrown!DateTimeException(testPTZFailure(tzName));
2396                 }
2397             }
2398         }
2399     }
2400 
2401 
2402 private:
2403 
2404     /+
2405         Holds information on when a time transition occures (usually a
2406         transition to or from DST) as well as a pointer to the $(D TTInfo) which
2407         holds information on the utc offset past the transition.
2408       +/
2409     struct Transition
2410     {
thisTransition2411         this(long timeT, immutable (TTInfo)* ttInfo) @safe pure
2412         {
2413             this.timeT = timeT;
2414             this.ttInfo = ttInfo;
2415         }
2416 
2417         long    timeT;
2418         immutable (TTInfo)* ttInfo;
2419     }
2420 
2421 
2422     /+
2423         Holds information on when a leap second occurs.
2424       +/
2425     struct LeapSecond
2426     {
this(long timeT,int total)2427         this(long timeT, int total) @safe pure
2428         {
2429             this.timeT = timeT;
2430             this.total = total;
2431         }
2432 
2433         long timeT;
2434         int total;
2435     }
2436 
2437     /+
2438         Holds information on the utc offset after a transition as well as
2439         whether DST is in effect after that transition.
2440       +/
2441     struct TTInfo
2442     {
thisTTInfo2443         this(in TempTTInfo tempTTInfo, string abbrev) @safe immutable pure
2444         {
2445             utcOffset = tempTTInfo.tt_gmtoff;
2446             isDST = tempTTInfo.tt_isdst;
2447             this.abbrev = abbrev;
2448         }
2449 
2450         immutable int    utcOffset;  // Offset from UTC.
2451         immutable bool   isDST;      // Whether DST is in effect.
2452         immutable string abbrev;     // The current abbreviation for the time zone.
2453     }
2454 
2455 
2456     /+
2457         Struct used to hold information relating to $(D TTInfo) while organizing
2458         the time zone information prior to putting it in its final form.
2459       +/
2460     struct TempTTInfo
2461     {
this(int gmtOff,bool isDST,ubyte abbrInd)2462         this(int gmtOff, bool isDST, ubyte abbrInd) @safe pure
2463         {
2464             tt_gmtoff = gmtOff;
2465             tt_isdst = isDST;
2466             tt_abbrind = abbrInd;
2467         }
2468 
2469         int   tt_gmtoff;
2470         bool  tt_isdst;
2471         ubyte tt_abbrind;
2472     }
2473 
2474 
2475     /+
2476         Struct used to hold information relating to $(D Transition) while
2477         organizing the time zone information prior to putting it in its final
2478         form.
2479       +/
2480     struct TempTransition
2481     {
thisTempTransition2482         this(long timeT, immutable (TTInfo)* ttInfo, TransitionType* ttype) @safe pure
2483         {
2484             this.timeT = timeT;
2485             this.ttInfo = ttInfo;
2486             this.ttype = ttype;
2487         }
2488 
2489         long                timeT;
2490         immutable (TTInfo)* ttInfo;
2491         TransitionType*     ttype;
2492     }
2493 
2494 
2495     /+
2496         Struct used to hold information relating to $(D Transition) and
2497         $(D TTInfo) while organizing the time zone information prior to putting
2498         it in its final form.
2499       +/
2500     struct TransitionType
2501     {
this(bool isStd,bool inUTC)2502         this(bool isStd, bool inUTC) @safe pure
2503         {
2504             this.isStd = isStd;
2505             this.inUTC = inUTC;
2506         }
2507 
2508         // Whether the transition is in std time (as opposed to wall clock time).
2509         bool isStd;
2510 
2511         // Whether the transition is in UTC (as opposed to local time).
2512         bool inUTC;
2513     }
2514 
2515 
2516     /+
2517         Reads an int from a TZ file.
2518       +/
2519     static T readVal(T)(ref File tzFile) @trusted
2520         if ((isIntegral!T || isSomeChar!T) || is(Unqual!T == bool))
2521     {
2522         import std.bitmanip : bigEndianToNative;
2523         T[1] buff;
2524 
2525         _enforceValidTZFile(!tzFile.eof);
2526         tzFile.rawRead(buff);
2527 
2528         return bigEndianToNative!T(cast(ubyte[T.sizeof]) buff);
2529     }
2530 
2531     /+
2532         Reads an array of values from a TZ file.
2533       +/
2534     static T readVal(T)(ref File tzFile, size_t length) @trusted
2535         if (isArray!T)
2536     {
2537         auto buff = new T(length);
2538 
2539         _enforceValidTZFile(!tzFile.eof);
2540         tzFile.rawRead(buff);
2541 
2542         return buff;
2543     }
2544 
2545 
2546     /+
2547         Reads a $(D TempTTInfo) from a TZ file.
2548       +/
2549     static T readVal(T)(ref File tzFile) @safe
2550         if (is(T == TempTTInfo))
2551     {
2552         return TempTTInfo(readVal!int(tzFile),
2553                           readVal!bool(tzFile),
2554                           readVal!ubyte(tzFile));
2555     }
2556 
2557 
2558     /+
2559         Throws:
2560             $(REF DateTimeException,std,datetime,date) if $(D result) is false.
2561       +/
2562     static void _enforceValidTZFile(bool result, size_t line = __LINE__) @safe pure
2563     {
2564         if (!result)
2565             throw new DateTimeException("Not a valid tzdata file.", __FILE__, line);
2566     }
2567 
2568 
calculateLeapSeconds(long stdTime)2569     int calculateLeapSeconds(long stdTime) @safe const pure nothrow
2570     {
2571         if (_leapSeconds.empty)
2572             return 0;
2573 
2574         immutable unixTime = stdTimeToUnixTime(stdTime);
2575 
2576         if (_leapSeconds.front.timeT >= unixTime)
2577             return 0;
2578 
2579         immutable found = countUntil!"b < a.timeT"(_leapSeconds, unixTime);
2580 
2581         if (found == -1)
2582             return _leapSeconds.back.total;
2583 
2584         immutable leapSecond = found == 0 ? _leapSeconds[0] : _leapSeconds[found - 1];
2585 
2586         return leapSecond.total;
2587     }
2588 
2589 
this(immutable Transition[]transitions,immutable LeapSecond[]leapSeconds,string name,string stdName,string dstName,bool hasDST)2590     this(immutable Transition[] transitions,
2591          immutable LeapSecond[] leapSeconds,
2592          string name,
2593          string stdName,
2594          string dstName,
2595          bool hasDST) @safe immutable pure
2596     {
2597         if (dstName.empty && !stdName.empty)
2598             dstName = stdName;
2599         else if (stdName.empty && !dstName.empty)
2600             stdName = dstName;
2601 
2602         super(name, stdName, dstName);
2603 
2604         if (!transitions.empty)
2605         {
2606             foreach (i, transition; transitions[0 .. $-1])
2607                 _enforceValidTZFile(transition.timeT < transitions[i + 1].timeT);
2608         }
2609 
2610         foreach (i, leapSecond; leapSeconds)
2611             _enforceValidTZFile(i == leapSeconds.length - 1 || leapSecond.timeT < leapSeconds[i + 1].timeT);
2612 
2613         _transitions = transitions;
2614         _leapSeconds = leapSeconds;
2615         _hasDST = hasDST;
2616     }
2617 
2618     // Android concatenates the usual timezone directories into a single file,
2619     // tzdata, along with an index to jump to each timezone's offset.  In older
2620     // versions of Android, the index was stored in a separate file, zoneinfo.idx,
2621     // whereas now it's stored at the beginning of tzdata.
version(Android)2622     version (Android)
2623     {
2624         // Keep track of whether there's a separate index, zoneinfo.idx.  Only
2625         // check this after calling tzdataIndex, as it's initialized there.
2626         static shared bool separate_index;
2627 
2628         // Extracts the name of each time zone and the offset where its data is
2629         // located in the tzdata file from the index and caches it for later.
2630         static const(uint[string]) tzdataIndex(string tzDir)
2631         {
2632             import std.concurrency : initOnce;
2633 
2634             static __gshared uint[string] _tzIndex;
2635 
2636             // _tzIndex is initialized once and then shared across all threads.
2637             initOnce!_tzIndex(
2638             {
2639                 import std.conv : to;
2640                 import std.format : format;
2641                 import std.path : asNormalizedPath, chainPath;
2642 
2643                 enum indexEntrySize = 52;
2644                 const combinedFile = asNormalizedPath(chainPath(tzDir, "tzdata")).to!string;
2645                 const indexFile = asNormalizedPath(chainPath(tzDir, "zoneinfo.idx")).to!string;
2646                 File tzFile;
2647                 uint indexEntries, dataOffset;
2648                 uint[string] initIndex;
2649 
2650                 // Check for the combined file tzdata, which stores the index
2651                 // and the time zone data together.
2652                 if (combinedFile.exists() && combinedFile.isFile)
2653                 {
2654                     tzFile = File(combinedFile);
2655                     _enforceValidTZFile(readVal!(char[])(tzFile, 6) == "tzdata");
2656                     auto tzDataVersion = readVal!(char[])(tzFile, 6);
2657                     _enforceValidTZFile(tzDataVersion[5] == '\0');
2658 
2659                     uint indexOffset = readVal!uint(tzFile);
2660                     dataOffset = readVal!uint(tzFile);
2661                     readVal!uint(tzFile);
2662 
2663                     indexEntries = (dataOffset - indexOffset) / indexEntrySize;
2664                     separate_index = false;
2665                 }
2666                 else if (indexFile.exists() && indexFile.isFile)
2667                 {
2668                     tzFile = File(indexFile);
2669                     indexEntries = to!uint(tzFile.size/indexEntrySize);
2670                     separate_index = true;
2671                 }
2672                 else
2673                 {
2674                     throw new DateTimeException(format("Both timezone files %s and %s do not exist.",
2675                                                        combinedFile, indexFile));
2676                 }
2677 
2678                 foreach (_; 0 .. indexEntries)
2679                 {
2680                     string tzName = to!string(readVal!(char[])(tzFile, 40).ptr);
2681                     uint tzOffset = readVal!uint(tzFile);
2682                     readVal!(uint[])(tzFile, 2);
2683                     initIndex[tzName] = dataOffset + tzOffset;
2684                 }
2685                 initIndex.rehash;
2686                 return initIndex;
2687             }());
2688             return _tzIndex;
2689         }
2690     }
2691 
2692     // List of times when the utc offset changes.
2693     immutable Transition[] _transitions;
2694 
2695     // List of leap second occurrences.
2696     immutable LeapSecond[] _leapSeconds;
2697 
2698     // Whether DST is in effect for this time zone at any point in time.
2699     immutable bool _hasDST;
2700 }
2701 
2702 
version(StdDdoc)2703 version (StdDdoc)
2704 {
2705     /++
2706         $(BLUE This class is Windows-Only.)
2707 
2708         Represents a time zone from the Windows registry. Unfortunately, Windows
2709         does not use the TZ Database. To use the TZ Database, use
2710         $(LREF PosixTimeZone) (which reads its information from the TZ Database
2711         files on disk) on Windows by providing the TZ Database files and telling
2712         $(D PosixTimeZone.getTimeZone) where the directory holding them is.
2713 
2714         The TZ Database files and Windows' time zone information frequently
2715         do not match. Windows has many errors with regards to when DST switches
2716         occur (especially for historical dates). Also, the TZ Database files
2717         include far more time zones than Windows does. So, for accurate
2718         time zone information, use the TZ Database files with
2719         $(LREF PosixTimeZone) rather than $(D WindowsTimeZone). However, because
2720         $(D WindowsTimeZone) uses Windows system calls to deal with the time,
2721         it's far more likely to match the behavior of other Windows programs.
2722         Be aware of the differences when selecting a method.
2723 
2724         $(D WindowsTimeZone) does not exist on Posix systems.
2725 
2726         To get a $(D WindowsTimeZone), either call
2727         $(D WindowsTimeZone.getTimeZone) or call $(D TimeZone.getTimeZone)
2728         (which will give a $(LREF PosixTimeZone) on Posix systems and a
2729          $(D WindowsTimeZone) on Windows systems).
2730 
2731         See_Also:
2732             $(HTTP www.iana.org/time-zones, Home of the TZ Database files)
2733       +/
2734     final class WindowsTimeZone : TimeZone
2735     {
2736     public:
2737 
2738         /++
2739             Whether this time zone has Daylight Savings Time at any point in
2740             time. Note that for some time zone types it may not have DST for
2741             current dates but will still return true for $(D hasDST) because the
2742             time zone did at some point have DST.
2743           +/
2744         @property override bool hasDST() @safe const nothrow;
2745 
2746 
2747         /++
2748             Takes the number of hnsecs (100 ns) since midnight, January 1st,
2749             1 A.D. in UTC time (i.e. std time) and returns whether DST is in
2750             effect in this time zone at the given point in time.
2751 
2752             Params:
2753                 stdTime = The UTC time that needs to be checked for DST in this
2754                           time zone.
2755           +/
2756         override bool dstInEffect(long stdTime) @safe const nothrow;
2757 
2758 
2759         /++
2760             Takes the number of hnsecs (100 ns) since midnight, January 1st,
2761             1 A.D. in UTC time (i.e. std time) and converts it to this time
2762                 zone's time.
2763 
2764             Params:
2765                 stdTime = The UTC time that needs to be adjusted to this time
2766                           zone's time.
2767           +/
2768         override long utcToTZ(long stdTime) @safe const nothrow;
2769 
2770 
2771         /++
2772             Takes the number of hnsecs (100 ns) since midnight, January 1st,
2773             1 A.D. in this time zone's time and converts it to UTC (i.e. std
2774             time).
2775 
2776             Params:
2777                 adjTime = The time in this time zone that needs to be adjusted
2778                           to UTC time.
2779           +/
2780         override long tzToUTC(long adjTime) @safe const nothrow;
2781 
2782 
2783         /++
2784             Returns a $(LREF TimeZone) with the given name per the Windows time
2785             zone names. The time zone information is fetched from the Windows
2786             registry.
2787 
2788             See_Also:
2789                 $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ
2790                   Database)<br>
2791                 $(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List
2792                   of Time Zones)
2793 
2794             Params:
2795                 name = The TZ Database name of the desired time zone.
2796 
2797             Throws:
2798                 $(REF DateTimeException,std,datetime,date) if the given time
2799                 zone could not be found.
2800 
2801             Example:
2802     --------------------
2803     auto tz = WindowsTimeZone.getTimeZone("Pacific Standard Time");
2804     --------------------
2805           +/
2806         static immutable(WindowsTimeZone) getTimeZone(string name) @safe;
2807 
2808 
2809         /++
2810             Returns a list of the names of the time zones installed on the
2811             system. The list returned by WindowsTimeZone contains the Windows
2812             TZ names, not the TZ Database names. However,
2813             $(D TimeZone.getinstalledTZNames) will return the TZ Database names
2814             which are equivalent to the Windows TZ names.
2815           +/
2816         static string[] getInstalledTZNames() @safe;
2817 
2818     private:
2819 
2820         version (Windows)
2821         {}
2822         else
2823             alias TIME_ZONE_INFORMATION = void*;
2824 
2825         static bool _dstInEffect(const TIME_ZONE_INFORMATION* tzInfo, long stdTime) nothrow;
2826         static long _utcToTZ(const TIME_ZONE_INFORMATION* tzInfo, long stdTime, bool hasDST) nothrow;
2827         static long _tzToUTC(const TIME_ZONE_INFORMATION* tzInfo, long adjTime, bool hasDST) nothrow;
2828 
2829         this() immutable pure
2830         {
2831             super("", "", "");
2832         }
2833     }
2834 
2835 }
2836 else version (Windows)
2837 {
2838     final class WindowsTimeZone : TimeZone
2839     {
2840         import std.algorithm.sorting : sort;
2841         import std.array : appender;
2842         import std.conv : to;
2843         import std.format : format;
2844 
2845     public:
2846 
2847         @property override bool hasDST() @safe const nothrow
2848         {
2849             return _tzInfo.DaylightDate.wMonth != 0;
2850         }
2851 
2852 
2853         override bool dstInEffect(long stdTime) @safe const nothrow
2854         {
2855             return _dstInEffect(&_tzInfo, stdTime);
2856         }
2857 
2858 
2859         override long utcToTZ(long stdTime) @safe const nothrow
2860         {
2861             return _utcToTZ(&_tzInfo, stdTime, hasDST);
2862         }
2863 
2864 
2865         override long tzToUTC(long adjTime) @safe const nothrow
2866         {
2867             return _tzToUTC(&_tzInfo, adjTime, hasDST);
2868         }
2869 
2870 
2871         static immutable(WindowsTimeZone) getTimeZone(string name) @trusted
2872         {
2873             scope baseKey = Registry.localMachine.getKey(`Software\Microsoft\Windows NT\CurrentVersion\Time Zones`);
2874 
2875             foreach (tzKeyName; baseKey.keyNames)
2876             {
2877                 if (tzKeyName != name)
2878                     continue;
2879 
2880                 scope tzKey = baseKey.getKey(tzKeyName);
2881 
2882                 scope stdVal = tzKey.getValue("Std");
2883                 auto stdName = stdVal.value_SZ;
2884 
2885                 scope dstVal = tzKey.getValue("Dlt");
2886                 auto dstName = dstVal.value_SZ;
2887 
2888                 scope tziVal = tzKey.getValue("TZI");
2889                 auto binVal = tziVal.value_BINARY;
2890                 assert(binVal.length == REG_TZI_FORMAT.sizeof);
2891                 auto tziFmt = cast(REG_TZI_FORMAT*) binVal.ptr;
2892 
2893                 TIME_ZONE_INFORMATION tzInfo;
2894 
2895                 auto wstdName = stdName.to!wstring;
2896                 auto wdstName = dstName.to!wstring;
2897                 auto wstdNameLen = wstdName.length > 32 ? 32 : wstdName.length;
2898                 auto wdstNameLen = wdstName.length > 32 ? 32 : wdstName.length;
2899 
2900                 tzInfo.Bias = tziFmt.Bias;
2901                 tzInfo.StandardName[0 .. wstdNameLen] = wstdName[0 .. wstdNameLen];
2902                 tzInfo.StandardName[wstdNameLen .. $] = '\0';
2903                 tzInfo.StandardDate = tziFmt.StandardDate;
2904                 tzInfo.StandardBias = tziFmt.StandardBias;
2905                 tzInfo.DaylightName[0 .. wdstNameLen] = wdstName[0 .. wdstNameLen];
2906                 tzInfo.DaylightName[wdstNameLen .. $] = '\0';
2907                 tzInfo.DaylightDate = tziFmt.DaylightDate;
2908                 tzInfo.DaylightBias = tziFmt.DaylightBias;
2909 
2910                 return new immutable WindowsTimeZone(name, tzInfo);
2911             }
2912             throw new DateTimeException(format("Failed to find time zone: %s", name));
2913         }
2914 
2915         static string[] getInstalledTZNames() @trusted
2916         {
2917             auto timezones = appender!(string[])();
2918 
2919             scope baseKey = Registry.localMachine.getKey(`Software\Microsoft\Windows NT\CurrentVersion\Time Zones`);
2920 
2921             foreach (tzKeyName; baseKey.keyNames)
2922                 timezones.put(tzKeyName);
2923             sort(timezones.data);
2924 
2925             return timezones.data;
2926         }
2927 
2928         @safe unittest
2929         {
2930             import std.exception : assertNotThrown;
2931             import std.stdio : writefln;
2932             static void testWTZSuccess(string tzName)
2933             {
2934                 scope(failure) writefln("TZName which threw: %s", tzName);
2935 
2936                 WindowsTimeZone.getTimeZone(tzName);
2937             }
2938 
2939             auto tzNames = getInstalledTZNames();
2940 
2941             foreach (tzName; tzNames)
2942                 assertNotThrown!DateTimeException(testWTZSuccess(tzName));
2943         }
2944 
2945 
2946     private:
2947 
2948         static bool _dstInEffect(const TIME_ZONE_INFORMATION* tzInfo, long stdTime) @trusted nothrow
2949         {
2950             try
2951             {
2952                 if (tzInfo.DaylightDate.wMonth == 0)
2953                     return false;
2954 
2955                 auto utcDateTime = cast(DateTime) SysTime(stdTime, UTC());
2956 
2957                 //The limits of what SystemTimeToTzSpecificLocalTime will accept.
2958                 if (utcDateTime.year < 1601)
2959                 {
2960                     if (utcDateTime.month == Month.feb && utcDateTime.day == 29)
2961                         utcDateTime.day = 28;
2962                     utcDateTime.year = 1601;
2963                 }
2964                 else if (utcDateTime.year > 30_827)
2965                 {
2966                     if (utcDateTime.month == Month.feb && utcDateTime.day == 29)
2967                         utcDateTime.day = 28;
2968                     utcDateTime.year = 30_827;
2969                 }
2970 
2971                 //SystemTimeToTzSpecificLocalTime doesn't act correctly at the
2972                 //beginning or end of the year (bleh). Unless some bizarre time
2973                 //zone changes DST on January 1st or December 31st, this should
2974                 //fix the problem.
2975                 if (utcDateTime.month == Month.jan)
2976                 {
2977                     if (utcDateTime.day == 1)
2978                         utcDateTime.day = 2;
2979                 }
2980                 else if (utcDateTime.month == Month.dec && utcDateTime.day == 31)
2981                     utcDateTime.day = 30;
2982 
2983                 SYSTEMTIME utcTime = void;
2984                 SYSTEMTIME otherTime = void;
2985 
2986                 utcTime.wYear = utcDateTime.year;
2987                 utcTime.wMonth = utcDateTime.month;
2988                 utcTime.wDay = utcDateTime.day;
2989                 utcTime.wHour = utcDateTime.hour;
2990                 utcTime.wMinute = utcDateTime.minute;
2991                 utcTime.wSecond = utcDateTime.second;
2992                 utcTime.wMilliseconds = 0;
2993 
2994                 immutable result = SystemTimeToTzSpecificLocalTime(cast(TIME_ZONE_INFORMATION*) tzInfo,
2995                                                                    &utcTime,
2996                                                                    &otherTime);
2997                 assert(result);
2998 
2999                 immutable otherDateTime = DateTime(otherTime.wYear,
3000                                                    otherTime.wMonth,
3001                                                    otherTime.wDay,
3002                                                    otherTime.wHour,
3003                                                    otherTime.wMinute,
3004                                                    otherTime.wSecond);
3005                 immutable diff = utcDateTime - otherDateTime;
3006                 immutable minutes = diff.total!"minutes" - tzInfo.Bias;
3007 
3008                 if (minutes == tzInfo.DaylightBias)
3009                     return true;
3010 
3011                 assert(minutes == tzInfo.StandardBias);
3012 
3013                 return false;
3014             }
3015             catch (Exception e)
3016                 assert(0, "DateTime's constructor threw.");
3017         }
3018 
3019         @system unittest
3020         {
3021             TIME_ZONE_INFORMATION tzInfo;
3022             GetTimeZoneInformation(&tzInfo);
3023 
3024             foreach (year; [1600, 1601, 30_827, 30_828])
3025                 WindowsTimeZone._dstInEffect(&tzInfo, SysTime(DateTime(year, 1, 1)).stdTime);
3026         }
3027 
3028 
3029         static long _utcToTZ(const TIME_ZONE_INFORMATION* tzInfo, long stdTime, bool hasDST) @safe nothrow
3030         {
3031             if (hasDST && WindowsTimeZone._dstInEffect(tzInfo, stdTime))
3032                 return stdTime - convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.DaylightBias);
3033 
3034             return stdTime - convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.StandardBias);
3035         }
3036 
3037 
3038         static long _tzToUTC(const TIME_ZONE_INFORMATION* tzInfo, long adjTime, bool hasDST) @trusted nothrow
3039         {
3040             if (hasDST)
3041             {
3042                 try
3043                 {
3044                     bool dstInEffectForLocalDateTime(DateTime localDateTime)
3045                     {
3046                         // The limits of what SystemTimeToTzSpecificLocalTime will accept.
3047                         if (localDateTime.year < 1601)
3048                         {
3049                             if (localDateTime.month == Month.feb && localDateTime.day == 29)
3050                                 localDateTime.day = 28;
3051 
3052                             localDateTime.year = 1601;
3053                         }
3054                         else if (localDateTime.year > 30_827)
3055                         {
3056                             if (localDateTime.month == Month.feb && localDateTime.day == 29)
3057                                 localDateTime.day = 28;
3058 
3059                             localDateTime.year = 30_827;
3060                         }
3061 
3062                         // SystemTimeToTzSpecificLocalTime doesn't act correctly at the
3063                         // beginning or end of the year (bleh). Unless some bizarre time
3064                         // zone changes DST on January 1st or December 31st, this should
3065                         // fix the problem.
3066                         if (localDateTime.month == Month.jan)
3067                         {
3068                             if (localDateTime.day == 1)
3069                                 localDateTime.day = 2;
3070                         }
3071                         else if (localDateTime.month == Month.dec && localDateTime.day == 31)
3072                             localDateTime.day = 30;
3073 
3074                         SYSTEMTIME utcTime = void;
3075                         SYSTEMTIME localTime = void;
3076 
3077                         localTime.wYear = localDateTime.year;
3078                         localTime.wMonth = localDateTime.month;
3079                         localTime.wDay = localDateTime.day;
3080                         localTime.wHour = localDateTime.hour;
3081                         localTime.wMinute = localDateTime.minute;
3082                         localTime.wSecond = localDateTime.second;
3083                         localTime.wMilliseconds = 0;
3084 
3085                         immutable result = TzSpecificLocalTimeToSystemTime(cast(TIME_ZONE_INFORMATION*) tzInfo,
3086                                                                            &localTime,
3087                                                                            &utcTime);
3088                         assert(result);
3089 
3090                         immutable utcDateTime = DateTime(utcTime.wYear,
3091                                                          utcTime.wMonth,
3092                                                          utcTime.wDay,
3093                                                          utcTime.wHour,
3094                                                          utcTime.wMinute,
3095                                                          utcTime.wSecond);
3096 
3097                         immutable diff = localDateTime - utcDateTime;
3098                         immutable minutes = -tzInfo.Bias - diff.total!"minutes";
3099 
3100                         if (minutes == tzInfo.DaylightBias)
3101                             return true;
3102 
3103                         assert(minutes == tzInfo.StandardBias);
3104 
3105                         return false;
3106                     }
3107 
3108                     auto localDateTime = cast(DateTime) SysTime(adjTime, UTC());
3109                     auto localDateTimeBefore = localDateTime - dur!"hours"(1);
3110                     auto localDateTimeAfter = localDateTime + dur!"hours"(1);
3111 
3112                     auto dstInEffectNow = dstInEffectForLocalDateTime(localDateTime);
3113                     auto dstInEffectBefore = dstInEffectForLocalDateTime(localDateTimeBefore);
3114                     auto dstInEffectAfter = dstInEffectForLocalDateTime(localDateTimeAfter);
3115 
3116                     bool isDST;
3117 
3118                     if (dstInEffectBefore && dstInEffectNow && dstInEffectAfter)
3119                         isDST = true;
3120                     else if (!dstInEffectBefore && !dstInEffectNow && !dstInEffectAfter)
3121                         isDST = false;
3122                     else if (!dstInEffectBefore && dstInEffectAfter)
3123                         isDST = false;
3124                     else if (dstInEffectBefore && !dstInEffectAfter)
3125                         isDST = dstInEffectNow;
3126                     else
3127                         assert(0, "Bad Logic.");
3128 
3129                     if (isDST)
3130                         return adjTime + convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.DaylightBias);
3131                 }
3132                 catch (Exception e)
3133                     assert(0, "SysTime's constructor threw.");
3134             }
3135 
3136             return adjTime + convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.StandardBias);
3137         }
3138 
3139 
3140         this(string name, TIME_ZONE_INFORMATION tzInfo) @trusted immutable pure
3141         {
3142             super(name, to!string(tzInfo.StandardName.ptr), to!string(tzInfo.DaylightName.ptr));
3143             _tzInfo = tzInfo;
3144         }
3145 
3146 
3147         TIME_ZONE_INFORMATION _tzInfo;
3148     }
3149 }
3150 
3151 
version(StdDdoc)3152 version (StdDdoc)
3153 {
3154     /++
3155         $(BLUE This function is Posix-Only.)
3156 
3157         Sets the local time zone on Posix systems with the TZ
3158         Database name by setting the TZ environment variable.
3159 
3160         Unfortunately, there is no way to do it on Windows using the TZ
3161         Database name, so this function only exists on Posix systems.
3162       +/
3163     void setTZEnvVar(string tzDatabaseName) @safe nothrow;
3164 
3165 
3166     /++
3167         $(BLUE This function is Posix-Only.)
3168 
3169         Clears the TZ environment variable.
3170       +/
3171     void clearTZEnvVar() @safe nothrow;
3172 }
version(Posix)3173 else version (Posix)
3174 {
3175     void setTZEnvVar(string tzDatabaseName) @trusted nothrow
3176     {
3177         import core.stdc.time : tzset;
3178         import core.sys.posix.stdlib : setenv;
3179         import std.internal.cstring : tempCString;
3180         import std.path : asNormalizedPath, chainPath;
3181 
3182         version (Android)
3183             auto value = asNormalizedPath(tzDatabaseName);
3184         else
3185             auto value = asNormalizedPath(chainPath(PosixTimeZone.defaultTZDatabaseDir, tzDatabaseName));
3186         setenv("TZ", value.tempCString(), 1);
3187         tzset();
3188     }
3189 
3190 
3191     void clearTZEnvVar() @trusted nothrow
3192     {
3193         import core.stdc.time : tzset;
3194         import core.sys.posix.stdlib : unsetenv;
3195 
3196         unsetenv("TZ");
3197         tzset();
3198     }
3199 }
3200 
3201 
3202 /++
3203     Provides the conversions between the IANA time zone database time zone names
3204     (which POSIX systems use) and the time zone names that Windows uses.
3205 
3206     Windows uses a different set of time zone names than the IANA time zone
3207     database does, and how they correspond to one another changes over time
3208     (particularly when Microsoft updates Windows).
3209     $(HTTP unicode.org/cldr/data/common/supplemental/windowsZones.xml, windowsZones.xml)
3210     provides the current conversions (which may or may not match up with what's
3211     on a particular Windows box depending on how up-to-date it is), and
3212     parseTZConversions reads in those conversions from windowsZones.xml so that
3213     a D program can use those conversions.
3214 
3215     However, it should be noted that the time zone information on Windows is
3216     frequently less accurate than that in the IANA time zone database, and if
3217     someone really wants accurate time zone information, they should use the
3218     IANA time zone database files with $(LREF PosixTimeZone) on Windows rather
3219     than $(LREF WindowsTimeZone), whereas $(LREF WindowsTimeZone) makes more
3220     sense when trying to match what Windows will think the time is in a specific
3221     time zone.
3222 
3223     Also, the IANA time zone database has a lot more time zones than Windows
3224     does.
3225 
3226     Params:
3227         windowsZonesXMLText = The text from
3228         $(HTTP unicode.org/cldr/data/common/supplemental/windowsZones.xml, windowsZones.xml)
3229 
3230     Throws:
3231         Exception if there is an error while parsing the given XML.
3232 
3233 --------------------
3234     // Parse the conversions from a local file.
3235     auto text = std.file.readText("path/to/windowsZones.xml");
3236     auto conversions = parseTZConversions(text);
3237 
3238     // Alternatively, grab the XML file from the web at runtime
3239     // and parse it so that it's guaranteed to be up-to-date, though
3240     // that has the downside that the code needs to worry about the
3241     // site being down or unicode.org changing the URL.
3242     auto url = "http://unicode.org/cldr/data/common/supplemental/windowsZones.xml";
3243     auto conversions2 = parseTZConversions(std.net.curl.get(url));
3244 --------------------
3245   +/
3246 struct TZConversions
3247 {
3248     /++
3249         The key is the Windows time zone name, and the value is a list of
3250         IANA TZ database names which are close (currently only ever one, but
3251         it allows for multiple in case it's ever necessary).
3252       +/
3253     string[][string] toWindows;
3254 
3255     /++
3256         The key is the IANA time zone database name, and the value is a list of
3257         Windows time zone names which are close (usually only one, but it could
3258         be multiple).
3259       +/
3260     string[][string] fromWindows;
3261 }
3262 
3263 /++ ditto +/
parseTZConversions(string windowsZonesXMLText)3264 TZConversions parseTZConversions(string windowsZonesXMLText) @safe pure
3265 {
3266     // This is a bit hacky, since it doesn't properly read XML, but it avoids
3267     // needing to pull in std.xml (which we're theoretically replacing at some
3268     // point anyway).
3269     import std.algorithm.iteration : uniq;
3270     import std.algorithm.searching : find;
3271     import std.algorithm.sorting : sort;
3272     import std.array : array, split;
3273     import std.string : lineSplitter;
3274 
3275     string[][string] win2Nix;
3276     string[][string] nix2Win;
3277 
3278     immutable f1 = `<mapZone other="`;
3279     immutable f2 = `type="`;
3280 
3281     foreach (line; windowsZonesXMLText.lineSplitter())
3282     {
3283         // Sample line:
3284         // <mapZone other="Canada Central Standard Time" territory="CA" type="America/Regina America/Swift_Current"/>
3285 
3286         line = line.find(f1);
3287         if (line.empty)
3288             continue;
3289         line = line[f1.length .. $];
3290         auto next = line.find('"');
3291         enforce(!next.empty, "Error parsing. Text does not appear to be from windowsZones.xml");
3292         auto win = line[0 .. $ - next.length];
3293         line = next.find(f2);
3294         enforce(!line.empty, "Error parsing. Text does not appear to be from windowsZones.xml");
3295         line = line[f2.length .. $];
3296         next = line.find('"');
3297         enforce(!next.empty, "Error parsing. Text does not appear to be from windowsZones.xml");
3298         auto nixes = line[0 .. $ - next.length].split();
3299 
3300         if (auto n = win in win2Nix)
3301             *n ~= nixes;
3302         else
3303             win2Nix[win] = nixes;
3304 
3305         foreach (nix; nixes)
3306         {
3307             if (auto w = nix in nix2Win)
3308                 *w ~= win;
3309             else
3310                 nix2Win[nix] = [win];
3311         }
3312     }
3313 
3314     foreach (key, ref value; nix2Win)
3315         value = value.sort().uniq().array();
3316     foreach (key, ref value; win2Nix)
3317         value = value.sort().uniq().array();
3318 
3319     return TZConversions(nix2Win, win2Nix);
3320 }
3321 
3322 @safe unittest
3323 {
3324     import std.algorithm.comparison : equal;
3325     import std.algorithm.iteration : uniq;
3326     import std.algorithm.sorting : isSorted;
3327 
3328     // Reduced text from http://unicode.org/cldr/data/common/supplemental/windowsZones.xml
3329     auto sampleFileText =
3330 `<?xml version="1.0" encoding="UTF-8" ?>
3331 <!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
3332 <!--
3333 Copyright © 1991-2013 Unicode, Inc.
3334 CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/)
3335 For terms of use, see http://www.unicode.org/copyright.html
3336 -->
3337 
3338 <supplementalData>
3339     <version number="$Revision$"/>
3340 
3341     <windowsZones>
3342         <mapTimezones otherVersion="7df0005" typeVersion="2015g">
3343 
3344             <!-- (UTC-12:00) International Date Line West -->
3345             <mapZone other="Dateline Standard Time" territory="001" type="Etc/GMT+12"/>
3346             <mapZone other="Dateline Standard Time" territory="ZZ" type="Etc/GMT+12"/>
3347 
3348             <!-- (UTC-11:00) Coordinated Universal Time-11 -->
3349             <mapZone other="UTC-11" territory="001" type="Etc/GMT+11"/>
3350             <mapZone other="UTC-11" territory="AS" type="Pacific/Pago_Pago"/>
3351             <mapZone other="UTC-11" territory="NU" type="Pacific/Niue"/>
3352             <mapZone other="UTC-11" territory="UM" type="Pacific/Midway"/>
3353             <mapZone other="UTC-11" territory="ZZ" type="Etc/GMT+11"/>
3354 
3355             <!-- (UTC-10:00) Hawaii -->
3356             <mapZone other="Hawaiian Standard Time" territory="001" type="Pacific/Honolulu"/>
3357             <mapZone other="Hawaiian Standard Time" territory="CK" type="Pacific/Rarotonga"/>
3358             <mapZone other="Hawaiian Standard Time" territory="PF" type="Pacific/Tahiti"/>
3359             <mapZone other="Hawaiian Standard Time" territory="UM" type="Pacific/Johnston"/>
3360             <mapZone other="Hawaiian Standard Time" territory="US" type="Pacific/Honolulu"/>
3361             <mapZone other="Hawaiian Standard Time" territory="ZZ" type="Etc/GMT+10"/>
3362 
3363             <!-- (UTC-09:00) Alaska -->
3364             <mapZone other="Alaskan Standard Time" territory="001" type="America/Anchorage"/>
3365             <mapZone other="Alaskan Standard Time" territory="US" type="America/Anchorage America/Juneau America/Nome America/Sitka America/Yakutat"/>
3366         </mapTimezones>
3367     </windowsZones>
3368 </supplementalData>`;
3369 
3370     auto tzConversions = parseTZConversions(sampleFileText);
3371     assert(tzConversions.toWindows.length == 15);
3372     assert(tzConversions.toWindows["America/Anchorage"] == ["Alaskan Standard Time"]);
3373     assert(tzConversions.toWindows["America/Juneau"] == ["Alaskan Standard Time"]);
3374     assert(tzConversions.toWindows["America/Nome"] == ["Alaskan Standard Time"]);
3375     assert(tzConversions.toWindows["America/Sitka"] == ["Alaskan Standard Time"]);
3376     assert(tzConversions.toWindows["America/Yakutat"] == ["Alaskan Standard Time"]);
3377     assert(tzConversions.toWindows["Etc/GMT+10"] == ["Hawaiian Standard Time"]);
3378     assert(tzConversions.toWindows["Etc/GMT+11"] == ["UTC-11"]);
3379     assert(tzConversions.toWindows["Etc/GMT+12"] == ["Dateline Standard Time"]);
3380     assert(tzConversions.toWindows["Pacific/Honolulu"] == ["Hawaiian Standard Time"]);
3381     assert(tzConversions.toWindows["Pacific/Johnston"] == ["Hawaiian Standard Time"]);
3382     assert(tzConversions.toWindows["Pacific/Midway"] == ["UTC-11"]);
3383     assert(tzConversions.toWindows["Pacific/Niue"] == ["UTC-11"]);
3384     assert(tzConversions.toWindows["Pacific/Pago_Pago"] == ["UTC-11"]);
3385     assert(tzConversions.toWindows["Pacific/Rarotonga"] == ["Hawaiian Standard Time"]);
3386     assert(tzConversions.toWindows["Pacific/Tahiti"] == ["Hawaiian Standard Time"]);
3387 
3388     assert(tzConversions.fromWindows.length == 4);
3389     assert(tzConversions.fromWindows["Alaskan Standard Time"] ==
3390            ["America/Anchorage", "America/Juneau", "America/Nome", "America/Sitka", "America/Yakutat"]);
3391     assert(tzConversions.fromWindows["Dateline Standard Time"] == ["Etc/GMT+12"]);
3392     assert(tzConversions.fromWindows["Hawaiian Standard Time"] ==
3393            ["Etc/GMT+10", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Rarotonga", "Pacific/Tahiti"]);
3394     assert(tzConversions.fromWindows["UTC-11"] ==
3395            ["Etc/GMT+11", "Pacific/Midway", "Pacific/Niue", "Pacific/Pago_Pago"]);
3396 
3397     foreach (key, value; tzConversions.fromWindows)
3398     {
3399         assert(value.isSorted, key);
3400         assert(equal(value.uniq(), value), key);
3401     }
3402 }
3403