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.defaultTZDatabaseDir, "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.
2018 +/
2019 enum defaultTZDatabaseDir = "";
2020 }
2021 else version (TZDatabaseDir)
2022 {
2023 import std.string : strip;
2024 enum defaultTZDatabaseDir = strip(import("TZDatabaseDirFile"));
2025 }
2026 else version (Android)
2027 {
2028 enum defaultTZDatabaseDir = "/system/usr/share/zoneinfo/";
2029 }
2030 else version (Solaris)
2031 {
2032 enum defaultTZDatabaseDir = "/usr/share/lib/zoneinfo/";
2033 }
2034 else version (Posix)
2035 {
2036 enum defaultTZDatabaseDir = "/usr/share/zoneinfo/";
2037 }
2038 else version (Windows)
2039 {
2040 enum defaultTZDatabaseDir = "";
2041 }
2042
2043
2044 /++
2045 Returns a $(LREF TimeZone) with the give name per the TZ Database. The
2046 time zone information is fetched from the TZ Database time zone files in
2047 the given directory.
2048
2049 See_Also:
2050 $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ
2051 Database)<br>
2052 $(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List of
2053 Time Zones)
2054
2055 Params:
2056 name = The TZ Database name of the desired time zone
2057 tzDatabaseDir = The directory where the TZ Database files are
2058 located. Because these files are not located on
2059 Windows systems, provide them
2060 and give their location here to
2061 use $(LREF PosixTimeZone)s.
2062
2063 Throws:
2064 $(REF DateTimeException,std,datetime,date) if the given time zone
2065 could not be found or `FileException` if the TZ Database file
2066 could not be opened.
2067 +/
2068 // TODO make it possible for tzDatabaseDir to be gzipped tar file rather than an uncompressed
2069 // directory.
2070 static immutable(PosixTimeZone) getTimeZone(string name, string tzDatabaseDir = defaultTZDatabaseDir) @trusted
2071 {
2072 import std.algorithm.sorting : sort;
2073 import std.conv : to;
2074 import std.datetime.date : DateTimeException;
2075 import std.exception : enforce;
2076 import std.format : format;
2077 import std.path : asNormalizedPath, chainPath;
2078 import std.range : retro;
2079
2080 name = strip(name);
2081
2082 enforce(tzDatabaseDir.exists(), new DateTimeException(format("Directory %s does not exist.", tzDatabaseDir)));
2083 enforce(tzDatabaseDir.isDir, new DateTimeException(format("%s is not a directory.", tzDatabaseDir)));
2084
2085 version (Android)
2086 {
2087 auto tzfileOffset = name in tzdataIndex(tzDatabaseDir);
2088 enforce(tzfileOffset, new DateTimeException(format("The time zone %s is not listed.", name)));
2089 string tzFilename = separate_index ? "zoneinfo.dat" : "tzdata";
2090 const file = asNormalizedPath(chainPath(tzDatabaseDir, tzFilename)).to!string;
2091 }
2092 else
2093 const file = asNormalizedPath(chainPath(tzDatabaseDir, name)).to!string;
2094
2095 enforce(file.exists(), new DateTimeException(format("File %s does not exist.", file)));
2096 enforce(file.isFile, new DateTimeException(format("%s is not a file.", file)));
2097
2098 auto tzFile = File(file);
2099 version (Android) tzFile.seek(*tzfileOffset);
2100 immutable gmtZone = name.representation().canFind("GMT");
2101
2102 import std.datetime.date : DateTimeException;
2103 try
2104 {
2105 _enforceValidTZFile(readVal!(char[])(tzFile, 4) == "TZif");
2106
2107 immutable char tzFileVersion = readVal!char(tzFile);
2108 _enforceValidTZFile(tzFileVersion == '\0' || tzFileVersion == '2' || tzFileVersion == '3');
2109
2110 {
2111 auto zeroBlock = readVal!(ubyte[])(tzFile, 15);
2112 bool allZeroes = true;
2113
2114 foreach (val; zeroBlock)
2115 {
2116 if (val != 0)
2117 {
2118 allZeroes = false;
2119 break;
2120 }
2121 }
2122
2123 _enforceValidTZFile(allZeroes);
2124 }
2125
2126
2127 // The number of UTC/local indicators stored in the file.
2128 auto tzh_ttisgmtcnt = readVal!int(tzFile);
2129
2130 // The number of standard/wall indicators stored in the file.
2131 auto tzh_ttisstdcnt = readVal!int(tzFile);
2132
2133 // The number of leap seconds for which data is stored in the file.
2134 auto tzh_leapcnt = readVal!int(tzFile);
2135
2136 // The number of "transition times" for which data is stored in the file.
2137 auto tzh_timecnt = readVal!int(tzFile);
2138
2139 // The number of "local time types" for which data is stored in the file (must not be zero).
2140 auto tzh_typecnt = readVal!int(tzFile);
2141 _enforceValidTZFile(tzh_typecnt != 0);
2142
2143 // The number of characters of "timezone abbreviation strings" stored in the file.
2144 auto tzh_charcnt = readVal!int(tzFile);
2145
2146 // time_ts where DST transitions occur.
2147 auto transitionTimeTs = new long[](tzh_timecnt);
2148 foreach (ref transition; transitionTimeTs)
2149 transition = readVal!int(tzFile);
2150
2151 // Indices into ttinfo structs indicating the changes
2152 // to be made at the corresponding DST transition.
2153 auto ttInfoIndices = new ubyte[](tzh_timecnt);
2154 foreach (ref ttInfoIndex; ttInfoIndices)
2155 ttInfoIndex = readVal!ubyte(tzFile);
2156
2157 // ttinfos which give info on DST transitions.
2158 auto tempTTInfos = new TempTTInfo[](tzh_typecnt);
2159 foreach (ref ttInfo; tempTTInfos)
2160 ttInfo = readVal!TempTTInfo(tzFile);
2161
2162 // The array of time zone abbreviation characters.
2163 auto tzAbbrevChars = readVal!(char[])(tzFile, tzh_charcnt);
2164
2165 auto leapSeconds = new LeapSecond[](tzh_leapcnt);
2166 foreach (ref leapSecond; leapSeconds)
2167 {
2168 // The time_t when the leap second occurs.
2169 auto timeT = readVal!int(tzFile);
2170
2171 // The total number of leap seconds to be applied after
2172 // the corresponding leap second.
2173 auto total = readVal!int(tzFile);
2174
2175 leapSecond = LeapSecond(timeT, total);
2176 }
2177
2178 // Indicate whether each corresponding DST transition were specified
2179 // in standard time or wall clock time.
2180 auto transitionIsStd = new bool[](tzh_ttisstdcnt);
2181 foreach (ref isStd; transitionIsStd)
2182 isStd = readVal!bool(tzFile);
2183
2184 // Indicate whether each corresponding DST transition associated with
2185 // local time types are specified in UTC or local time.
2186 auto transitionInUTC = new bool[](tzh_ttisgmtcnt);
2187 foreach (ref inUTC; transitionInUTC)
2188 inUTC = readVal!bool(tzFile);
2189
2190 _enforceValidTZFile(!tzFile.eof);
2191
2192 // If version 2 or 3, the information is duplicated in 64-bit.
2193 if (tzFileVersion == '2' || tzFileVersion == '3')
2194 {
2195 _enforceValidTZFile(readVal!(char[])(tzFile, 4) == "TZif");
2196
2197 immutable char tzFileVersion2 = readVal!(char)(tzFile);
2198 _enforceValidTZFile(tzFileVersion2 == '2' || tzFileVersion2 == '3');
2199
2200 {
2201 auto zeroBlock = readVal!(ubyte[])(tzFile, 15);
2202 bool allZeroes = true;
2203
2204 foreach (val; zeroBlock)
2205 {
2206 if (val != 0)
2207 {
2208 allZeroes = false;
2209 break;
2210 }
2211 }
2212
2213 _enforceValidTZFile(allZeroes);
2214 }
2215
2216
2217 // The number of UTC/local indicators stored in the file.
2218 tzh_ttisgmtcnt = readVal!int(tzFile);
2219
2220 // The number of standard/wall indicators stored in the file.
2221 tzh_ttisstdcnt = readVal!int(tzFile);
2222
2223 // The number of leap seconds for which data is stored in the file.
2224 tzh_leapcnt = readVal!int(tzFile);
2225
2226 // The number of "transition times" for which data is stored in the file.
2227 tzh_timecnt = readVal!int(tzFile);
2228
2229 // The number of "local time types" for which data is stored in the file (must not be zero).
2230 tzh_typecnt = readVal!int(tzFile);
2231 _enforceValidTZFile(tzh_typecnt != 0);
2232
2233 // The number of characters of "timezone abbreviation strings" stored in the file.
2234 tzh_charcnt = readVal!int(tzFile);
2235
2236 // time_ts where DST transitions occur.
2237 transitionTimeTs = new long[](tzh_timecnt);
2238 foreach (ref transition; transitionTimeTs)
2239 transition = readVal!long(tzFile);
2240
2241 // Indices into ttinfo structs indicating the changes
2242 // to be made at the corresponding DST transition.
2243 ttInfoIndices = new ubyte[](tzh_timecnt);
2244 foreach (ref ttInfoIndex; ttInfoIndices)
2245 ttInfoIndex = readVal!ubyte(tzFile);
2246
2247 // ttinfos which give info on DST transitions.
2248 tempTTInfos = new TempTTInfo[](tzh_typecnt);
2249 foreach (ref ttInfo; tempTTInfos)
2250 ttInfo = readVal!TempTTInfo(tzFile);
2251
2252 // The array of time zone abbreviation characters.
2253 tzAbbrevChars = readVal!(char[])(tzFile, tzh_charcnt);
2254
2255 leapSeconds = new LeapSecond[](tzh_leapcnt);
2256 foreach (ref leapSecond; leapSeconds)
2257 {
2258 // The time_t when the leap second occurs.
2259 auto timeT = readVal!long(tzFile);
2260
2261 // The total number of leap seconds to be applied after
2262 // the corresponding leap second.
2263 auto total = readVal!int(tzFile);
2264
2265 leapSecond = LeapSecond(timeT, total);
2266 }
2267
2268 // Indicate whether each corresponding DST transition were specified
2269 // in standard time or wall clock time.
2270 transitionIsStd = new bool[](tzh_ttisstdcnt);
2271 foreach (ref isStd; transitionIsStd)
2272 isStd = readVal!bool(tzFile);
2273
2274 // Indicate whether each corresponding DST transition associated with
2275 // local time types are specified in UTC or local time.
2276 transitionInUTC = new bool[](tzh_ttisgmtcnt);
2277 foreach (ref inUTC; transitionInUTC)
2278 inUTC = readVal!bool(tzFile);
2279 }
2280
2281 _enforceValidTZFile(tzFile.readln().strip().empty);
2282
2283 cast(void) tzFile.readln();
2284
2285 version (Android)
2286 {
2287 // Android uses a single file for all timezone data, so the file
2288 // doesn't end here.
2289 }
2290 else
2291 {
2292 _enforceValidTZFile(tzFile.readln().strip().empty);
2293 _enforceValidTZFile(tzFile.eof);
2294 }
2295
2296
2297 auto transitionTypes = new TransitionType*[](tempTTInfos.length);
2298
2299 foreach (i, ref ttype; transitionTypes)
2300 {
2301 bool isStd = false;
2302
2303 if (i < transitionIsStd.length && !transitionIsStd.empty)
2304 isStd = transitionIsStd[i];
2305
2306 bool inUTC = false;
2307
2308 if (i < transitionInUTC.length && !transitionInUTC.empty)
2309 inUTC = transitionInUTC[i];
2310
2311 ttype = new TransitionType(isStd, inUTC);
2312 }
2313
2314 auto ttInfos = new immutable(TTInfo)*[](tempTTInfos.length);
2315 foreach (i, ref ttInfo; ttInfos)
2316 {
2317 auto tempTTInfo = tempTTInfos[i];
2318
2319 if (gmtZone)
2320 tempTTInfo.tt_gmtoff = -tempTTInfo.tt_gmtoff;
2321
2322 auto abbrevChars = tzAbbrevChars[tempTTInfo.tt_abbrind .. $];
2323 string abbrev = abbrevChars[0 .. abbrevChars.countUntil('\0')].idup;
2324
2325 ttInfo = new immutable(TTInfo)(tempTTInfos[i], abbrev);
2326 }
2327
2328 auto tempTransitions = new TempTransition[](transitionTimeTs.length);
2329 foreach (i, ref tempTransition; tempTransitions)
2330 {
2331 immutable ttiIndex = ttInfoIndices[i];
2332 auto transitionTimeT = transitionTimeTs[i];
2333 auto ttype = transitionTypes[ttiIndex];
2334 auto ttInfo = ttInfos[ttiIndex];
2335
2336 tempTransition = TempTransition(transitionTimeT, ttInfo, ttype);
2337 }
2338
2339 if (tempTransitions.empty)
2340 {
2341 _enforceValidTZFile(ttInfos.length == 1 && transitionTypes.length == 1);
2342 tempTransitions ~= TempTransition(0, ttInfos[0], transitionTypes[0]);
2343 }
2344
2345 sort!"a.timeT < b.timeT"(tempTransitions);
2346 sort!"a.timeT < b.timeT"(leapSeconds);
2347
2348 auto transitions = new Transition[](tempTransitions.length);
2349 foreach (i, ref transition; transitions)
2350 {
2351 auto tempTransition = tempTransitions[i];
2352 auto transitionTimeT = tempTransition.timeT;
2353 auto ttInfo = tempTransition.ttInfo;
2354
2355 _enforceValidTZFile(i == 0 || transitionTimeT > tempTransitions[i - 1].timeT);
2356
2357 transition = Transition(transitionTimeT, ttInfo);
2358 }
2359
2360 string stdName;
2361 string dstName;
2362 bool hasDST = false;
2363
2364 foreach (transition; retro(transitions))
2365 {
2366 auto ttInfo = transition.ttInfo;
2367
2368 if (ttInfo.isDST)
2369 {
2370 if (dstName.empty)
2371 dstName = ttInfo.abbrev;
2372 hasDST = true;
2373 }
2374 else
2375 {
2376 if (stdName.empty)
2377 stdName = ttInfo.abbrev;
2378 }
2379
2380 if (!stdName.empty && !dstName.empty)
2381 break;
2382 }
2383
2384 return new immutable PosixTimeZone(transitions.idup, leapSeconds.idup, name, stdName, dstName, hasDST);
2385 }
2386 catch (DateTimeException dte)
2387 throw dte;
2388 catch (Exception e)
2389 throw new DateTimeException("Not a valid TZ data file", __FILE__, __LINE__, e);
2390 }
2391
2392 ///
2393 @safe unittest
2394 {
2395 version (Posix)
2396 {
2397 auto tz = PosixTimeZone.getTimeZone("America/Los_Angeles");
2398
2399 assert(tz.name == "America/Los_Angeles");
2400 assert(tz.stdName == "PST");
2401 assert(tz.dstName == "PDT");
2402 }
2403 }
2404
2405 /++
2406 Returns a list of the names of the time zones installed on the system.
2407
2408 Providing a sub-name narrows down the list of time zones (which
2409 can number in the thousands). For example,
2410 passing in "America" as the sub-name returns only the time zones which
2411 begin with "America".
2412
2413 Params:
2414 subName = The first part of the desired time zones.
2415 tzDatabaseDir = The directory where the TZ Database files are
2416 located.
2417
2418 Throws:
2419 `FileException` if it fails to read from disk.
2420 +/
2421 static string[] getInstalledTZNames(string subName = "", string tzDatabaseDir = defaultTZDatabaseDir) @safe
2422 {
2423 import std.algorithm.sorting : sort;
2424 import std.array : appender;
2425 import std.exception : enforce;
2426 import std.format : format;
2427
2428 version (Posix)
2429 subName = strip(subName);
2430 else version (Windows)
2431 {
2432 import std.array : replace;
2433 import std.path : dirSeparator;
2434 subName = replace(strip(subName), "/", dirSeparator);
2435 }
2436
2437 import std.datetime.date : DateTimeException;
2438 enforce(tzDatabaseDir.exists(), new DateTimeException(format("Directory %s does not exist.", tzDatabaseDir)));
2439 enforce(tzDatabaseDir.isDir, new DateTimeException(format("%s is not a directory.", tzDatabaseDir)));
2440
2441 auto timezones = appender!(string[])();
2442
2443 version (Android)
2444 {
2445 import std.algorithm.iteration : filter;
2446 import std.algorithm.mutation : copy;
2447
2448 const index = () @trusted { return tzdataIndex(tzDatabaseDir); }();
2449 index.byKey.filter!(a => a.startsWith(subName)).copy(timezones);
2450 }
2451 else
2452 {
2453 import std.path : baseName;
2454 // dirEntries is @system because it uses a DirIterator with a
2455 // RefCounted variable, but here, no references to the payload is
2456 // escaped to the outside, so this should be @trusted
2457 () @trusted {
2458 foreach (DirEntry de; dirEntries(tzDatabaseDir, SpanMode.depth))
2459 {
2460 if (de.isFile)
2461 {
2462 auto tzName = de.name[tzDatabaseDir.length .. $];
2463
2464 if (!tzName.extension().empty ||
2465 !tzName.startsWith(subName) ||
2466 baseName(tzName) == "leapseconds" ||
2467 tzName == "+VERSION" ||
2468 tzName == "SECURITY")
2469 {
2470 continue;
2471 }
2472
2473 timezones.put(tzName);
2474 }
2475 }
2476 }();
2477 }
2478
2479 sort(timezones.data);
2480
2481 return timezones.data;
2482 }
2483
2484 version (Posix) @system unittest
2485 {
2486 import std.exception : assertNotThrown;
2487 import std.stdio : writefln;
2488 static void testPTZSuccess(string tzName)
2489 {
2490 scope(failure) writefln("TZName which threw: %s", tzName);
2491
2492 PosixTimeZone.getTimeZone(tzName);
2493 }
2494
2495 static void testPTZFailure(string tzName)
2496 {
2497 scope(success) writefln("TZName which was supposed to throw: %s", tzName);
2498
2499 PosixTimeZone.getTimeZone(tzName);
2500 }
2501
2502 auto tzNames = getInstalledTZNames();
2503
2504 import std.datetime.date : DateTimeException;
2505 foreach (tzName; tzNames)
2506 assertNotThrown!DateTimeException(testPTZSuccess(tzName));
2507
2508 // No timezone directories on Android, just a single tzdata file
2509 version (Android)
2510 {}
2511 else
2512 {
2513 foreach (DirEntry de; dirEntries(defaultTZDatabaseDir, SpanMode.depth))
2514 {
2515 if (de.isFile)
2516 {
2517 auto tzName = de.name[defaultTZDatabaseDir.length .. $];
2518
2519 if (!canFind(tzNames, tzName))
2520 assertThrown!DateTimeException(testPTZFailure(tzName));
2521 }
2522 }
2523 }
2524 }
2525
2526
2527 private:
2528
2529 /+
2530 Holds information on when a time transition occures (usually a
2531 transition to or from DST) as well as a pointer to the `TTInfo` which
2532 holds information on the utc offset past the transition.
2533 +/
2534 struct Transition
2535 {
2536 this(long timeT, immutable (TTInfo)* ttInfo) @safe pure
2537 {
2538 this.timeT = timeT;
2539 this.ttInfo = ttInfo;
2540 }
2541
2542 long timeT;
2543 immutable (TTInfo)* ttInfo;
2544 }
2545
2546
2547 /+
2548 Holds information on when a leap second occurs.
2549 +/
2550 struct LeapSecond
2551 {
2552 this(long timeT, int total) @safe pure
2553 {
2554 this.timeT = timeT;
2555 this.total = total;
2556 }
2557
2558 long timeT;
2559 int total;
2560 }
2561
2562 /+
2563 Holds information on the utc offset after a transition as well as
2564 whether DST is in effect after that transition.
2565 +/
2566 struct TTInfo
2567 {
2568 this(scope const TempTTInfo tempTTInfo, string abbrev) @safe immutable pure
2569 {
2570 utcOffset = tempTTInfo.tt_gmtoff;
2571 isDST = tempTTInfo.tt_isdst;
2572 this.abbrev = abbrev;
2573 }
2574
2575 immutable int utcOffset; // Offset from UTC.
2576 immutable bool isDST; // Whether DST is in effect.
2577 immutable string abbrev; // The current abbreviation for the time zone.
2578 }
2579
2580
2581 /+
2582 Struct used to hold information relating to `TTInfo` while organizing
2583 the time zone information prior to putting it in its final form.
2584 +/
2585 struct TempTTInfo
2586 {
2587 this(int gmtOff, bool isDST, ubyte abbrInd) @safe pure
2588 {
2589 tt_gmtoff = gmtOff;
2590 tt_isdst = isDST;
2591 tt_abbrind = abbrInd;
2592 }
2593
2594 int tt_gmtoff;
2595 bool tt_isdst;
2596 ubyte tt_abbrind;
2597 }
2598
2599
2600 /+
2601 Struct used to hold information relating to `Transition` while
2602 organizing the time zone information prior to putting it in its final
2603 form.
2604 +/
2605 struct TempTransition
2606 {
2607 this(long timeT, immutable (TTInfo)* ttInfo, TransitionType* ttype) @safe pure
2608 {
2609 this.timeT = timeT;
2610 this.ttInfo = ttInfo;
2611 this.ttype = ttype;
2612 }
2613
2614 long timeT;
2615 immutable (TTInfo)* ttInfo;
2616 TransitionType* ttype;
2617 }
2618
2619
2620 /+
2621 Struct used to hold information relating to `Transition` and
2622 `TTInfo` while organizing the time zone information prior to putting
2623 it in its final form.
2624 +/
2625 struct TransitionType
2626 {
2627 this(bool isStd, bool inUTC) @safe pure
2628 {
2629 this.isStd = isStd;
2630 this.inUTC = inUTC;
2631 }
2632
2633 // Whether the transition is in std time (as opposed to wall clock time).
2634 bool isStd;
2635
2636 // Whether the transition is in UTC (as opposed to local time).
2637 bool inUTC;
2638 }
2639
2640
2641 /+
2642 Reads an int from a TZ file.
2643 +/
2644 static T readVal(T)(ref File tzFile) @trusted
2645 if ((isIntegral!T || isSomeChar!T) || is(immutable T == immutable bool))
2646 {
2647 import std.bitmanip : bigEndianToNative;
2648 T[1] buff;
2649
2650 _enforceValidTZFile(!tzFile.eof);
2651 tzFile.rawRead(buff);
2652
2653 return bigEndianToNative!T(cast(ubyte[T.sizeof]) buff);
2654 }
2655
2656 /+
2657 Reads an array of values from a TZ file.
2658 +/
2659 static T readVal(T)(ref File tzFile, size_t length) @trusted
2660 if (isArray!T)
2661 {
2662 auto buff = new T(length);
2663
2664 _enforceValidTZFile(!tzFile.eof);
2665 tzFile.rawRead(buff);
2666
2667 return buff;
2668 }
2669
2670
2671 /+
2672 Reads a `TempTTInfo` from a TZ file.
2673 +/
2674 static T readVal(T)(ref File tzFile) @safe
2675 if (is(T == TempTTInfo))
2676 {
2677 return TempTTInfo(readVal!int(tzFile),
2678 readVal!bool(tzFile),
2679 readVal!ubyte(tzFile));
2680 }
2681
2682
2683 /+
2684 Throws:
2685 $(REF DateTimeException,std,datetime,date) if `result` is false.
2686 +/
2687 static void _enforceValidTZFile(bool result, size_t line = __LINE__) @safe pure
2688 {
2689 import std.datetime.date : DateTimeException;
2690 if (!result)
2691 throw new DateTimeException("Not a valid tzdata file.", __FILE__, line);
2692 }
2693
2694
2695 int calculateLeapSeconds(long stdTime) @safe const scope pure nothrow
2696 {
2697 if (_leapSeconds.empty)
2698 return 0;
2699
2700 immutable unixTime = stdTimeToUnixTime(stdTime);
2701
2702 if (_leapSeconds.front.timeT >= unixTime)
2703 return 0;
2704
2705 immutable found = countUntil!"b < a.timeT"(_leapSeconds, unixTime);
2706
2707 if (found == -1)
2708 return _leapSeconds.back.total;
2709
2710 immutable leapSecond = found == 0 ? _leapSeconds[0] : _leapSeconds[found - 1];
2711
2712 return leapSecond.total;
2713 }
2714
2715
2716 this(immutable Transition[] transitions,
2717 immutable LeapSecond[] leapSeconds,
2718 string name,
2719 string stdName,
2720 string dstName,
2721 bool hasDST) @safe immutable pure
2722 {
2723 if (dstName.empty && !stdName.empty)
2724 dstName = stdName;
2725 else if (stdName.empty && !dstName.empty)
2726 stdName = dstName;
2727
2728 super(name, stdName, dstName);
2729
2730 if (!transitions.empty)
2731 {
2732 foreach (i, transition; transitions[0 .. $-1])
2733 _enforceValidTZFile(transition.timeT < transitions[i + 1].timeT);
2734 }
2735
2736 foreach (i, leapSecond; leapSeconds)
2737 _enforceValidTZFile(i == leapSeconds.length - 1 || leapSecond.timeT < leapSeconds[i + 1].timeT);
2738
2739 _transitions = transitions;
2740 _leapSeconds = leapSeconds;
2741 _hasDST = hasDST;
2742 }
2743
2744 // Android concatenates the usual timezone directories into a single file,
2745 // tzdata, along with an index to jump to each timezone's offset. In older
2746 // versions of Android, the index was stored in a separate file, zoneinfo.idx,
2747 // whereas now it's stored at the beginning of tzdata.
2748 version (Android)
2749 {
2750 // Keep track of whether there's a separate index, zoneinfo.idx. Only
2751 // check this after calling tzdataIndex, as it's initialized there.
2752 static shared bool separate_index;
2753
2754 // Extracts the name of each time zone and the offset where its data is
2755 // located in the tzdata file from the index and caches it for later.
2756 static const(uint[string]) tzdataIndex(string tzDir)
2757 {
2758 import std.concurrency : initOnce;
2759
2760 __gshared uint[string] _tzIndex;
2761
2762 // _tzIndex is initialized once and then shared across all threads.
2763 initOnce!_tzIndex(
2764 {
2765 import std.conv : to;
2766 import std.datetime.date : DateTimeException;
2767 import std.format : format;
2768 import std.path : asNormalizedPath, chainPath;
2769
2770 enum indexEntrySize = 52;
2771 const combinedFile = asNormalizedPath(chainPath(tzDir, "tzdata")).to!string;
2772 const indexFile = asNormalizedPath(chainPath(tzDir, "zoneinfo.idx")).to!string;
2773 File tzFile;
2774 uint indexEntries, dataOffset;
2775 uint[string] initIndex;
2776
2777 // Check for the combined file tzdata, which stores the index
2778 // and the time zone data together.
2779 if (combinedFile.exists() && combinedFile.isFile)
2780 {
2781 tzFile = File(combinedFile);
2782 _enforceValidTZFile(readVal!(char[])(tzFile, 6) == "tzdata");
2783 auto tzDataVersion = readVal!(char[])(tzFile, 6);
2784 _enforceValidTZFile(tzDataVersion[5] == '\0');
2785
2786 uint indexOffset = readVal!uint(tzFile);
2787 dataOffset = readVal!uint(tzFile);
2788 readVal!uint(tzFile);
2789
2790 indexEntries = (dataOffset - indexOffset) / indexEntrySize;
2791 separate_index = false;
2792 }
2793 else if (indexFile.exists() && indexFile.isFile)
2794 {
2795 tzFile = File(indexFile);
2796 indexEntries = to!uint(tzFile.size/indexEntrySize);
2797 separate_index = true;
2798 }
2799 else
2800 {
2801 throw new DateTimeException(format("Both timezone files %s and %s do not exist.",
2802 combinedFile, indexFile));
2803 }
2804
2805 foreach (_; 0 .. indexEntries)
2806 {
2807 string tzName = to!string(readVal!(char[])(tzFile, 40).ptr);
2808 uint tzOffset = readVal!uint(tzFile);
2809 readVal!(uint[])(tzFile, 2);
2810 initIndex[tzName] = dataOffset + tzOffset;
2811 }
2812 initIndex.rehash;
2813 return initIndex;
2814 }());
2815 return _tzIndex;
2816 }
2817 }
2818
2819 // List of times when the utc offset changes.
2820 immutable Transition[] _transitions;
2821
2822 // List of leap second occurrences.
2823 immutable LeapSecond[] _leapSeconds;
2824
2825 // Whether DST is in effect for this time zone at any point in time.
2826 immutable bool _hasDST;
2827 }
2828
2829
2830 version (StdDdoc)
2831 {
2832 /++
2833 $(BLUE This class is Windows-Only.)
2834
2835 Represents a time zone from the Windows registry. Unfortunately, Windows
2836 does not use the TZ Database. To use the TZ Database, use
2837 $(LREF PosixTimeZone) (which reads its information from the TZ Database
2838 files on disk) on Windows by providing the TZ Database files and telling
2839 `PosixTimeZone.getTimeZone` where the directory holding them is.
2840
2841 The TZ Database files and Windows' time zone information frequently
2842 do not match. Windows has many errors with regards to when DST switches
2843 occur (especially for historical dates). Also, the TZ Database files
2844 include far more time zones than Windows does. So, for accurate
2845 time zone information, use the TZ Database files with
2846 $(LREF PosixTimeZone) rather than `WindowsTimeZone`. However, because
2847 `WindowsTimeZone` uses Windows system calls to deal with the time,
2848 it's far more likely to match the behavior of other Windows programs.
2849 Be aware of the differences when selecting a method.
2850
2851 `WindowsTimeZone` does not exist on Posix systems.
2852
2853 To get a `WindowsTimeZone`, call `WindowsTimeZone.getTimeZone`.
2854
2855 See_Also:
2856 $(HTTP www.iana.org/time-zones, Home of the TZ Database files)
2857 +/
2858 final class WindowsTimeZone : TimeZone
2859 {
2860 public:
2861
2862 /++
2863 Whether this time zone has Daylight Savings Time at any point in
2864 time. Note that for some time zone types it may not have DST for
2865 current dates but will still return true for `hasDST` because the
2866 time zone did at some point have DST.
2867 +/
2868 @property override bool hasDST() @safe const scope nothrow;
2869
2870
2871 /++
2872 Takes the number of hnsecs (100 ns) since midnight, January 1st,
2873 1 A.D. in UTC time (i.e. std time) and returns whether DST is in
2874 effect in this time zone at the given point in time.
2875
2876 Params:
2877 stdTime = The UTC time that needs to be checked for DST in this
2878 time zone.
2879 +/
2880 override bool dstInEffect(long stdTime) @safe const scope nothrow;
2881
2882
2883 /++
2884 Takes the number of hnsecs (100 ns) since midnight, January 1st,
2885 1 A.D. in UTC time (i.e. std time) and converts it to this time
2886 zone's time.
2887
2888 Params:
2889 stdTime = The UTC time that needs to be adjusted to this time
2890 zone's time.
2891 +/
2892 override long utcToTZ(long stdTime) @safe const scope nothrow;
2893
2894
2895 /++
2896 Takes the number of hnsecs (100 ns) since midnight, January 1st,
2897 1 A.D. in this time zone's time and converts it to UTC (i.e. std
2898 time).
2899
2900 Params:
2901 adjTime = The time in this time zone that needs to be adjusted
2902 to UTC time.
2903 +/
2904 override long tzToUTC(long adjTime) @safe const scope nothrow;
2905
2906
2907 /++
2908 Returns a $(LREF TimeZone) with the given name per the Windows time
2909 zone names. The time zone information is fetched from the Windows
2910 registry.
2911
2912 See_Also:
2913 $(HTTP en.wikipedia.org/wiki/Tz_database, Wikipedia entry on TZ
2914 Database)<br>
2915 $(HTTP en.wikipedia.org/wiki/List_of_tz_database_time_zones, List
2916 of Time Zones)
2917
2918 Params:
2919 name = The TZ Database name of the desired time zone.
2920
2921 Throws:
2922 $(REF DateTimeException,std,datetime,date) if the given time
2923 zone could not be found.
2924
2925 Example:
2926 --------------------
2927 auto tz = WindowsTimeZone.getTimeZone("Pacific Standard Time");
2928 --------------------
2929 +/
2930 static immutable(WindowsTimeZone) getTimeZone(string name) @safe;
2931
2932
2933 /++
2934 Returns a list of the names of the time zones installed on the
2935 system. The list returned by WindowsTimeZone contains the Windows
2936 TZ names, not the TZ Database names. However,
2937 `TimeZone.getinstalledTZNames` will return the TZ Database names
2938 which are equivalent to the Windows TZ names.
2939 +/
2940 static string[] getInstalledTZNames() @safe;
2941
2942 private:
2943
2944 version (Windows)
2945 {}
2946 else
2947 alias TIME_ZONE_INFORMATION = void*;
2948
2949 static bool _dstInEffect(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime) nothrow;
2950 static long _utcToTZ(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime, bool hasDST) nothrow;
2951 static long _tzToUTC(const scope TIME_ZONE_INFORMATION* tzInfo, long adjTime, bool hasDST) nothrow;
2952
2953 this() immutable pure
2954 {
2955 super("", "", "");
2956 }
2957 }
2958
2959 }
2960 else version (Windows)
2961 {
2962 final class WindowsTimeZone : TimeZone
2963 {
2964 import std.algorithm.sorting : sort;
2965 import std.array : appender;
2966 import std.conv : to;
2967 import std.format : format;
2968
2969 public:
2970
2971 @property override bool hasDST() @safe const scope nothrow
2972 {
2973 return _tzInfo.DaylightDate.wMonth != 0;
2974 }
2975
2976
2977 override bool dstInEffect(long stdTime) @safe const scope nothrow
2978 {
2979 return _dstInEffect(&_tzInfo, stdTime);
2980 }
2981
2982
2983 override long utcToTZ(long stdTime) @safe const scope nothrow
2984 {
2985 return _utcToTZ(&_tzInfo, stdTime, hasDST);
2986 }
2987
2988
2989 override long tzToUTC(long adjTime) @safe const scope nothrow
2990 {
2991 return _tzToUTC(&_tzInfo, adjTime, hasDST);
2992 }
2993
2994
2995 static immutable(WindowsTimeZone) getTimeZone(string name) @trusted
2996 {
2997 scope baseKey = Registry.localMachine.getKey(`Software\Microsoft\Windows NT\CurrentVersion\Time Zones`);
2998
2999 foreach (tzKeyName; baseKey.keyNames)
3000 {
3001 if (tzKeyName != name)
3002 continue;
3003
3004 scope tzKey = baseKey.getKey(tzKeyName);
3005
3006 scope stdVal = tzKey.getValue("Std");
3007 auto stdName = stdVal.value_SZ;
3008
3009 scope dstVal = tzKey.getValue("Dlt");
3010 auto dstName = dstVal.value_SZ;
3011
3012 scope tziVal = tzKey.getValue("TZI");
3013 auto binVal = tziVal.value_BINARY;
3014 assert(binVal.length == REG_TZI_FORMAT.sizeof,
3015 "Unexpected size while getTimeZone with name " ~ name);
3016 auto tziFmt = cast(REG_TZI_FORMAT*) binVal.ptr;
3017
3018 TIME_ZONE_INFORMATION tzInfo;
3019
3020 auto wstdName = stdName.to!wstring;
3021 auto wdstName = dstName.to!wstring;
3022 auto wstdNameLen = wstdName.length > 32 ? 32 : wstdName.length;
3023 auto wdstNameLen = wdstName.length > 32 ? 32 : wdstName.length;
3024
3025 tzInfo.Bias = tziFmt.Bias;
3026 tzInfo.StandardName[0 .. wstdNameLen] = wstdName[0 .. wstdNameLen];
3027 tzInfo.StandardName[wstdNameLen .. $] = '\0';
3028 tzInfo.StandardDate = tziFmt.StandardDate;
3029 tzInfo.StandardBias = tziFmt.StandardBias;
3030 tzInfo.DaylightName[0 .. wdstNameLen] = wdstName[0 .. wdstNameLen];
3031 tzInfo.DaylightName[wdstNameLen .. $] = '\0';
3032 tzInfo.DaylightDate = tziFmt.DaylightDate;
3033 tzInfo.DaylightBias = tziFmt.DaylightBias;
3034
3035 return new immutable WindowsTimeZone(name, tzInfo);
3036 }
3037 import std.datetime.date : DateTimeException;
3038 throw new DateTimeException(format("Failed to find time zone: %s", name));
3039 }
3040
3041 static string[] getInstalledTZNames() @trusted
3042 {
3043 auto timezones = appender!(string[])();
3044
3045 scope baseKey = Registry.localMachine.getKey(`Software\Microsoft\Windows NT\CurrentVersion\Time Zones`);
3046
3047 foreach (tzKeyName; baseKey.keyNames)
3048 timezones.put(tzKeyName);
3049 sort(timezones.data);
3050
3051 return timezones.data;
3052 }
3053
3054 @safe unittest
3055 {
3056 import std.exception : assertNotThrown;
3057 import std.stdio : writefln;
3058 static void testWTZSuccess(string tzName)
3059 {
3060 scope(failure) writefln("TZName which threw: %s", tzName);
3061
3062 WindowsTimeZone.getTimeZone(tzName);
3063 }
3064
3065 auto tzNames = getInstalledTZNames();
3066
3067 import std.datetime.date : DateTimeException;
3068 foreach (tzName; tzNames)
3069 assertNotThrown!DateTimeException(testWTZSuccess(tzName));
3070 }
3071
3072
3073 private:
3074
3075 static bool _dstInEffect(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime) @trusted nothrow
3076 {
3077 try
3078 {
3079 if (tzInfo.DaylightDate.wMonth == 0)
3080 return false;
3081
3082 import std.datetime.date : DateTime, Month;
3083 auto utcDateTime = cast(DateTime) SysTime(stdTime, UTC());
3084
3085 //The limits of what SystemTimeToTzSpecificLocalTime will accept.
3086 if (utcDateTime.year < 1601)
3087 {
3088 import std.datetime.date : Month;
3089 if (utcDateTime.month == Month.feb && utcDateTime.day == 29)
3090 utcDateTime.day = 28;
3091 utcDateTime.year = 1601;
3092 }
3093 else if (utcDateTime.year > 30_827)
3094 {
3095 if (utcDateTime.month == Month.feb && utcDateTime.day == 29)
3096 utcDateTime.day = 28;
3097 utcDateTime.year = 30_827;
3098 }
3099
3100 //SystemTimeToTzSpecificLocalTime doesn't act correctly at the
3101 //beginning or end of the year (bleh). Unless some bizarre time
3102 //zone changes DST on January 1st or December 31st, this should
3103 //fix the problem.
3104 if (utcDateTime.month == Month.jan)
3105 {
3106 if (utcDateTime.day == 1)
3107 utcDateTime.day = 2;
3108 }
3109 else if (utcDateTime.month == Month.dec && utcDateTime.day == 31)
3110 utcDateTime.day = 30;
3111
3112 SYSTEMTIME utcTime = void;
3113 SYSTEMTIME otherTime = void;
3114
3115 utcTime.wYear = utcDateTime.year;
3116 utcTime.wMonth = utcDateTime.month;
3117 utcTime.wDay = utcDateTime.day;
3118 utcTime.wHour = utcDateTime.hour;
3119 utcTime.wMinute = utcDateTime.minute;
3120 utcTime.wSecond = utcDateTime.second;
3121 utcTime.wMilliseconds = 0;
3122
3123 immutable result = SystemTimeToTzSpecificLocalTime(cast(TIME_ZONE_INFORMATION*) tzInfo,
3124 &utcTime,
3125 &otherTime);
3126 assert(result, "Failed to create SystemTimeToTzSpecificLocalTime");
3127
3128 immutable otherDateTime = DateTime(otherTime.wYear,
3129 otherTime.wMonth,
3130 otherTime.wDay,
3131 otherTime.wHour,
3132 otherTime.wMinute,
3133 otherTime.wSecond);
3134 immutable diff = utcDateTime - otherDateTime;
3135 immutable minutes = diff.total!"minutes" - tzInfo.Bias;
3136
3137 if (minutes == tzInfo.DaylightBias)
3138 return true;
3139
3140 assert(minutes == tzInfo.StandardBias, "Unexpected difference");
3141
3142 return false;
3143 }
3144 catch (Exception e)
3145 assert(0, "DateTime's constructor threw.");
3146 }
3147
3148 @system unittest
3149 {
3150 TIME_ZONE_INFORMATION tzInfo;
3151 GetTimeZoneInformation(&tzInfo);
3152
3153 import std.datetime.date : DateTime;
3154 foreach (year; [1600, 1601, 30_827, 30_828])
3155 WindowsTimeZone._dstInEffect(&tzInfo, SysTime(DateTime(year, 1, 1)).stdTime);
3156 }
3157
3158
3159 static long _utcToTZ(const scope TIME_ZONE_INFORMATION* tzInfo, long stdTime, bool hasDST) @safe nothrow
3160 {
3161 if (hasDST && WindowsTimeZone._dstInEffect(tzInfo, stdTime))
3162 return stdTime - convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.DaylightBias);
3163
3164 return stdTime - convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.StandardBias);
3165 }
3166
3167
3168 static long _tzToUTC(const scope TIME_ZONE_INFORMATION* tzInfo, long adjTime, bool hasDST) @trusted nothrow
3169 {
3170 if (hasDST)
3171 {
3172 try
3173 {
3174 import std.datetime.date : DateTime, Month;
3175 bool dstInEffectForLocalDateTime(DateTime localDateTime)
3176 {
3177 // The limits of what SystemTimeToTzSpecificLocalTime will accept.
3178 if (localDateTime.year < 1601)
3179 {
3180 if (localDateTime.month == Month.feb && localDateTime.day == 29)
3181 localDateTime.day = 28;
3182
3183 localDateTime.year = 1601;
3184 }
3185 else if (localDateTime.year > 30_827)
3186 {
3187 if (localDateTime.month == Month.feb && localDateTime.day == 29)
3188 localDateTime.day = 28;
3189
3190 localDateTime.year = 30_827;
3191 }
3192
3193 // SystemTimeToTzSpecificLocalTime doesn't act correctly at the
3194 // beginning or end of the year (bleh). Unless some bizarre time
3195 // zone changes DST on January 1st or December 31st, this should
3196 // fix the problem.
3197 if (localDateTime.month == Month.jan)
3198 {
3199 if (localDateTime.day == 1)
3200 localDateTime.day = 2;
3201 }
3202 else if (localDateTime.month == Month.dec && localDateTime.day == 31)
3203 localDateTime.day = 30;
3204
3205 SYSTEMTIME utcTime = void;
3206 SYSTEMTIME localTime = void;
3207
3208 localTime.wYear = localDateTime.year;
3209 localTime.wMonth = localDateTime.month;
3210 localTime.wDay = localDateTime.day;
3211 localTime.wHour = localDateTime.hour;
3212 localTime.wMinute = localDateTime.minute;
3213 localTime.wSecond = localDateTime.second;
3214 localTime.wMilliseconds = 0;
3215
3216 immutable result = TzSpecificLocalTimeToSystemTime(cast(TIME_ZONE_INFORMATION*) tzInfo,
3217 &localTime,
3218 &utcTime);
3219 assert(result);
3220 assert(result, "Failed to create _tzToUTC");
3221
3222 immutable utcDateTime = DateTime(utcTime.wYear,
3223 utcTime.wMonth,
3224 utcTime.wDay,
3225 utcTime.wHour,
3226 utcTime.wMinute,
3227 utcTime.wSecond);
3228
3229 immutable diff = localDateTime - utcDateTime;
3230 immutable minutes = -tzInfo.Bias - diff.total!"minutes";
3231
3232 if (minutes == tzInfo.DaylightBias)
3233 return true;
3234
3235 assert(minutes == tzInfo.StandardBias, "Unexpected difference");
3236
3237 return false;
3238 }
3239
3240 import std.datetime.date : DateTime;
3241 auto localDateTime = cast(DateTime) SysTime(adjTime, UTC());
3242 auto localDateTimeBefore = localDateTime - dur!"hours"(1);
3243 auto localDateTimeAfter = localDateTime + dur!"hours"(1);
3244
3245 auto dstInEffectNow = dstInEffectForLocalDateTime(localDateTime);
3246 auto dstInEffectBefore = dstInEffectForLocalDateTime(localDateTimeBefore);
3247 auto dstInEffectAfter = dstInEffectForLocalDateTime(localDateTimeAfter);
3248
3249 bool isDST;
3250
3251 if (dstInEffectBefore && dstInEffectNow && dstInEffectAfter)
3252 isDST = true;
3253 else if (!dstInEffectBefore && !dstInEffectNow && !dstInEffectAfter)
3254 isDST = false;
3255 else if (!dstInEffectBefore && dstInEffectAfter)
3256 isDST = false;
3257 else if (dstInEffectBefore && !dstInEffectAfter)
3258 isDST = dstInEffectNow;
3259 else
3260 assert(0, "Bad Logic.");
3261
3262 if (isDST)
3263 return adjTime + convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.DaylightBias);
3264 }
3265 catch (Exception e)
3266 assert(0, "SysTime's constructor threw.");
3267 }
3268
3269 return adjTime + convert!("minutes", "hnsecs")(tzInfo.Bias + tzInfo.StandardBias);
3270 }
3271
3272
3273 this(string name, TIME_ZONE_INFORMATION tzInfo) @trusted immutable pure
3274 {
3275 super(name, to!string(tzInfo.StandardName.ptr), to!string(tzInfo.DaylightName.ptr));
3276 _tzInfo = tzInfo;
3277 }
3278
3279
3280 TIME_ZONE_INFORMATION _tzInfo;
3281 }
3282 }
3283
3284
3285 version (StdDdoc)
3286 {
3287 /++
3288 $(BLUE This function is Posix-Only.)
3289
3290 Sets the local time zone on Posix systems with the TZ
3291 Database name by setting the TZ environment variable.
3292
3293 Unfortunately, there is no way to do it on Windows using the TZ
3294 Database name, so this function only exists on Posix systems.
3295 +/
3296 void setTZEnvVar(string tzDatabaseName) @safe nothrow;
3297
3298
3299 /++
3300 $(BLUE This function is Posix-Only.)
3301
3302 Clears the TZ environment variable.
3303 +/
3304 void clearTZEnvVar() @safe nothrow;
3305 }
3306 else version (Posix)
3307 {
3308 void setTZEnvVar(string tzDatabaseName) @trusted nothrow
3309 {
3310 import core.stdc.time : tzset;
3311 import core.sys.posix.stdlib : setenv;
3312 import std.internal.cstring : tempCString;
3313 import std.path : asNormalizedPath, chainPath;
3314
3315 version (Android)
3316 auto value = asNormalizedPath(tzDatabaseName);
3317 else
3318 auto value = asNormalizedPath(chainPath(PosixTimeZone.defaultTZDatabaseDir, tzDatabaseName));
3319 setenv("TZ", value.tempCString(), 1);
3320 tzset();
3321 }
3322
3323
3324 void clearTZEnvVar() @trusted nothrow
3325 {
3326 import core.stdc.time : tzset;
3327 import core.sys.posix.stdlib : unsetenv;
3328
3329 unsetenv("TZ");
3330 tzset();
3331 }
3332 }
3333
3334
3335 /++
3336 Provides the conversions between the IANA time zone database time zone names
3337 (which POSIX systems use) and the time zone names that Windows uses.
3338
3339 Windows uses a different set of time zone names than the IANA time zone
3340 database does, and how they correspond to one another changes over time
3341 (particularly when Microsoft updates Windows).
3342 $(HTTP github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml, windowsZones.xml)
3343 provides the current conversions (which may or may not match up with what's
3344 on a particular Windows box depending on how up-to-date it is), and
3345 parseTZConversions reads in those conversions from windowsZones.xml so that
3346 a D program can use those conversions.
3347
3348 However, it should be noted that the time zone information on Windows is
3349 frequently less accurate than that in the IANA time zone database, and if
3350 someone really wants accurate time zone information, they should use the
3351 IANA time zone database files with $(LREF PosixTimeZone) on Windows rather
3352 than $(LREF WindowsTimeZone), whereas $(LREF WindowsTimeZone) makes more
3353 sense when trying to match what Windows will think the time is in a specific
3354 time zone.
3355
3356 Also, the IANA time zone database has a lot more time zones than Windows
3357 does.
3358
3359 Params:
3360 windowsZonesXMLText = The text from
3361 $(HTTP github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml, windowsZones.xml)
3362
3363 Throws:
3364 Exception if there is an error while parsing the given XML.
3365
3366 --------------------
3367 // Parse the conversions from a local file.
3368 auto text = std.file.readText("path/to/windowsZones.xml");
3369 auto conversions = parseTZConversions(text);
3370
3371 // Alternatively, grab the XML file from the web at runtime
3372 // and parse it so that it's guaranteed to be up-to-date, though
3373 // that has the downside that the code needs to worry about the
3374 // site being down or unicode.org changing the URL.
3375 auto url = "https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/windowsZones.xml";
3376 auto conversions2 = parseTZConversions(std.net.curl.get(url));
3377 --------------------
3378 +/
3379 struct TZConversions
3380 {
3381 /++
3382 The key is the Windows time zone name, and the value is a list of
3383 IANA TZ database names which are close (currently only ever one, but
3384 it allows for multiple in case it's ever necessary).
3385 +/
3386 string[][string] toWindows;
3387
3388 /++
3389 The key is the IANA time zone database name, and the value is a list of
3390 Windows time zone names which are close (usually only one, but it could
3391 be multiple).
3392 +/
3393 string[][string] fromWindows;
3394 }
3395
3396 /++ ditto +/
3397 TZConversions parseTZConversions(string windowsZonesXMLText) @safe pure
3398 {
3399 // This is a bit hacky, since it doesn't properly read XML, but it avoids
3400 // needing to pull in an xml parsing module.
3401 import std.algorithm.iteration : uniq;
3402 import std.algorithm.searching : find;
3403 import std.algorithm.sorting : sort;
3404 import std.array : array, split;
3405 import std.string : lineSplitter;
3406
3407 string[][string] win2Nix;
3408 string[][string] nix2Win;
3409
3410 immutable f1 = `<mapZone other="`;
3411 immutable f2 = `type="`;
3412
3413 foreach (line; windowsZonesXMLText.lineSplitter())
3414 {
3415 import std.exception : enforce;
3416 // Sample line:
3417 // <mapZone other="Canada Central Standard Time" territory="CA" type="America/Regina America/Swift_Current"/>
3418
3419 line = line.find(f1);
3420 if (line.empty)
3421 continue;
3422 line = line[f1.length .. $];
3423 auto next = line.find('"');
3424 enforce(!next.empty, "Error parsing. Text does not appear to be from windowsZones.xml");
3425 auto win = line[0 .. $ - next.length];
3426 line = next.find(f2);
3427 enforce(!line.empty, "Error parsing. Text does not appear to be from windowsZones.xml");
3428 line = line[f2.length .. $];
3429 next = line.find('"');
3430 enforce(!next.empty, "Error parsing. Text does not appear to be from windowsZones.xml");
3431 auto nixes = line[0 .. $ - next.length].split();
3432
3433 if (auto n = win in win2Nix)
3434 *n ~= nixes;
3435 else
3436 win2Nix[win] = nixes;
3437
3438 foreach (nix; nixes)
3439 {
3440 if (auto w = nix in nix2Win)
3441 *w ~= win;
3442 else
3443 nix2Win[nix] = [win];
3444 }
3445 }
3446
3447 foreach (key, ref value; nix2Win)
3448 value = value.sort().uniq().array();
3449 foreach (key, ref value; win2Nix)
3450 value = value.sort().uniq().array();
3451
3452 return TZConversions(nix2Win, win2Nix);
3453 }
3454
3455 @safe unittest
3456 {
3457 import std.algorithm.comparison : equal;
3458 import std.algorithm.iteration : uniq;
3459 import std.algorithm.sorting : isSorted;
3460
3461 // Reduced text from https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml
3462 auto sampleFileText =
3463 `<?xml version="1.0" encoding="UTF-8" ?>
3464 <!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
3465 <!--
3466 Copyright © 1991-2013 Unicode, Inc.
3467 CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/)
3468 For terms of use, see http://www.unicode.org/copyright.html
3469 -->
3470
3471 <supplementalData>
3472 <version number="$Revision$"/>
3473
3474 <windowsZones>
3475 <mapTimezones otherVersion="7df0005" typeVersion="2015g">
3476
3477 <!-- (UTC-12:00) International Date Line West -->
3478 <mapZone other="Dateline Standard Time" territory="001" type="Etc/GMT+12"/>
3479 <mapZone other="Dateline Standard Time" territory="ZZ" type="Etc/GMT+12"/>
3480
3481 <!-- (UTC-11:00) Coordinated Universal Time-11 -->
3482 <mapZone other="UTC-11" territory="001" type="Etc/GMT+11"/>
3483 <mapZone other="UTC-11" territory="AS" type="Pacific/Pago_Pago"/>
3484 <mapZone other="UTC-11" territory="NU" type="Pacific/Niue"/>
3485 <mapZone other="UTC-11" territory="UM" type="Pacific/Midway"/>
3486 <mapZone other="UTC-11" territory="ZZ" type="Etc/GMT+11"/>
3487
3488 <!-- (UTC-10:00) Hawaii -->
3489 <mapZone other="Hawaiian Standard Time" territory="001" type="Pacific/Honolulu"/>
3490 <mapZone other="Hawaiian Standard Time" territory="CK" type="Pacific/Rarotonga"/>
3491 <mapZone other="Hawaiian Standard Time" territory="PF" type="Pacific/Tahiti"/>
3492 <mapZone other="Hawaiian Standard Time" territory="UM" type="Pacific/Johnston"/>
3493 <mapZone other="Hawaiian Standard Time" territory="US" type="Pacific/Honolulu"/>
3494 <mapZone other="Hawaiian Standard Time" territory="ZZ" type="Etc/GMT+10"/>
3495
3496 <!-- (UTC-09:00) Alaska -->
3497 <mapZone other="Alaskan Standard Time" territory="001" type="America/Anchorage"/>
3498 <mapZone other="Alaskan Standard Time" territory="US" `
3499 ~ `type="America/Anchorage America/Juneau America/Nome America/Sitka America/Yakutat"/>
3500 </mapTimezones>
3501 </windowsZones>
3502 </supplementalData>`;
3503
3504 auto tzConversions = parseTZConversions(sampleFileText);
3505 assert(tzConversions.toWindows.length == 15);
3506 assert(tzConversions.toWindows["America/Anchorage"] == ["Alaskan Standard Time"]);
3507 assert(tzConversions.toWindows["America/Juneau"] == ["Alaskan Standard Time"]);
3508 assert(tzConversions.toWindows["America/Nome"] == ["Alaskan Standard Time"]);
3509 assert(tzConversions.toWindows["America/Sitka"] == ["Alaskan Standard Time"]);
3510 assert(tzConversions.toWindows["America/Yakutat"] == ["Alaskan Standard Time"]);
3511 assert(tzConversions.toWindows["Etc/GMT+10"] == ["Hawaiian Standard Time"]);
3512 assert(tzConversions.toWindows["Etc/GMT+11"] == ["UTC-11"]);
3513 assert(tzConversions.toWindows["Etc/GMT+12"] == ["Dateline Standard Time"]);
3514 assert(tzConversions.toWindows["Pacific/Honolulu"] == ["Hawaiian Standard Time"]);
3515 assert(tzConversions.toWindows["Pacific/Johnston"] == ["Hawaiian Standard Time"]);
3516 assert(tzConversions.toWindows["Pacific/Midway"] == ["UTC-11"]);
3517 assert(tzConversions.toWindows["Pacific/Niue"] == ["UTC-11"]);
3518 assert(tzConversions.toWindows["Pacific/Pago_Pago"] == ["UTC-11"]);
3519 assert(tzConversions.toWindows["Pacific/Rarotonga"] == ["Hawaiian Standard Time"]);
3520 assert(tzConversions.toWindows["Pacific/Tahiti"] == ["Hawaiian Standard Time"]);
3521
3522 assert(tzConversions.fromWindows.length == 4);
3523 assert(tzConversions.fromWindows["Alaskan Standard Time"] ==
3524 ["America/Anchorage", "America/Juneau", "America/Nome", "America/Sitka", "America/Yakutat"]);
3525 assert(tzConversions.fromWindows["Dateline Standard Time"] == ["Etc/GMT+12"]);
3526 assert(tzConversions.fromWindows["Hawaiian Standard Time"] ==
3527 ["Etc/GMT+10", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Rarotonga", "Pacific/Tahiti"]);
3528 assert(tzConversions.fromWindows["UTC-11"] ==
3529 ["Etc/GMT+11", "Pacific/Midway", "Pacific/Niue", "Pacific/Pago_Pago"]);
3530
3531 foreach (key, value; tzConversions.fromWindows)
3532 {
3533 assert(value.isSorted, key);
3534 assert(equal(value.uniq(), value), key);
3535 }
3536 }