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 return hasTransitions();
156}
157
158bool QMacTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
159{
160 const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
161 return [m_nstz isDaylightSavingTimeForDate:[NSDate dateWithTimeIntervalSince1970:seconds]];
162}
163
164QTimeZonePrivate::Data QMacTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
165{
166 QMacAutoReleasePool pool;
167 const NSTimeInterval seconds = forMSecsSinceEpoch / 1000.0;
168 NSDate *date = [NSDate dateWithTimeIntervalSince1970:seconds];
169 Data data;
170 data.atMSecsSinceEpoch = forMSecsSinceEpoch;
171 data.offsetFromUtc = [m_nstz secondsFromGMTForDate:date];
172 data.daylightTimeOffset = [m_nstz daylightSavingTimeOffsetForDate:date];
173 data.standardTimeOffset = data.offsetFromUtc - data.daylightTimeOffset;
174 data.abbreviation = QString::fromNSString([m_nstz abbreviationForDate:date]);
175 return data;
176}
177
178bool QMacTimeZonePrivate::hasTransitions() const
179{
180 // TODO No direct Mac API, so return if has next after 1970, i.e. since start of tz
181 // TODO Not sure what is returned in event of no transitions, assume will be before requested date
182 NSDate *epoch = [NSDate dateWithTimeIntervalSince1970:0];
183 const NSDate *date = [m_nstz nextDaylightSavingTimeTransitionAfterDate:epoch];
184 const bool result = (date.timeIntervalSince1970 > epoch.timeIntervalSince1970);
185 return result;
186}
187
188QTimeZonePrivate::Data QMacTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
189{
190 Data tran;
191 const NSTimeInterval seconds = afterMSecsSinceEpoch / 1000.0;
192 NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:seconds];
193 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
194 const NSTimeInterval nextSecs = nextDate.timeIntervalSince1970;
195 if (nextDate == nil || nextSecs <= seconds) {
196 [nextDate release];
197 return {};
198 }
199 tran.atMSecsSinceEpoch = nextSecs * 1000;
200 tran.offsetFromUtc = [m_nstz secondsFromGMTForDate:nextDate];
201 tran.daylightTimeOffset = [m_nstz daylightSavingTimeOffsetForDate:nextDate];
202 tran.standardTimeOffset = tran.offsetFromUtc - tran.daylightTimeOffset;
203 tran.abbreviation = QString::fromNSString([m_nstz abbreviationForDate:nextDate]);
204 return tran;
205}
206
207QTimeZonePrivate::Data QMacTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
208{
209 // The native API only lets us search forward, so we need to find an early-enough start:
210 constexpr NSTimeInterval lowerBound = std::numeric_limits<NSTimeInterval>::lowest();
211 const qint64 endSecs = beforeMSecsSinceEpoch / 1000;
212 const int year = 366 * 24 * 3600; // a (long) year, in seconds
213 NSTimeInterval prevSecs = endSecs; // sentinel for later check
214 NSTimeInterval nextSecs = prevSecs - year;
215 NSTimeInterval tranSecs = lowerBound; // time at a transition; may be > endSecs
216
217 NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:nextSecs];
218 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
219 if (nextDate != nil
220 && (tranSecs = nextDate.timeIntervalSince1970) < endSecs) {
221 // There's a transition within the last year before endSecs:
222 nextSecs = tranSecs;
223 } else {
224 // Need to start our search earlier:
225 nextDate = [NSDate dateWithTimeIntervalSince1970:lowerBound];
226 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
227 if (nextDate != nil) {
228 NSTimeInterval lateSecs = nextSecs;
229 nextSecs = nextDate.timeIntervalSince1970;
230 Q_ASSERT(nextSecs <= endSecs - year || nextSecs == tranSecs);
231 /*
232 We're looking at the first ever transition for our zone, at
233 nextSecs (and our zone *does* have at least one transition). If
234 it's later than endSecs - year, then we must have found it on the
235 initial check and therefore set tranSecs to the same transition
236 time (which, we can infer here, is >= endSecs). In this case, we
237 won't enter the binary-chop loop, below.
238
239 In the loop, nextSecs < lateSecs < endSecs: we have a transition
240 at nextSecs and there is no transition between lateSecs and
241 endSecs. The loop narrows the interval between nextSecs and
242 lateSecs by looking for a transition after their mid-point; if it
243 finds one < endSecs, nextSecs moves to this transition; otherwise,
244 lateSecs moves to the mid-point. This soon enough narrows the gap
245 to within a year, after which walking forward one transition at a
246 time (the "Wind through" loop, below) is good enough.
247 */
248
249 // Binary chop to within a year of last transition before endSecs:
250 while (nextSecs + year < lateSecs) {
251 // Careful about overflow, not fussy about rounding errors:
252 NSTimeInterval middle = nextSecs / 2 + lateSecs / 2;
253 NSDate *split = [NSDate dateWithTimeIntervalSince1970:middle];
254 split = [m_nstz nextDaylightSavingTimeTransitionAfterDate:split];
255 if (split != nil && (tranSecs = split.timeIntervalSince1970) < endSecs) {
256 nextDate = split;
257 nextSecs = tranSecs;
258 } else {
259 lateSecs = middle;
260 }
261 }
262 Q_ASSERT(nextDate != nil);
263 // ... and nextSecs < endSecs unless first transition ever was >= endSecs.
264 } // else: we have no data - prevSecs is still endSecs, nextDate is still nil
265 }
266 // Either nextDate is nil or nextSecs is at its transition.
267
268 // Wind through remaining transitions (spanning at most a year), one at a time:
269 while (nextDate != nil && nextSecs < endSecs) {
270 prevSecs = nextSecs;
271 nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
272 nextSecs = nextDate.timeIntervalSince1970;
273 if (nextSecs <= prevSecs) // presumably no later data available
274 break;
275 }
276 if (prevSecs < endSecs) // i.e. we did make it into that while loop
277 return data(qint64(prevSecs * 1e3));
278
279 // No transition data; or first transition later than requested time.
280 return {};
281}
282
283QByteArray QMacTimeZonePrivate::systemTimeZoneId() const
284{
285 QMacAutoReleasePool pool;
286
287 // Reset the cached system tz then return the name
288 [NSTimeZone resetSystemTimeZone];
289 Q_ASSERT(NSTimeZone.systemTimeZone);
290 return QString::fromNSString(NSTimeZone.systemTimeZone.name).toUtf8();
291}
292
293bool QMacTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray& ianaId) const
294{
295 QMacAutoReleasePool pool;
296 return [NSTimeZone timeZoneWithName:QString::fromUtf8(ianaId).toNSString()] != nil;
297}
298
299QList<QByteArray> QMacTimeZonePrivate::availableTimeZoneIds() const
300{
301 NSEnumerator *enumerator = NSTimeZone.knownTimeZoneNames.objectEnumerator;
302 QByteArray tzid = QString::fromNSString(enumerator.nextObject).toUtf8();
303
304 QList<QByteArray> list;
305 while (!tzid.isEmpty()) {
306 list << tzid;
307 tzid = QString::fromNSString(enumerator.nextObject).toUtf8();
308 }
309
310 std::sort(list.begin(), list.end());
311 list.erase(std::unique(list.begin(), list.end()), list.end());
312
313 return list;
314}
315
316NSTimeZone *QMacTimeZonePrivate::nsTimeZone() const
317{
318 return m_nstz;
319}
320
321QT_END_NAMESPACE