Skip to content

Commit 21aa337

Browse files
committed
Bringing back TimeZoneInfo.Unity to support time zones better
1 parent 8fe8828 commit 21aa337

12 files changed

+250
-18
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//
2+
// System.TimeZoneInfo helper for Unity
3+
// because the devices cannot access the file system to read the data
4+
//
5+
// Authors:
6+
// Michael DeRoy <[email protected]>
7+
// Jonathan Chambers <[email protected]>
8+
//
9+
// Copyright 2018 Unity Technologies, Inc.
10+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
11+
12+
#if UNITY
13+
14+
using System;
15+
using System.Collections.Generic;
16+
using System.Collections.ObjectModel;
17+
using System.Globalization;
18+
using System.IO;
19+
using System.Runtime.CompilerServices;
20+
21+
namespace System {
22+
23+
public partial class TimeZoneInfo {
24+
enum TimeZoneData
25+
{
26+
DaylightSavingFirstTransitionIdx,
27+
DaylightSavingSecondTransitionIdx,
28+
UtcOffsetIdx,
29+
AdditionalDaylightOffsetIdx
30+
};
31+
32+
enum TimeZoneNames
33+
{
34+
StandardNameIdx,
35+
DaylightNameIdx
36+
};
37+
38+
static List<AdjustmentRule> CreateAdjustmentRule (int year, out Int64[] data, out string[] names, string standardNameCurrentYear, string daylightNameCurrentYear)
39+
{
40+
List<AdjustmentRule> rulesForYear = new List<AdjustmentRule> ();
41+
bool dst_inverted;
42+
if (!System.CurrentSystemTimeZone.GetTimeZoneData(year, out data, out names, out dst_inverted))
43+
return rulesForYear;
44+
var firstTransition = new DateTime (data[(int)TimeZoneData.DaylightSavingFirstTransitionIdx]);
45+
var secondTransition = new DateTime (data[(int)TimeZoneData.DaylightSavingSecondTransitionIdx]);
46+
var daylightOffset = new TimeSpan (data[(int)TimeZoneData.AdditionalDaylightOffsetIdx]);
47+
48+
/* C# TimeZoneInfo does not support timezones the same way as unix. In unix, timezone files are specified by region such as
49+
* America/New_York or Asia/Singapore. If a region like Asia/Singapore changes it's timezone from +0730 to +08, the UTC offset
50+
* has changed, but there is no support in the C# code to transition to this new UTC offset except for the case of daylight
51+
* savings time. As such we'll only generate timezone rules for a region at the times associated with the timezone of the current year.
52+
*/
53+
if(standardNameCurrentYear != names[(int)TimeZoneNames.StandardNameIdx])
54+
return rulesForYear;
55+
if(daylightNameCurrentYear != names[(int)TimeZoneNames.DaylightNameIdx])
56+
return rulesForYear;
57+
58+
// If the first and second transition DateTime objects are the same, ValidateAdjustmentRule will throw
59+
// an exception. I'm unsure why these would be the same, but we do see that occur for some locales.
60+
// In that case, just exit early.
61+
if (firstTransition.Equals(secondTransition))
62+
return rulesForYear;
63+
64+
var beginningOfYear = new DateTime (year, 1, 1, 0, 0, 0, 0);
65+
var endOfYearDay = new DateTime (year, 12, DateTime.DaysInMonth (year, 12));
66+
var endOfYearMaxTimeout = new DateTime (year, 12, DateTime.DaysInMonth(year, 12), 23, 59, 59, 999);
67+
68+
if (!dst_inverted) {
69+
// For daylight savings time that happens between jan and dec, create a rule from jan 1 to dec 31 (the entire year)
70+
71+
// This rule (for the whole year) specifies the starting and ending months of daylight savings time.
72+
var startOfDaylightSavingsTime = TransitionTime.CreateFixedDateRule (new DateTime (1,1,1).Add (firstTransition.TimeOfDay),
73+
firstTransition.Month, firstTransition.Day);
74+
var endOfDaylightSavingsTime = TransitionTime.CreateFixedDateRule (new DateTime (1,1,1).Add (secondTransition.TimeOfDay),
75+
secondTransition.Month, secondTransition.Day);
76+
77+
var fullYearRule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule (beginningOfYear,
78+
endOfYearDay,
79+
daylightOffset,
80+
startOfDaylightSavingsTime,
81+
endOfDaylightSavingsTime);
82+
rulesForYear.Add (fullYearRule);
83+
} else {
84+
// Some timezones (Australia/Sydney) have daylight savings over the new year.
85+
// Our icall returns the transitions for the current year, so we need two adjustment rules each year for this case
86+
87+
// The first rule specifies daylight savings starting at jan 1 and ending at the first transition.
88+
var startOfFirstDaylightSavingsTime = TransitionTime.CreateFixedDateRule (new DateTime (1,1,1), 1, 1);
89+
var endOfFirstDaylightSavingsTime = TransitionTime.CreateFixedDateRule (new DateTime (1,1,1).Add (firstTransition.TimeOfDay),
90+
firstTransition.Month, firstTransition.Day);
91+
92+
var transitionOutOfDaylightSavingsRule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule (
93+
new DateTime (year, 1, 1),
94+
new DateTime (firstTransition.Year, firstTransition.Month, firstTransition.Day),
95+
daylightOffset,
96+
startOfFirstDaylightSavingsTime,
97+
endOfFirstDaylightSavingsTime);
98+
rulesForYear.Add (transitionOutOfDaylightSavingsRule);
99+
100+
// The second rule specifies daylight savings time starting the day after we transition out of daylight savings
101+
// and ending at the end of the year, with daylight savings starting near the end and ending on the last day of the year
102+
var startOfSecondDaylightSavingsTime = TransitionTime.CreateFixedDateRule (new DateTime (1,1,1).Add (secondTransition.TimeOfDay),
103+
secondTransition.Month, secondTransition.Day);
104+
var endOfSecondDaylightSavingsTime = TransitionTime.CreateFixedDateRule (new DateTime (1,1,1).Add (endOfYearMaxTimeout.TimeOfDay),
105+
endOfYearMaxTimeout.Month, endOfYearMaxTimeout.Day);
106+
107+
var transitionIntoDaylightSavingsRule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule (
108+
new DateTime (firstTransition.Year, firstTransition.Month, firstTransition.Day).AddDays (1),
109+
endOfYearDay,
110+
daylightOffset,
111+
startOfSecondDaylightSavingsTime,
112+
endOfSecondDaylightSavingsTime);
113+
rulesForYear.Add (transitionIntoDaylightSavingsRule);
114+
}
115+
return rulesForYear;
116+
}
117+
118+
static TimeZoneInfo CreateLocalUnity ()
119+
{
120+
Int64[] data;
121+
string[] names;
122+
//Some timezones start in DST on january first and disable it during the summer
123+
bool dst_inverted;
124+
int currentYear = DateTime.UtcNow.Year;
125+
if (!System.CurrentSystemTimeZone.GetTimeZoneData (currentYear, out data, out names, out dst_inverted))
126+
throw new NotSupportedException ("Can't get timezone name.");
127+
128+
var utcOffsetTS = TimeSpan.FromTicks (data[(int)TimeZoneData.UtcOffsetIdx]);
129+
char utcOffsetSign = (utcOffsetTS >= TimeSpan.Zero) ? '+' : '-';
130+
string displayName = "(GMT" + utcOffsetSign + utcOffsetTS.ToString (@"hh\:mm") + ") Local Time";
131+
string standardDisplayName = names[(int)TimeZoneNames.StandardNameIdx];
132+
string daylightDisplayName = names[(int)TimeZoneNames.DaylightNameIdx];
133+
134+
var adjustmentRulesList = new List<AdjustmentRule> ();
135+
bool disableDaylightSavings = data[(int)TimeZoneData.AdditionalDaylightOffsetIdx] == 0;
136+
//If the timezone supports daylight savings time, generate adjustment rules for the timezone
137+
if (!disableDaylightSavings) {
138+
//the icall only supports years from 1970 through 2037.
139+
int firstSupportedDate = 1971;
140+
int lastSupportedDate = 2037;
141+
142+
//first, generate rules from the current year until the last year mktime is guaranteed to supports
143+
for (int year = currentYear; year <= lastSupportedDate; year++) {
144+
var rulesForCurrentYear = CreateAdjustmentRule (year, out data, out names, standardDisplayName, daylightDisplayName);
145+
//breakout if no more rules
146+
if (rulesForCurrentYear.Count > 0)
147+
adjustmentRulesList.AddRange (rulesForCurrentYear);
148+
else
149+
break;
150+
151+
}
152+
153+
for (int year = currentYear - 1; year >= firstSupportedDate; year--) {
154+
var rulesForCurrentYear = CreateAdjustmentRule (year, out data, out names, standardDisplayName, daylightDisplayName);
155+
//breakout if no more rules
156+
if (rulesForCurrentYear.Count > 0)
157+
adjustmentRulesList.AddRange (rulesForCurrentYear);
158+
else
159+
break;
160+
}
161+
162+
adjustmentRulesList.Sort ( (rule1, rule2) => rule1.DateStart.CompareTo (rule2.DateStart) );
163+
}
164+
return TimeZoneInfo.CreateCustomTimeZone ("Local",
165+
utcOffsetTS,
166+
displayName,
167+
standardDisplayName,
168+
daylightDisplayName,
169+
adjustmentRulesList.ToArray (),
170+
disableDaylightSavings);
171+
}
172+
}
173+
174+
partial class TimeZone
175+
{
176+
// Internal method to get timezone data.
177+
// data[0]: start of daylight saving time (in DateTime ticks).
178+
// data[1]: end of daylight saving time (in DateTime ticks).
179+
// data[2]: utcoffset (in TimeSpan ticks).
180+
// data[3]: additional offset when daylight saving (in TimeSpan ticks).
181+
// name[0]: name of this timezone when not daylight saving.
182+
// name[1]: name of this timezone when daylight saving.
183+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
184+
public static extern bool GetTimeZoneData (int year, out Int64[] data, out string[] names, out bool daylight_inverted);
185+
}
186+
}
187+
188+
#endif

