Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtimezoneprivate_mac.mm
Go to the documentation of this file.
1// Copyright (C) 2019 The Qt Company Ltd.
2// Copyright (C) 2013 John Layt <jlayt@kde.org>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:significant reason:default
5
6#include "qtimezone.h"
8
9#include "private/qcore_mac_p.h"
10#include "qstringlist.h"
11
12#include <Foundation/NSTimeZone.h>
13
14#include <qdebug.h>
15
16#include <algorithm>
17
18QT_BEGIN_NAMESPACE
19
20/*
21 Private
22
23 Darwin system implementation
24 https://developer.apple.com/documentation/foundation/nstimezone
25*/
26
27// Create the system default time zone
28QMacTimeZonePrivate::QMacTimeZonePrivate()
29{
30 QMacAutoReleasePool pool;
31 // Reset the cached system tz then instantiate it:
32 [NSTimeZone resetSystemTimeZone];
33 m_nstz = [NSTimeZone.systemTimeZone retain];
34 Q_ASSERT(m_nstz);
35 m_id = QString::fromNSString(m_nstz.name).toUtf8();
36}
37
38// Create a named time zone
39QMacTimeZonePrivate::QMacTimeZonePrivate(const QByteArray &ianaId)
40 : m_nstz(nil)
41{
42 init(ianaId);
43}
44
45QMacTimeZonePrivate::QMacTimeZonePrivate(const QMacTimeZonePrivate &other)
46 : QTimeZonePrivate(other), m_nstz([other.m_nstz copy])
47{
48}
49
50QMacTimeZonePrivate::~QMacTimeZonePrivate()
51{
52 [m_nstz release];
53}
54
55QMacTimeZonePrivate *QMacTimeZonePrivate::clone() const
56{
57 return new QMacTimeZonePrivate(*this);
58}
59
60void QMacTimeZonePrivate::init(const QByteArray &ianaId)
61{
62 QMacAutoReleasePool pool;
63
64 m_nstz = [[NSTimeZone timeZoneWithName:QString::fromUtf8(ianaId).toNSString()] retain];
65 if (m_nstz) {
66 m_id = ianaId;
67 } else {
68 // macOS has been seen returning a systemTimeZone which reports its name
69 // as Asia/Kolkata, which doesn't appear in knownTimeZoneNames (which
70 // calls the zone Asia/Calcutta). So explicitly check for the name
71 // systemTimeZoneId() returns, and use systemTimeZone if we get it:
72 m_nstz = [NSTimeZone.systemTimeZone retain];
73 Q_ASSERT(m_nstz);
74 if (QString::fromNSString(m_nstz.name).toUtf8() == ianaId)
75 m_id = ianaId;
76 }
77}
78
79QString QMacTimeZonePrivate::comment() const
80{
81 return QString::fromNSString(m_nstz.description);
82}
83
84QString QMacTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
85 QTimeZone::NameType nameType,
86 const QLocale &locale) const
87{
88 // TODO Mac doesn't support OffsetName yet so use standard offset name
89 if (nameType == QTimeZone::OffsetName) {
90 const Data nowData = data(QDateTime::currentMSecsSinceEpoch());
91 // TODO Cheat for now, assume if has dst the offset if 1 hour
92 if (timeType == QTimeZone::DaylightTime && hasDaylightTime())
93 return isoOffsetFormat(nowData.standardTimeOffset + 3600);
94 else
95 return isoOffsetFormat(nowData.standardTimeOffset);
96 }
97
98 NSTimeZoneNameStyle style = NSTimeZoneNameStyleStandard;
99
100 switch (nameType) {
101 case QTimeZone::ShortName :
102 if (timeType == QTimeZone::DaylightTime)
103 style = NSTimeZoneNameStyleShortDaylightSaving;
104 else if (timeType == QTimeZone::GenericTime)
105 style = NSTimeZoneNameStyleShortGeneric;
106 else
107 style = NSTimeZoneNameStyleShortStandard;
108 break;
109 case QTimeZone::DefaultName :
110 case QTimeZone::LongName :
111 if (timeType == QTimeZone::DaylightTime)
112 style = NSTimeZoneNameStyleDaylightSaving;
113 else if (timeType == QTimeZone::GenericTime)
114 style = NSTimeZoneNameStyleGeneric;
115 else
116 style = NSTimeZoneNameStyleStandard;
117 break;
118 case QTimeZone::OffsetName :
119 Q_UNREACHABLE();
120 break;
121 }
122
123 NSString *macLocaleCode = locale.name().toNSString();
124 NSLocale *macLocale = [[NSLocale alloc] initWithLocaleIdentifier:macLocaleCode];
125 const QString result = QString::fromNSString([m_nstz localizedName:style locale:macLocale]);
126 [macLocale release];
127 return result;
128}
129
130QString QMacTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
131{
132 const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
133 return QString::fromNSString([m_nstz abbreviationForDate:[NSDate dateWithTimeIntervalSince1970:seconds]]);
134}
135
136int QMacTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
137{
138 const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
139 return [m_nstz secondsFromGMTForDate:[NSDate dateWithTimeIntervalSince1970:seconds]];
140}
141
142int QMacTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
143{
144 return offsetFromUtc(atMSecsSinceEpoch) - daylightTimeOffset(atMSecsSinceEpoch);
145}
146
147int QMacTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
148{
149 const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
150 return [m_nstz daylightSavingTimeOffsetForDate:[NSDate dateWithTimeIntervalSince1970:seconds]];
151}
152
153bool QMacTimeZonePrivate::hasDaylightTime() const
154{
155 // TODO Scan transitions for one after which isDaylightSavingTimeForDate is true.
156 // TODO No direct Mac API, so return if has next after 1970, i.e. since start of tz
157 // TODO Not sure what is returned in event of no transitions, assume will be before requested date
158 NSDate *epoch = [NSDate dateWithTimeIntervalSince1970:0];
159 const NSDate *date = [m_nstz nextDaylightSavingTimeTransitionAfterDate:epoch];
160 const bool result = (date.timeIntervalSince1970 > epoch.timeIntervalSince1970);
161 return result;
162}
163
164bool QMacTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
165{
166 const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
167 return [m_nstz isDaylightSavingTimeForDate:[NSDate dateWithTimeIntervalSince1970:seconds]];
168}
169
170QTimeZonePrivate::Data QMacTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
171{
172 QMacAutoReleasePool pool;
173 const NSTimeInterval seconds = forMSecsSinceEpoch / 1000.0;
174 NSDate *date = [NSDate dateWithTimeIntervalSince1970:seconds];
175 Data data;
176 data.atMSecsSinceEpoch = forMSecsSinceEpoch;
177 data.offsetFromUtc = [m_nstz secondsFromGMTForDate:date];
178 data.daylightTimeOffset = [m_nstz daylightSavingTimeOffsetForDate:date];
179 data.standardTimeOffset = data.offsetFromUtc - data.daylightTimeOffset;
180 data.abbreviation = QString::fromNSString([m_nstz abbreviationForDate:date]);
181 return data;
182}
183
184bool QMacTimeZonePrivate::hasTransitions() const
185{
186 // NB: this is about the backend capability, not the particular zone.
187 return true; // albeit with caveats, see hasDaylightTime()
188}
189
190QTimeZonePrivate::Data QMacTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
191{
192 Data tran;
193 const NSTimeInterval seconds = afterMSecsSinceEpoch / 1000.0;
194 NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:seconds];
195 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
196 const NSTimeInterval nextSecs = nextDate.timeIntervalSince1970;
197 if (nextDate == nil || nextSecs <= seconds) {
198 [nextDate release];
199 return {};
200 }
201 tran.atMSecsSinceEpoch = nextSecs * 1000;
202 tran.offsetFromUtc = [m_nstz secondsFromGMTForDate:nextDate];
203 tran.daylightTimeOffset = [m_nstz daylightSavingTimeOffsetForDate:nextDate];
204 tran.standardTimeOffset = tran.offsetFromUtc - tran.daylightTimeOffset;
205 tran.abbreviation = QString::fromNSString([m_nstz abbreviationForDate:nextDate]);
206 return tran;
207}
208
209QTimeZonePrivate::Data QMacTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
210{
211 // The native API only lets us search forward, so we need to find an early-enough start:
212 constexpr NSTimeInterval lowerBound = std::numeric_limits<NSTimeInterval>::lowest();
213 const qint64 endSecs = beforeMSecsSinceEpoch / 1000;
214 const int year = 366 * 24 * 3600; // a (long) year, in seconds
215 NSTimeInterval prevSecs = endSecs; // sentinel for later check
216 NSTimeInterval nextSecs = prevSecs - year;
217 NSTimeInterval tranSecs = lowerBound; // time at a transition; may be > endSecs
218
219 NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:nextSecs];
220 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
221 if (nextDate != nil
222 && (tranSecs = nextDate.timeIntervalSince1970) < endSecs) {
223 // There's a transition within the last year before endSecs:
224 nextSecs = tranSecs;
225 } else {
226 // Need to start our search earlier:
227 nextDate = [NSDate dateWithTimeIntervalSince1970:lowerBound];
228 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
229 if (nextDate != nil) {
230 NSTimeInterval lateSecs = nextSecs;
231 nextSecs = nextDate.timeIntervalSince1970;
232 Q_ASSERT(nextSecs <= endSecs - year || nextSecs == tranSecs);
233 /*
234 We're looking at the first ever transition for our zone, at
235 nextSecs (and our zone *does* have at least one transition). If
236 it's later than endSecs - year, then we must have found it on the
237 initial check and therefore set tranSecs to the same transition
238 time (which, we can infer here, is >= endSecs). In this case, we
239 won't enter the binary-chop loop, below.
240
241 In the loop, nextSecs < lateSecs < endSecs: we have a transition
242 at nextSecs and there is no transition between lateSecs and
243 endSecs. The loop narrows the interval between nextSecs and
244 lateSecs by looking for a transition after their mid-point; if it
245 finds one < endSecs, nextSecs moves to this transition; otherwise,
246 lateSecs moves to the mid-point. This soon enough narrows the gap
247 to within a year, after which walking forward one transition at a
248 time (the "Wind through" loop, below) is good enough.
249 */
250
251 // Binary chop to within a year of last transition before endSecs:
252 while (nextSecs + year < lateSecs) {
253 // Careful about overflow, not fussy about rounding errors:
254 NSTimeInterval middle = nextSecs / 2 + lateSecs / 2;
255 NSDate *split = [NSDate dateWithTimeIntervalSince1970:middle];
256 split = [m_nstz nextDaylightSavingTimeTransitionAfterDate:split];
257 if (split != nil && (tranSecs = split.timeIntervalSince1970) < endSecs) {
258 nextDate = split;
259 nextSecs = tranSecs;
260 } else {
261 lateSecs = middle;
262 }
263 }
264 Q_ASSERT(nextDate != nil);
265 // ... and nextSecs < endSecs unless first transition ever was >= endSecs.
266 } // else: we have no data - prevSecs is still endSecs, nextDate is still nil
267 }
268 // Either nextDate is nil or nextSecs is at its transition.
269
270 // Wind through remaining transitions (spanning at most a year), one at a time:
271 while (nextDate != nil && nextSecs < endSecs) {
272 prevSecs = nextSecs;
273 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
274 nextSecs = nextDate.timeIntervalSince1970;
275 if (nextSecs <= prevSecs) // presumably no later data available
276 break;
277 }
278 if (prevSecs < endSecs) // i.e. we did make it into that while loop
279 return data(qint64(prevSecs * 1e3));
280
281 // No transition data; or first transition later than requested time.
282 return {};
283}
284
285QByteArray QMacTimeZonePrivate::systemTimeZoneId() const
286{
287 QMacAutoReleasePool pool;
288
289 // Reset the cached system tz then return the name
290 [NSTimeZone resetSystemTimeZone];
291 Q_ASSERT(NSTimeZone.systemTimeZone);
292 return QString::fromNSString(NSTimeZone.systemTimeZone.name).toUtf8();
293}
294
295bool QMacTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
296{
297 QMacAutoReleasePool pool;
298 return [NSTimeZone timeZoneWithName:QString::fromUtf8(ianaId).toNSString()] != nil;
299}
300
301QList<QByteArray> QMacTimeZonePrivate::availableTimeZoneIds() const
302{
303 NSEnumerator *enumerator = NSTimeZone.knownTimeZoneNames.objectEnumerator;
304 QByteArray tzid = QString::fromNSString(enumerator.nextObject).toUtf8();
305
306 QList<QByteArray> list;
307 while (!tzid.isEmpty()) {
308 list << tzid;
309 tzid = QString::fromNSString(enumerator.nextObject).toUtf8();
310 }
311
312 std::sort(list.begin(), list.end());
313 list.erase(std::unique(list.begin(), list.end()), list.end());
314
315 return list;
316}
317
318NSTimeZone *QMacTimeZonePrivate::nsTimeZone() const
319{
320 return m_nstz;
321}
322
323QT_END_NAMESPACE