[Android] Enable access to up-to-date tzdata on Android 10+ (#20350)
[mono-project.git] / mcs / class / corlib / System / TimeZoneInfo.Android.cs
blob792a9fe2bc3363a02d4a3bafbf1a6fd61271f86e
1 /*
2 * System.TimeZoneInfo Android Support
4 * Author(s)
5 * Jonathan Pryor <jpryor@novell.com>
6 * The Android Open Source Project
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
21 #if MONODROID
23 using System;
24 using System.Collections.Generic;
25 using System.IO;
26 using System.Runtime.CompilerServices;
27 using System.Runtime.InteropServices;
28 using System.Text;
30 namespace System {
32 interface IAndroidTimeZoneDB {
33 IEnumerable<string> GetAvailableIds ();
34 byte[] GetTimeZoneData (string id);
37 [StructLayout (LayoutKind.Sequential, Pack=1)]
38 unsafe struct AndroidTzDataHeader {
39 public fixed byte signature [12];
40 public int indexOffset;
41 public int dataOffset;
42 public int zoneTabOffset;
45 [StructLayout (LayoutKind.Sequential, Pack=1)]
46 unsafe struct AndroidTzDataEntry {
47 public fixed byte id [40];
48 public int byteOffset;
49 public int length;
50 public int rawUtcOffset;
54 * Android v4.3 Timezone support infrastructure.
56 * This is a C# port of libcore.util.ZoneInfoDB:
58 * https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java
60 * This is needed in order to read Android v4.3 tzdata files.
62 * Android 10+ moved the up-to-date tzdata location to a module updatable via the Google Play Store and the
63 * database location changed (https://source.android.com/devices/architecture/modular-system/runtime#time-zone-data-interactions)
64 * The older locations still exist (at least the `/system/usr/share/zoneinfo` one) but they won't be updated.
66 sealed class AndroidTzData : IAndroidTimeZoneDB {
68 internal static readonly string[] Paths = new string[]{
69 GetApexTimeDataRoot () + "/etc/tz/tzdata", // Android 10+, TimeData module where the updates land
70 GetApexRuntimeRoot () + "/etc/tz/tzdata", // Android 10+, Fallback location if the above isn't found or corrupted
71 Environment.GetEnvironmentVariable ("ANDROID_DATA") + "/misc/zoneinfo/tzdata",
72 Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata",
75 string tzdataPath;
76 Stream data;
77 string version;
78 string zoneTab;
80 string[] ids;
81 int[] byteOffsets;
82 int[] lengths;
84 public AndroidTzData (params string[] paths)
86 foreach (var path in paths)
87 if (LoadData (path)) {
88 tzdataPath = path;
89 return;
92 Console.Error.WriteLine ("Couldn't find any tzdata!");
93 tzdataPath = "/";
94 version = "missing";
95 zoneTab = "# Emergency fallback data.\n";
96 ids = new[]{ "GMT" };
99 public string Version {
100 get {return version;}
103 public string ZoneTab {
104 get {return zoneTab;}
107 static string GetApexTimeDataRoot ()
109 string ret = Environment.GetEnvironmentVariable ("ANDROID_TZDATA_ROOT");
110 if (!String.IsNullOrEmpty (ret)) {
111 return ret;
114 return "/apex/com.android.tzdata";
117 static string GetApexRuntimeRoot ()
119 string ret = Environment.GetEnvironmentVariable ("ANDROID_RUNTIME_ROOT");
120 if (!String.IsNullOrEmpty (ret)) {
121 return ret;
124 return "/apex/com.android.runtime";
127 bool LoadData (string path)
129 if (!File.Exists (path))
130 return false;
131 try {
132 data = File.OpenRead (path);
133 } catch (IOException) {
134 return false;
135 } catch (UnauthorizedAccessException) {
136 return false;
139 try {
140 ReadHeader ();
141 return true;
142 } catch (Exception e) {
143 Console.Error.WriteLine ("tzdata file \"{0}\" was present but invalid: {1}", path, e);
145 return false;
148 unsafe void ReadHeader ()
150 int size = System.Math.Max (Marshal.SizeOf (typeof (AndroidTzDataHeader)), Marshal.SizeOf (typeof (AndroidTzDataEntry)));
151 var buffer = new byte [size];
152 var header = ReadAt<AndroidTzDataHeader>(0, buffer);
154 header.indexOffset = NetworkToHostOrder (header.indexOffset);
155 header.dataOffset = NetworkToHostOrder (header.dataOffset);
156 header.zoneTabOffset = NetworkToHostOrder (header.zoneTabOffset);
158 sbyte* s = (sbyte*) header.signature;
159 string magic = new string (s, 0, 6, Encoding.ASCII);
160 if (magic != "tzdata" || header.signature [11] != 0) {
161 var b = new StringBuilder ();
162 b.Append ("bad tzdata magic:");
163 for (int i = 0; i < 12; ++i) {
164 b.Append (" ").Append (((byte) s [i]).ToString ("x2"));
166 throw new InvalidOperationException ("bad tzdata magic: " + b.ToString ());
169 version = new string (s, 6, 5, Encoding.ASCII);
171 ReadIndex (header.indexOffset, header.dataOffset, buffer);
172 ReadZoneTab (header.zoneTabOffset, checked ((int) data.Length) - header.zoneTabOffset);
175 unsafe T ReadAt<T> (long position, byte[] buffer)
176 where T : struct
178 int size = Marshal.SizeOf (typeof (T));
179 if (buffer.Length < size)
180 throw new InvalidOperationException ("Internal error: buffer too small");
182 data.Position = position;
183 int r;
184 if ((r = data.Read (buffer, 0, size)) < size)
185 throw new InvalidOperationException (
186 string.Format ("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size));
188 fixed (byte* b = buffer)
189 return (T) Marshal.PtrToStructure ((IntPtr) b, typeof (T));
192 static int NetworkToHostOrder (int value)
194 if (!BitConverter.IsLittleEndian)
195 return value;
197 return
198 (((value >> 24) & 0xFF) |
199 ((value >> 08) & 0xFF00) |
200 ((value << 08) & 0xFF0000) |
201 ((value << 24)));
204 unsafe void ReadIndex (int indexOffset, int dataOffset, byte[] buffer)
206 int indexSize = dataOffset - indexOffset;
207 int entryCount = indexSize / Marshal.SizeOf (typeof (AndroidTzDataEntry));
208 int entrySize = Marshal.SizeOf (typeof (AndroidTzDataEntry));
210 byteOffsets = new int [entryCount];
211 ids = new string [entryCount];
212 lengths = new int [entryCount];
214 for (int i = 0; i < entryCount; ++i) {
215 var entry = ReadAt<AndroidTzDataEntry>(indexOffset + (entrySize*i), buffer);
216 var p = (sbyte*) entry.id;
218 byteOffsets [i] = NetworkToHostOrder (entry.byteOffset) + dataOffset;
219 ids [i] = new string (p, 0, GetStringLength (p, 40), Encoding.ASCII);
220 lengths [i] = NetworkToHostOrder (entry.length);
222 if (lengths [i] < Marshal.SizeOf (typeof (AndroidTzDataHeader)))
223 throw new InvalidOperationException ("Length in index file < sizeof(tzhead)");
227 static unsafe int GetStringLength (sbyte* s, int maxLength)
229 int len;
230 for (len = 0; len < maxLength; len++, s++) {
231 if (*s == 0)
232 break;
234 return len;
237 unsafe void ReadZoneTab (int zoneTabOffset, int zoneTabSize)
239 byte[] zoneTab = new byte [zoneTabSize];
241 data.Position = zoneTabOffset;
243 int r;
244 if ((r = data.Read (zoneTab, 0, zoneTab.Length)) < zoneTab.Length)
245 throw new InvalidOperationException (
246 string.Format ("Error reading zonetab: read {0} bytes, expected {1}", r, zoneTabSize));
248 this.zoneTab = Encoding.ASCII.GetString (zoneTab, 0, zoneTab.Length);
251 public IEnumerable<string> GetAvailableIds ()
253 return ids;
256 public byte[] GetTimeZoneData (string id)
258 int i = Array.BinarySearch (ids, id, StringComparer.Ordinal);
259 if (i < 0)
260 return null;
262 int offset = byteOffsets [i];
263 int length = lengths [i];
264 var buffer = new byte [length];
266 lock (data) {
267 data.Position = offset;
268 int r;
269 if ((r = data.Read (buffer, 0, buffer.Length)) < buffer.Length)
270 throw new InvalidOperationException (
271 string.Format ("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.",
272 tzdataPath, offset, length, r, buffer.Length));
275 TimeZoneInfo.DumpTimeZoneDataToFile (id, buffer);
276 return buffer;
280 partial class TimeZoneInfo {
282 static TimeZoneInfo CreateLocal ()
284 return AndroidTimeZones.Local;
287 static TimeZoneInfo FindSystemTimeZoneByIdCore (string id)
289 var timeZoneInfo = AndroidTimeZones.GetTimeZone (id, id);
290 if (timeZoneInfo == null)
291 throw new TimeZoneNotFoundException ();
292 return timeZoneInfo;
295 static void GetSystemTimeZonesCore (List<TimeZoneInfo> systemTimeZones)
297 foreach (string id in AndroidTimeZones.GetAvailableIds ()) {
298 var tz = AndroidTimeZones.GetTimeZone (id, id);
299 if (tz != null)
300 systemTimeZones.Add (tz);
305 * Android < v4.3 Timezone support infrastructure.
307 * This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB:
309 * http://android.git.kernel.org/?p=platform/libcore.git;a=blob;f=luni/src/main/java/org/apache/harmony/luni/internal/util/ZoneInfoDB.java;h=3e7bdc3a952b24da535806d434a3a27690feae26;hb=HEAD
311 * From the ZoneInfoDB source:
313 * However, to conserve disk space the data for all time zones are
314 * concatenated into a single file, and a second file is used to indicate
315 * the starting position of each time zone record. A third file indicates
316 * the version of the zoneinfo databse used to generate the data.
318 * which succinctly describes why we can't just use the LIBC implementation in
319 * TimeZoneInfo.cs -- the "standard Unixy" directory structure is NOT used.
321 sealed class ZoneInfoDB : IAndroidTimeZoneDB {
322 const int TimeZoneNameLength = 40;
323 const int TimeZoneIntSize = 4;
325 internal static readonly string ZoneDirectoryName = Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/";
327 const string ZoneFileName = "zoneinfo.dat";
328 const string IndexFileName = "zoneinfo.idx";
329 const string DefaultVersion = "2007h";
330 const string VersionFileName = "zoneinfo.version";
332 readonly string zoneRoot;
333 readonly string version;
334 readonly string[] names;
335 readonly int[] starts;
336 readonly int[] lengths;
337 readonly int[] offsets;
339 public ZoneInfoDB (string zoneInfoDB = null)
341 zoneRoot = zoneInfoDB ?? ZoneDirectoryName;
342 try {
343 version = ReadVersion (Path.Combine (zoneRoot, VersionFileName));
344 } catch {
345 version = DefaultVersion;
348 try {
349 ReadDatabase (Path.Combine (zoneRoot, IndexFileName), out names, out starts, out lengths, out offsets);
350 } catch {
351 names = new string [0];
352 starts = new int [0];
353 lengths = new int [0];
354 offsets = new int [0];
358 static string ReadVersion (string path)
360 using (var file = new StreamReader (path, Encoding.GetEncoding ("iso-8859-1"))) {
361 return file.ReadToEnd ().Trim ();
365 void ReadDatabase (string path, out string[] names, out int[] starts, out int[] lengths, out int[] offsets)
367 using (var file = File.OpenRead (path)) {
368 var nbuf = new byte [TimeZoneNameLength];
370 int numEntries = (int) (file.Length / (TimeZoneNameLength + 3*TimeZoneIntSize));
372 char[] namebuf = new char [TimeZoneNameLength];
374 names = new string [numEntries];
375 starts = new int [numEntries];
376 lengths = new int [numEntries];
377 offsets = new int [numEntries];
379 for (int i = 0; i < numEntries; ++i) {
380 Fill (file, nbuf, nbuf.Length);
381 int namelen;
382 for (namelen = 0; namelen < nbuf.Length; ++namelen) {
383 if (nbuf [namelen] == '\0')
384 break;
385 namebuf [namelen] = (char) (nbuf [namelen] & 0xFF);
388 names [i] = new string (namebuf, 0, namelen);
389 starts [i] = ReadInt32 (file, nbuf);
390 lengths [i] = ReadInt32 (file, nbuf);
391 offsets [i] = ReadInt32 (file, nbuf);
396 static void Fill (Stream stream, byte[] nbuf, int required)
398 int read = 0, offset = 0;
399 while (offset < required && (read = stream.Read (nbuf, offset, required - offset)) > 0)
400 offset += read;
401 if (read != required)
402 throw new EndOfStreamException ("Needed to read " + required + " bytes; read " + read + " bytes");
405 // From java.io.RandomAccessFioe.readInt(), as we need to use the same
406 // byte ordering as Java uses.
407 static int ReadInt32 (Stream stream, byte[] nbuf)
409 Fill (stream, nbuf, 4);
410 return ((nbuf [0] & 0xff) << 24) + ((nbuf [1] & 0xff) << 16) +
411 ((nbuf [2] & 0xff) << 8) + (nbuf [3] & 0xff);
414 internal string Version {
415 get {return version;}
418 public IEnumerable<string> GetAvailableIds ()
420 return GetAvailableIds (0, false);
423 IEnumerable<string> GetAvailableIds (int rawOffset)
425 return GetAvailableIds (rawOffset, true);
428 IEnumerable<string> GetAvailableIds (int rawOffset, bool checkOffset)
430 for (int i = 0; i < offsets.Length; ++i) {
431 if (!checkOffset || offsets [i] == rawOffset)
432 yield return names [i];
436 public byte[] GetTimeZoneData (string id)
438 int start, length;
439 using (var stream = GetTimeZoneData (id, out start, out length)) {
440 if (stream == null)
441 return null;
442 byte[] buf = new byte [length];
443 Fill (stream, buf, buf.Length);
444 return buf;
448 FileStream GetTimeZoneData (string name, out int start, out int length)
450 if (name == null) { // Just in case, to avoid NREX as in xambug #4902
451 start = 0;
452 length = 0;
453 return null;
456 var f = new FileInfo (Path.Combine (zoneRoot, name));
457 if (f.Exists) {
458 start = 0;
459 length = (int) f.Length;
460 return f.OpenRead ();
463 start = length = 0;
465 int i = Array.BinarySearch (names, name, StringComparer.Ordinal);
466 if (i < 0)
467 return null;
469 start = starts [i];
470 length = lengths [i];
472 var stream = File.OpenRead (Path.Combine (zoneRoot, ZoneFileName));
473 stream.Seek (start, SeekOrigin.Begin);
475 return stream;
479 static class AndroidTimeZones {
481 static IAndroidTimeZoneDB db;
483 static AndroidTimeZones ()
485 db = GetDefaultTimeZoneDB ();
488 static IAndroidTimeZoneDB GetDefaultTimeZoneDB ()
490 foreach (var p in AndroidTzData.Paths)
491 if (File.Exists (p))
492 return new AndroidTzData (AndroidTzData.Paths);
493 if (Directory.Exists (ZoneInfoDB.ZoneDirectoryName))
494 return new ZoneInfoDB ();
495 return null;
498 internal static IEnumerable<string> GetAvailableIds ()
500 return db == null
501 ? new string [0]
502 : db.GetAvailableIds ();
505 static TimeZoneInfo _GetTimeZone (string id, string name)
507 if (db == null)
508 return null;
509 byte[] buffer = db.GetTimeZoneData (name);
510 if (buffer == null)
511 return null;
512 return TimeZoneInfo.ParseTZBuffer (id, buffer, buffer.Length);
515 internal static TimeZoneInfo GetTimeZone (string id, string name)
517 if (name != null) {
518 if (name == "GMT" || name == "UTC")
519 return new TimeZoneInfo (id, TimeSpan.FromSeconds (0), id, name, name, null, disableDaylightSavingTime:true);
520 if (name.StartsWith ("GMT"))
521 return new TimeZoneInfo (id,
522 TimeSpan.FromSeconds (ParseNumericZone (name)),
523 id, name, name, null, disableDaylightSavingTime:true);
526 try {
527 return _GetTimeZone (id, name);
528 } catch (Exception) {
529 return null;
533 static int ParseNumericZone (string name)
535 if (name == null || !name.StartsWith ("GMT") || name.Length <= 3)
536 return 0;
538 int sign;
539 if (name [3] == '+')
540 sign = 1;
541 else if (name [3] == '-')
542 sign = -1;
543 else
544 return 0;
546 int where;
547 int hour = 0;
548 bool colon = false;
549 for (where = 4; where < name.Length; where++) {
550 char c = name [where];
552 if (c == ':') {
553 where++;
554 colon = true;
555 break;
558 if (c >= '0' && c <= '9')
559 hour = hour * 10 + c - '0';
560 else
561 return 0;
564 int min = 0;
565 for (; where < name.Length; where++) {
566 char c = name [where];
568 if (c >= '0' && c <= '9')
569 min = min * 10 + c - '0';
570 else
571 return 0;
574 if (colon)
575 return sign * (hour * 60 + min) * 60;
576 else if (hour >= 100)
577 return sign * ((hour / 100) * 60 + (hour % 100)) * 60;
578 else
579 return sign * (hour * 60) * 60;
582 internal static TimeZoneInfo Local {
583 get {
584 var id = GetDefaultTimeZoneName ();
585 return GetTimeZone (id, id);
589 [DllImport ("__Internal")]
590 static extern int monodroid_get_system_property (string name, ref IntPtr value);
592 [DllImport ("__Internal")]
593 static extern void monodroid_free (IntPtr ptr);
595 static string GetDefaultTimeZoneName ()
597 IntPtr value = IntPtr.Zero;
598 int n = 0;
599 string defaultTimeZone = Environment.GetEnvironmentVariable ("__XA_OVERRIDE_TIMEZONE_ID__");
601 if (!string.IsNullOrEmpty (defaultTimeZone))
602 return defaultTimeZone;
604 // Used by the tests
605 if (Environment.GetEnvironmentVariable ("__XA_USE_JAVA_DEFAULT_TIMEZONE_ID__") == null)
606 n = monodroid_get_system_property ("persist.sys.timezone", ref value);
608 if (n > 0 && value != IntPtr.Zero) {
609 defaultTimeZone = (Marshal.PtrToStringAnsi (value) ?? String.Empty).Trim ();
610 monodroid_free (value);
611 if (!String.IsNullOrEmpty (defaultTimeZone))
612 return defaultTimeZone;
615 defaultTimeZone = (AndroidPlatform.GetDefaultTimeZone () ?? String.Empty).Trim ();
616 if (!String.IsNullOrEmpty (defaultTimeZone))
617 return defaultTimeZone;
619 return null;
622 #if SELF_TEST
624 * Compile:
625 * mcs /debug+ /out:tzi.exe /unsafe "/d:INSIDE_CORLIB;MONODROID;NET_4_0;LIBC;SELF_TEST" ../corlib/System/AndroidPlatform.cs System/TimeZone*.cs ../../build/common/Consts.cs ../Mono.Options/Mono.Options/Options.cs
626 * Prep:
627 * mkdir -p android/tzdb/usr/share/zoneinfo
628 * mkdir -p android/tzdb/misc/zoneinfo/zoneinfo
629 * android_root=`adb shell echo '$ANDROID_ROOT' | tr -d "\r"`
630 * android_data=`adb shell echo '$ANDROID_DATA' | tr -d "\r"`
631 * adb pull $android_root/usr/share/zoneinfo android/tzdb/usr/share/zoneinfo
632 * adb pull $android_data/misc/zoneinfo/tzdata android/tzdb/misc/zoneinfo
633 * Run:
634 * # Dump all timezone names
635 * __XA_OVERRIDE_TIMEZONE_ID__=America/New_York ANDROID_ROOT=`pwd` ANDROID_DATA=`pwd` mono --debug tzi.exe --offset=1969-01-01
637 * # Dump TimeZone data to files under path `tzdata`
638 * __XA_OVERRIDE_TIMEZONE_ID__=America/New_York ANDROID_ROOT=`pwd` ANDROID_DATA=`pwd` mono --debug tzi.exe -o android/tzdata
640 * # Dump TimeZone rules for specific timezone data (as dumped above)
641 * mono tzi.exe --offset=2012-10-24 -i=tzdata/Asia/Amman
643 static void Main (string[] args)
645 DateTime? offset = null;
646 Func<IAndroidTimeZoneDB> c = () => GetDefaultTimeZoneDB ();
647 bool dump_rules = false;
648 Mono.Options.OptionSet p = null;
649 p = new Mono.Options.OptionSet () {
650 { "i=",
651 "TimeZone data {FILE} to parse and dump",
652 v => DumpTimeZoneFile (v, offset)
654 { "o=",
655 "Write TimeZone data files to {PATH}",
656 v => TimeZoneInfo.TimeZoneDataExportPath = v
658 { "T=", "Create AndroidTzData from {PATH}.", v => {
659 c = () => new AndroidTzData (v);
660 } },
661 { "Z=", "Create ZoneInfoDB from {DIR}.", v => {
662 c = () => new ZoneInfoDB (v);
663 } },
664 { "offset=", "Show timezone info offset for DateTime {OFFSET}.", v => {
665 offset = DateTime.Parse (v);
666 Console.WriteLine ("Using DateTime Offset: {0}", offset);
667 } },
668 { "R|dump-rules",
669 "Show timezone info offset for DateTime {OFFSET}.",
670 v => dump_rules = v != null },
671 { "help", "Show this message and exit", v => {
672 p.WriteOptionDescriptions (Console.Out);
673 Environment.Exit (0);
674 } },
676 p.Parse (args);
677 AndroidTimeZones.db = c ();
678 Console.WriteLine ("DB type: {0}", AndroidTimeZones.db.GetType ().FullName);
679 foreach (var id in GetAvailableIds ()) {
680 Console.Write ("name={0,-40}", id);
681 try {
682 TimeZoneInfo zone = _GetTimeZone (id, id);
683 if (zone != null) {
684 Console.Write (" {0,-40}", zone);
685 if (offset.HasValue) {
686 Console.Write ("From Offset: {0}", zone.GetUtcOffset (offset.Value));
688 if (dump_rules) {
689 WriteZoneRules (zone);
692 else {
693 Console.Write (" ERROR:null");
695 } catch (Exception e) {
696 Console.WriteLine ();
697 Console.Write ("ERROR: {0}", e);
699 Console.WriteLine ();
703 static void WriteZoneRules (TimeZoneInfo zone)
705 var rules = zone.GetAdjustmentRules ();
706 for (int i = 0; i < rules.Length; ++i) {
707 var rule = rules [i];
708 Console.WriteLine ();
709 Console.Write ("\tAdjustmentRules[{0,3}]: DaylightDelta={1}; DateStart={2:yyyy-MM}; DateEnd={3:yyyy-MM}; DaylightTransitionStart={4:D2}-{5:D2}T{6}; DaylightTransitionEnd={7:D2}-{8:D2}T{9}",
711 rule.DaylightDelta,
712 rule.DateStart, rule.DateEnd,
713 rule.DaylightTransitionStart.Month, rule.DaylightTransitionStart.Day, rule.DaylightTransitionStart.TimeOfDay.TimeOfDay,
714 rule.DaylightTransitionEnd.Month, rule.DaylightTransitionEnd.Day, rule.DaylightTransitionEnd.TimeOfDay.TimeOfDay);
718 static void DumpTimeZoneFile (string path, DateTime? time)
720 var buffer = File.ReadAllBytes (path);
721 var zone = ParseTZBuffer (path, buffer, buffer.Length);
722 Console.Write ("Rules for: {0}", path);
723 WriteZoneRules (zone);
724 Console.WriteLine ();
725 if (time.HasValue) {
726 var offset = zone.GetUtcOffset (time.Value);
727 var isDst = zone.IsDaylightSavingTime (time.Value);
728 Console.WriteLine ("\tDate({0}): Offset({1}) IsDST({2})", time.Value, offset, isDst);
731 if (zone.transitions != null) {
732 Console.WriteLine ("Transitions for: {0}", path);
733 foreach (var transition in zone.transitions) {
734 Console.WriteLine ("\t Date({0}): {1}", transition.Key, transition.Value);
738 #endif
741 #if SELF_TEST
742 static string TimeZoneDataExportPath;
743 #endif
745 internal static void DumpTimeZoneDataToFile (string id, byte[] buffer)
747 #if SELF_TEST
748 int p = id.LastIndexOf ('/');
749 var o = Path.Combine (TimeZoneDataExportPath,
750 p >= 0 ? id.Substring (0, p) : id);
751 if (p >= 0)
752 o = Path.Combine (o, id.Substring (p+1));
753 Directory.CreateDirectory (Path.GetDirectoryName (o));
754 using (var f = File.OpenWrite (o))
755 f.Write (buffer, 0, buffer.Length);
756 #endif
761 #endif // MONODROID