|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
|
2 * vim: set ts=8 sts=4 et sw=4 tw=99: |
|
3 * This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 #include "vm/DateTime.h" |
|
8 |
|
9 #include <time.h> |
|
10 |
|
11 #include "jsutil.h" |
|
12 |
|
13 using mozilla::UnspecifiedNaN; |
|
14 |
|
15 static bool |
|
16 ComputeLocalTime(time_t local, struct tm *ptm) |
|
17 { |
|
18 #ifdef HAVE_LOCALTIME_R |
|
19 return localtime_r(&local, ptm); |
|
20 #else |
|
21 struct tm *otm = localtime(&local); |
|
22 if (!otm) |
|
23 return false; |
|
24 *ptm = *otm; |
|
25 return true; |
|
26 #endif |
|
27 } |
|
28 |
|
29 static bool |
|
30 ComputeUTCTime(time_t t, struct tm *ptm) |
|
31 { |
|
32 #ifdef HAVE_GMTIME_R |
|
33 return gmtime_r(&t, ptm); |
|
34 #else |
|
35 struct tm *otm = gmtime(&t); |
|
36 if (!otm) |
|
37 return false; |
|
38 *ptm = *otm; |
|
39 return true; |
|
40 #endif |
|
41 } |
|
42 |
|
43 /* |
|
44 * Compute the offset in seconds from the current UTC time to the current local |
|
45 * standard time (i.e. not including any offset due to DST). |
|
46 * |
|
47 * Examples: |
|
48 * |
|
49 * Suppose we are in California, USA on January 1, 2013 at 04:00 PST (UTC-8, no |
|
50 * DST in effect), corresponding to 12:00 UTC. This function would then return |
|
51 * -8 * SecondsPerHour, or -28800. |
|
52 * |
|
53 * Or suppose we are in Berlin, Germany on July 1, 2013 at 17:00 CEST (UTC+2, |
|
54 * DST in effect), corresponding to 15:00 UTC. This function would then return |
|
55 * +1 * SecondsPerHour, or +3600. |
|
56 */ |
|
57 static int32_t |
|
58 UTCToLocalStandardOffsetSeconds() |
|
59 { |
|
60 using js::SecondsPerDay; |
|
61 using js::SecondsPerHour; |
|
62 using js::SecondsPerMinute; |
|
63 |
|
64 #if defined(XP_WIN) |
|
65 // Windows doesn't follow POSIX: updates to the TZ environment variable are |
|
66 // not reflected immediately on that platform as they are on other systems |
|
67 // without this call. |
|
68 _tzset(); |
|
69 #endif |
|
70 |
|
71 // Get the current time. |
|
72 time_t currentMaybeWithDST = time(nullptr); |
|
73 if (currentMaybeWithDST == time_t(-1)) |
|
74 return 0; |
|
75 |
|
76 // Break down the current time into its (locally-valued, maybe with DST) |
|
77 // components. |
|
78 struct tm local; |
|
79 if (!ComputeLocalTime(currentMaybeWithDST, &local)) |
|
80 return 0; |
|
81 |
|
82 // Compute a |time_t| corresponding to |local| interpreted without DST. |
|
83 time_t currentNoDST; |
|
84 if (local.tm_isdst == 0) { |
|
85 // If |local| wasn't DST, we can use the same time. |
|
86 currentNoDST = currentMaybeWithDST; |
|
87 } else { |
|
88 // If |local| respected DST, we need a time broken down into components |
|
89 // ignoring DST. Turn off DST in the broken-down time. |
|
90 local.tm_isdst = 0; |
|
91 |
|
92 // Compute a |time_t t| corresponding to the broken-down time with DST |
|
93 // off. This has boundary-condition issues (for about the duration of |
|
94 // a DST offset) near the time a location moves to a different time |
|
95 // zone. But 1) errors will be transient; 2) locations rarely change |
|
96 // time zone; and 3) in the absence of an API that provides the time |
|
97 // zone offset directly, this may be the best we can do. |
|
98 currentNoDST = mktime(&local); |
|
99 if (currentNoDST == time_t(-1)) |
|
100 return 0; |
|
101 } |
|
102 |
|
103 // Break down the time corresponding to the no-DST |local| into UTC-based |
|
104 // components. |
|
105 struct tm utc; |
|
106 if (!ComputeUTCTime(currentNoDST, &utc)) |
|
107 return 0; |
|
108 |
|
109 // Finally, compare the seconds-based components of the local non-DST |
|
110 // representation and the UTC representation to determine the actual |
|
111 // difference. |
|
112 int utc_secs = utc.tm_hour * SecondsPerHour + utc.tm_min * SecondsPerMinute; |
|
113 int local_secs = local.tm_hour * SecondsPerHour + local.tm_min * SecondsPerMinute; |
|
114 |
|
115 // Same-day? Just subtract the seconds counts. |
|
116 if (utc.tm_mday == local.tm_mday) |
|
117 return local_secs - utc_secs; |
|
118 |
|
119 // If we have more UTC seconds, move local seconds into the UTC seconds' |
|
120 // frame of reference and then subtract. |
|
121 if (utc_secs > local_secs) |
|
122 return (SecondsPerDay + local_secs) - utc_secs; |
|
123 |
|
124 // Otherwise we have more local seconds, so move the UTC seconds into the |
|
125 // local seconds' frame of reference and then subtract. |
|
126 return local_secs - (utc_secs + SecondsPerDay); |
|
127 } |
|
128 |
|
129 void |
|
130 js::DateTimeInfo::updateTimeZoneAdjustment() |
|
131 { |
|
132 /* |
|
133 * The difference between local standard time and UTC will never change for |
|
134 * a given time zone. |
|
135 */ |
|
136 utcToLocalStandardOffsetSeconds = UTCToLocalStandardOffsetSeconds(); |
|
137 |
|
138 double newTZA = utcToLocalStandardOffsetSeconds * msPerSecond; |
|
139 if (newTZA == localTZA_) |
|
140 return; |
|
141 |
|
142 localTZA_ = newTZA; |
|
143 |
|
144 /* |
|
145 * The initial range values are carefully chosen to result in a cache miss |
|
146 * on first use given the range of possible values. Be careful to keep |
|
147 * these values and the caching algorithm in sync! |
|
148 */ |
|
149 offsetMilliseconds = 0; |
|
150 rangeStartSeconds = rangeEndSeconds = INT64_MIN; |
|
151 oldOffsetMilliseconds = 0; |
|
152 oldRangeStartSeconds = oldRangeEndSeconds = INT64_MIN; |
|
153 |
|
154 sanityCheck(); |
|
155 } |
|
156 |
|
157 /* |
|
158 * Since getDSTOffsetMilliseconds guarantees that all times seen will be |
|
159 * positive, we can initialize the range at construction time with large |
|
160 * negative numbers to ensure the first computation is always a cache miss and |
|
161 * doesn't return a bogus offset. |
|
162 */ |
|
163 js::DateTimeInfo::DateTimeInfo() |
|
164 { |
|
165 // Set to a totally impossible TZA so that the comparison above will fail |
|
166 // and all fields will be properly initialized. |
|
167 localTZA_ = UnspecifiedNaN<double>(); |
|
168 updateTimeZoneAdjustment(); |
|
169 } |
|
170 |
|
171 int64_t |
|
172 js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds) |
|
173 { |
|
174 MOZ_ASSERT(utcSeconds >= 0); |
|
175 MOZ_ASSERT(utcSeconds <= MaxUnixTimeT); |
|
176 |
|
177 #if defined(XP_WIN) |
|
178 // Windows does not follow POSIX. Updates to the TZ environment variable |
|
179 // are not reflected immediately on that platform as they are on UNIX |
|
180 // systems without this call. |
|
181 _tzset(); |
|
182 #endif |
|
183 |
|
184 struct tm tm; |
|
185 if (!ComputeLocalTime(static_cast<time_t>(utcSeconds), &tm)) |
|
186 return 0; |
|
187 |
|
188 int32_t dayoff = int32_t((utcSeconds + utcToLocalStandardOffsetSeconds) % SecondsPerDay); |
|
189 int32_t tmoff = tm.tm_sec + (tm.tm_min * SecondsPerMinute) + (tm.tm_hour * SecondsPerHour); |
|
190 |
|
191 int32_t diff = tmoff - dayoff; |
|
192 |
|
193 if (diff < 0) |
|
194 diff += SecondsPerDay; |
|
195 |
|
196 return diff * msPerSecond; |
|
197 } |
|
198 |
|
199 int64_t |
|
200 js::DateTimeInfo::getDSTOffsetMilliseconds(int64_t utcMilliseconds) |
|
201 { |
|
202 sanityCheck(); |
|
203 |
|
204 int64_t utcSeconds = utcMilliseconds / msPerSecond; |
|
205 |
|
206 if (utcSeconds > MaxUnixTimeT) { |
|
207 utcSeconds = MaxUnixTimeT; |
|
208 } else if (utcSeconds < 0) { |
|
209 /* Go ahead a day to make localtime work (does not work with 0). */ |
|
210 utcSeconds = SecondsPerDay; |
|
211 } |
|
212 |
|
213 /* |
|
214 * NB: Be aware of the initial range values when making changes to this |
|
215 * code: the first call to this method, with those initial range |
|
216 * values, must result in a cache miss. |
|
217 */ |
|
218 |
|
219 if (rangeStartSeconds <= utcSeconds && utcSeconds <= rangeEndSeconds) |
|
220 return offsetMilliseconds; |
|
221 |
|
222 if (oldRangeStartSeconds <= utcSeconds && utcSeconds <= oldRangeEndSeconds) |
|
223 return oldOffsetMilliseconds; |
|
224 |
|
225 oldOffsetMilliseconds = offsetMilliseconds; |
|
226 oldRangeStartSeconds = rangeStartSeconds; |
|
227 oldRangeEndSeconds = rangeEndSeconds; |
|
228 |
|
229 if (rangeStartSeconds <= utcSeconds) { |
|
230 int64_t newEndSeconds = Min(rangeEndSeconds + RangeExpansionAmount, MaxUnixTimeT); |
|
231 if (newEndSeconds >= utcSeconds) { |
|
232 int64_t endOffsetMilliseconds = computeDSTOffsetMilliseconds(newEndSeconds); |
|
233 if (endOffsetMilliseconds == offsetMilliseconds) { |
|
234 rangeEndSeconds = newEndSeconds; |
|
235 return offsetMilliseconds; |
|
236 } |
|
237 |
|
238 offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); |
|
239 if (offsetMilliseconds == endOffsetMilliseconds) { |
|
240 rangeStartSeconds = utcSeconds; |
|
241 rangeEndSeconds = newEndSeconds; |
|
242 } else { |
|
243 rangeEndSeconds = utcSeconds; |
|
244 } |
|
245 return offsetMilliseconds; |
|
246 } |
|
247 |
|
248 offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); |
|
249 rangeStartSeconds = rangeEndSeconds = utcSeconds; |
|
250 return offsetMilliseconds; |
|
251 } |
|
252 |
|
253 int64_t newStartSeconds = Max<int64_t>(rangeStartSeconds - RangeExpansionAmount, 0); |
|
254 if (newStartSeconds <= utcSeconds) { |
|
255 int64_t startOffsetMilliseconds = computeDSTOffsetMilliseconds(newStartSeconds); |
|
256 if (startOffsetMilliseconds == offsetMilliseconds) { |
|
257 rangeStartSeconds = newStartSeconds; |
|
258 return offsetMilliseconds; |
|
259 } |
|
260 |
|
261 offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); |
|
262 if (offsetMilliseconds == startOffsetMilliseconds) { |
|
263 rangeStartSeconds = newStartSeconds; |
|
264 rangeEndSeconds = utcSeconds; |
|
265 } else { |
|
266 rangeStartSeconds = utcSeconds; |
|
267 } |
|
268 return offsetMilliseconds; |
|
269 } |
|
270 |
|
271 rangeStartSeconds = rangeEndSeconds = utcSeconds; |
|
272 offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); |
|
273 return offsetMilliseconds; |
|
274 } |
|
275 |
|
276 void |
|
277 js::DateTimeInfo::sanityCheck() |
|
278 { |
|
279 MOZ_ASSERT(rangeStartSeconds <= rangeEndSeconds); |
|
280 MOZ_ASSERT_IF(rangeStartSeconds == INT64_MIN, rangeEndSeconds == INT64_MIN); |
|
281 MOZ_ASSERT_IF(rangeEndSeconds == INT64_MIN, rangeStartSeconds == INT64_MIN); |
|
282 MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN, |
|
283 rangeStartSeconds >= 0 && rangeEndSeconds >= 0); |
|
284 MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN, |
|
285 rangeStartSeconds <= MaxUnixTimeT && rangeEndSeconds <= MaxUnixTimeT); |
|
286 } |