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