mcs/class/corlib/System/TimeZoneInfo.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ private static string readlink (string path)
122122
private static bool TryGetNameFromPath (string path, out string name)
123123
{
124124
name = null;
125+
126+
#if UNITY
127+
//Avoids calling readlink on webgl, which causes abort due to dlopen
128+
if(!File.Exists(path))
129+
return false;
130+
#endif
131+
125132
var linkPath = readlink (path);
126133
if (linkPath != null) {
127134
if (Path.IsPathRooted(linkPath))
@@ -176,15 +183,34 @@ static TimeZoneInfo CreateLocal ()
176183
} catch {
177184
return Utc;
178185
}
179-
#else
186+
#else
187+
#if UNITY
188+
TimeZoneInfo localTimeZoneFallback = null;
189+
try {
190+
localTimeZoneFallback = CreateLocalUnity();
191+
} catch {
192+
localTimeZoneFallback = null;
193+
}
194+
195+
if (localTimeZoneFallback == null)
196+
localTimeZoneFallback = Utc;
197+
#endif
180198
var tz = Environment.GetEnvironmentVariable ("TZ");
181199
if (tz != null) {
182200
if (tz == String.Empty)
201+
#if UNITY
202+
return localTimeZoneFallback;
203+
#else
183204
return Utc;
205+
#endif
184206
try {
185207
return FindSystemTimeZoneByFileName (tz, Path.Combine (TimeZoneDirectory, tz));
186208
} catch {
209+
#if UNITY
210+
return localTimeZoneFallback;
211+
#else
187212
return Utc;
213+
#endif
188214
}
189215
}
190216

@@ -203,7 +229,11 @@ static TimeZoneInfo CreateLocal ()
203229
}
204230
}
205231

