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