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