232+
#if UNITY
233+
return localTimeZoneFallback;
234+
#else
206235
return Utc;
236+
#endif
207237
#endif
208238
}
209239

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
#include unix_build_corlib.dll.sources
1+
#include unix_build_corlib.dll.sources
2+
System/TimeZoneInfo.Unity.cs
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
#include unix_build_corlib.dll.sources
1+
#include unix_build_corlib.dll.sources
2+
System/TimeZoneInfo.Unity.cs
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include win32_build_corlib.dll.sources
2+
System/TimeZoneInfo.Unity.cs
23

34
../../../external/corefx/src/Common/src/Interop/Windows/kernel32/Interop.GetFileInformationByHandleEx.cs
45
../../../external/corefx/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.WinRT.cs

mono/metadata/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ unity_sources = \
222222
unity-memory-info.c \
223223
unity-memory-info.h \
224224
unity-utils.c \
225-
unity-utils.h
225+
unity-utils.h \
226+
unity-icall.c
226227

227228
if !DISABLE_ICALL_TABLES
228229
icall_tables_sources = \

mono/metadata/icall-def.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ HANDLES(CONSOLE_3, "SetBreak", ves_icall_System_ConsoleDriver_SetBreak, MonoBool
252252
HANDLES(CONSOLE_4, "SetEcho", ves_icall_System_ConsoleDriver_SetEcho, MonoBoolean, 1, (MonoBoolean))
253253
HANDLES(CONSOLE_5, "TtySetup", ves_icall_System_ConsoleDriver_TtySetup, MonoBoolean, 4, (MonoString, MonoString, MonoArrayOut, int_ptr_ref))
254254

255+
ICALL_TYPE(TZONE, "System.CurrentSystemTimeZone", TZONE_1)
256+
ICALL(TZONE_1, "GetTimeZoneData", ves_icall_System_CurrentSystemTimeZone_GetTimeZoneData)
257+
255258
ICALL_TYPE(DTIME, "System.DateTime", DTIME_1)
256259
NOHANDLES(ICALL(DTIME_1, "GetSystemTimeAsFileTime", ves_icall_System_DateTime_GetSystemTimeAsFileTime))
257260

mono/metadata/icall-internals.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
#include <glib.h>
1111
#include <mono/metadata/object-internals.h>
1212

13+
// UNITY
14+
guint32
15+
ves_icall_System_CurrentSystemTimeZone_GetTimeZoneData (guint32 year, MonoArray **data, MonoArray **names, MonoBoolean *daylight_inverted);
16+
1317
// On Windows platform implementation of bellow methods are hosted in separate source file
1418
// icall-windows.c or icall-windows-*.c. On other platforms the implementation is still keept
1519
// in icall.c still declared as static and in some places even inlined.

mono/metadata/icall-table.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include <mono/utils/mono-publib.h>
3636
#include <mono/utils/bsearch.h>
3737
#include <mono/metadata/icalls.h>
38+
#include <mono/metadata/icall-internals.h>
3839
#include "handle-decl.h"
3940
#include "icall-decl.h"
4041

mono/metadata/unity-icall.c

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <mono/metadata/metadata-internals.h>
1515
#include <mono/metadata/object.h>
1616
#include <mono/metadata/object-internals.h>
17+
#include <mono/metadata/icall-internals.h>
1718

1819
#ifdef HAVE_SYS_TIME_H
1920
#include <sys/time.h>
@@ -100,7 +101,8 @@ gmt_offset(struct tm *tm, time_t t)
100101
guint32
101102
ves_icall_System_CurrentSystemTimeZone_GetTimeZoneData (guint32 year, MonoArray **data, MonoArray **names, MonoBoolean *daylight_inverted)
102103
{
103-
MonoError error;
104+
MonoError lerror;
105+
MonoError* error = &lerror;
104106
#ifndef PLATFORM_WIN32
105107
MonoDomain *domain = mono_domain_get ();
106108
struct tm start, tt;
@@ -114,8 +116,8 @@ ves_icall_System_CurrentSystemTimeZone_GetTimeZoneData (guint32 year, MonoArray
114116
MONO_CHECK_ARG_NULL (data, FALSE);
115117
MONO_CHECK_ARG_NULL (names, FALSE);
116118

117-
mono_gc_wbarrier_generic_store (data, (MonoObject*) mono_array_new_checked (domain, mono_defaults.int64_class, 4, &error));
118-
mono_gc_wbarrier_generic_store (names, (MonoObject*) mono_array_new_checked (domain, mono_defaults.string_class, 2, &error));
119+
mono_gc_wbarrier_generic_store_internal (data, (MonoObject*) mono_array_new_checked (domain, mono_defaults.int64_class, 4, error));
120+
mono_gc_wbarrier_generic_store_internal (names, (MonoObject*) mono_array_new_checked (domain, mono_defaults.string_class, 2, error));
119121

120122
/*
121123
* no info is better than crashing: we'll need our own tz data
@@ -136,8 +138,8 @@ ves_icall_System_CurrentSystemTimeZone_GetTimeZoneData (guint32 year, MonoArray
136138
t = time (NULL);
137139
tt = *localtime (&t);
138140
strftime (tzone, sizeof (tzone), "%Z", &tt);
139-
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, &error));
140-
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, &error));
141+
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, error));
142+
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, error));
141143
*daylight_inverted = 0;
142144
return 1;
143145
}
@@ -176,17 +178,17 @@ ves_icall_System_CurrentSystemTimeZone_GetTimeZoneData (guint32 year, MonoArray
176178
/* Write data, if we're already in daylight saving, we're done. */
177179
if (is_transitioned) {
178180
if (!start.tm_isdst)
179-
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, &error));
181+
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, error));
180182
else
181-
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, &error));
183+
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, error));
182184

183185
mono_array_set ((*data), gint64, 1, ((gint64)t1 + EPOCH_ADJUST) * 10000000L);
184186
return 1;
185187
} else {
186188
if (!start.tm_isdst)
187-
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, &error));
189+
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, error));
188190
else
189-
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, &error));
191+
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, error));
190192

191193
mono_array_set ((*data), gint64, 0, ((gint64)t1 + EPOCH_ADJUST) * 10000000L);
192194
is_transitioned = 1;
@@ -208,8 +210,8 @@ ves_icall_System_CurrentSystemTimeZone_GetTimeZoneData (guint32 year, MonoArray
208210

209211
if (!is_transitioned) {
210212
strftime (tzone, sizeof (tzone), "%Z", &tt);
211-
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, &error));
212-
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, &error));
213+
mono_array_setref ((*names), 0, mono_string_new_checked (domain, tzone, error));
214+
mono_array_setref ((*names), 1, mono_string_new_checked (domain, tzone, error));
213215
mono_array_set ((*data), gint64, 0, 0);
214216
mono_array_set ((*data), gint64, 1, 0);
215217
mono_array_set ((*data), gint64, 2, (gint64) gmtoff * 10000000L);

0 commit comments

Comments
 (0)