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