blob: 1fb48a31d3120d8b0736377947818740cc6c5f9d [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2013 John Layt <jlayt@kde.org>
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qtimezone.h"
#include "qtimezoneprivate_p.h"
#include "private/qcore_mac_p.h"
#include "qstringlist.h"
#include <Foundation/NSTimeZone.h>
#include <qdebug.h>
#include <algorithm>
QT_BEGIN_NAMESPACE
/*
Private
OS X system implementation
*/
// Create the system default time zone
QMacTimeZonePrivate::QMacTimeZonePrivate()
{
// Reset the cached system tz then instantiate it:
[NSTimeZone resetSystemTimeZone];
m_nstz = [NSTimeZone.systemTimeZone retain];
Q_ASSERT(m_nstz);
m_id = QString::fromNSString(m_nstz.name).toUtf8();
}
// Create a named time zone
QMacTimeZonePrivate::QMacTimeZonePrivate(const QByteArray &ianaId)
: m_nstz(nil)
{
init(ianaId);
}
QMacTimeZonePrivate::QMacTimeZonePrivate(const QMacTimeZonePrivate &other)
: QTimeZonePrivate(other), m_nstz([other.m_nstz copy])
{
}
QMacTimeZonePrivate::~QMacTimeZonePrivate()
{
[m_nstz release];
}
QMacTimeZonePrivate *QMacTimeZonePrivate::clone() const
{
return new QMacTimeZonePrivate(*this);
}
void QMacTimeZonePrivate::init(const QByteArray &ianaId)
{
if (availableTimeZoneIds().contains(ianaId)) {
m_nstz = [[NSTimeZone timeZoneWithName:QString::fromUtf8(ianaId).toNSString()] retain];
if (m_nstz)
m_id = ianaId;
}
if (!m_nstz) {
// macOS has been seen returning a systemTimeZone which reports its name
// as Asia/Kolkata, which doesn't appear in knownTimeZoneNames (which
// calls the zone Asia/Calcutta). So explicitly check for the name
// systemTimeZoneId() returns, and use systemTimeZone if we get it:
m_nstz = [NSTimeZone.systemTimeZone retain];
Q_ASSERT(m_nstz);
if (QString::fromNSString(m_nstz.name).toUtf8() == ianaId)
m_id = ianaId;
}
}
QString QMacTimeZonePrivate::comment() const
{
return QString::fromNSString(m_nstz.description);
}
QString QMacTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QLocale &locale) const
{
// TODO Mac doesn't support OffsetName yet so use standard offset name
if (nameType == QTimeZone::OffsetName) {
const Data nowData = data(QDateTime::currentMSecsSinceEpoch());
// TODO Cheat for now, assume if has dst the offset if 1 hour
if (timeType == QTimeZone::DaylightTime && hasDaylightTime())
return isoOffsetFormat(nowData.standardTimeOffset + 3600);
else
return isoOffsetFormat(nowData.standardTimeOffset);
}
NSTimeZoneNameStyle style = NSTimeZoneNameStyleStandard;
switch (nameType) {
case QTimeZone::ShortName :
if (timeType == QTimeZone::DaylightTime)
style = NSTimeZoneNameStyleShortDaylightSaving;
else if (timeType == QTimeZone::GenericTime)
style = NSTimeZoneNameStyleShortGeneric;
else
style = NSTimeZoneNameStyleShortStandard;
break;
case QTimeZone::DefaultName :
case QTimeZone::LongName :
if (timeType == QTimeZone::DaylightTime)
style = NSTimeZoneNameStyleDaylightSaving;
else if (timeType == QTimeZone::GenericTime)
style = NSTimeZoneNameStyleGeneric;
else
style = NSTimeZoneNameStyleStandard;
break;
case QTimeZone::OffsetName :
// Unreachable
break;
}
NSString *macLocaleCode = locale.name().toNSString();
NSLocale *macLocale = [[NSLocale alloc] initWithLocaleIdentifier:macLocaleCode];
const QString result = QString::fromNSString([m_nstz localizedName:style locale:macLocale]);
[macLocale release];
return result;
}
QString QMacTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
{
const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
return QString::fromNSString([m_nstz abbreviationForDate:[NSDate dateWithTimeIntervalSince1970:seconds]]);
}
int QMacTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
{
const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
return [m_nstz secondsFromGMTForDate:[NSDate dateWithTimeIntervalSince1970:seconds]];
}
int QMacTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
{
return offsetFromUtc(atMSecsSinceEpoch) - daylightTimeOffset(atMSecsSinceEpoch);
}
int QMacTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
{
const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
return [m_nstz daylightSavingTimeOffsetForDate:[NSDate dateWithTimeIntervalSince1970:seconds]];
}
bool QMacTimeZonePrivate::hasDaylightTime() const
{
// TODO No Mac API, assume if has transitions
return hasTransitions();
}
bool QMacTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
{
const NSTimeInterval seconds = atMSecsSinceEpoch / 1000.0;
return [m_nstz isDaylightSavingTimeForDate:[NSDate dateWithTimeIntervalSince1970:seconds]];
}
QTimeZonePrivate::Data QMacTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
{
const NSTimeInterval seconds = forMSecsSinceEpoch / 1000.0;
NSDate *date = [NSDate dateWithTimeIntervalSince1970:seconds];
Data data;
data.atMSecsSinceEpoch = forMSecsSinceEpoch;
data.offsetFromUtc = [m_nstz secondsFromGMTForDate:date];
data.daylightTimeOffset = [m_nstz daylightSavingTimeOffsetForDate:date];
data.standardTimeOffset = data.offsetFromUtc - data.daylightTimeOffset;
data.abbreviation = QString::fromNSString([m_nstz abbreviationForDate:date]);
return data;
}
bool QMacTimeZonePrivate::hasTransitions() const
{
// TODO No direct Mac API, so return if has next after 1970, i.e. since start of tz
// TODO Not sure what is returned in event of no transitions, assume will be before requested date
NSDate *epoch = [NSDate dateWithTimeIntervalSince1970:0];
const NSDate *date = [m_nstz nextDaylightSavingTimeTransitionAfterDate:epoch];
const bool result = (date.timeIntervalSince1970 > epoch.timeIntervalSince1970);
return result;
}
QTimeZonePrivate::Data QMacTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
{
QTimeZonePrivate::Data tran;
const NSTimeInterval seconds = afterMSecsSinceEpoch / 1000.0;
NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:seconds];
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
const NSTimeInterval nextSecs = nextDate.timeIntervalSince1970;
if (nextDate == nil || nextSecs <= seconds) {
[nextDate release];
return invalidData();
}
tran.atMSecsSinceEpoch = nextSecs * 1000;
tran.offsetFromUtc = [m_nstz secondsFromGMTForDate:nextDate];
tran.daylightTimeOffset = [m_nstz daylightSavingTimeOffsetForDate:nextDate];
tran.standardTimeOffset = tran.offsetFromUtc - tran.daylightTimeOffset;
tran.abbreviation = QString::fromNSString([m_nstz abbreviationForDate:nextDate]);
return tran;
}
QTimeZonePrivate::Data QMacTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
{
// The native API only lets us search forward, so we need to find an early-enough start:
const NSTimeInterval lowerBound = std::numeric_limits<NSTimeInterval>::lowest();
const qint64 endSecs = beforeMSecsSinceEpoch / 1000;
const int year = 366 * 24 * 3600; // a (long) year, in seconds
NSTimeInterval prevSecs = endSecs; // sentinel for later check
NSTimeInterval nextSecs = prevSecs - year;
NSTimeInterval tranSecs = lowerBound; // time at a transition; may be > endSecs
NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:nextSecs];
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
if (nextDate != nil
&& (tranSecs = nextDate.timeIntervalSince1970) < endSecs) {
// There's a transition within the last year before endSecs:
nextSecs = tranSecs;
} else {
// Need to start our search earlier:
nextDate = [NSDate dateWithTimeIntervalSince1970:lowerBound];
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
if (nextDate != nil) {
NSTimeInterval lateSecs = nextSecs;
nextSecs = nextDate.timeIntervalSince1970;
Q_ASSERT(nextSecs <= endSecs - year || nextSecs == tranSecs);
/*
We're looking at the first ever transition for our zone, at
nextSecs (and our zone *does* have at least one transition). If
it's later than endSecs - year, then we must have found it on the
initial check and therefore set tranSecs to the same transition
time (which, we can infer here, is >= endSecs). In this case, we
won't enter the binary-chop loop, below.
In the loop, nextSecs < lateSecs < endSecs: we have a transition
at nextSecs and there is no transition between lateSecs and
endSecs. The loop narrows the interval between nextSecs and
lateSecs by looking for a transition after their mid-point; if it
finds one < endSecs, nextSecs moves to this transition; otherwise,
lateSecs moves to the mid-point. This soon enough narrows the gap
to within a year, after which walking forward one transition at a
time (the "Wind through" loop, below) is good enough.
*/
// Binary chop to within a year of last transition before endSecs:
while (nextSecs + year < lateSecs) {
// Careful about overflow, not fussy about rounding errors:
NSTimeInterval middle = nextSecs / 2 + lateSecs / 2;
NSDate *split = [NSDate dateWithTimeIntervalSince1970:middle];
split = [m_nstz nextDaylightSavingTimeTransitionAfterDate:split];
if (split != nil && (tranSecs = split.timeIntervalSince1970) < endSecs) {
nextDate = split;
nextSecs = tranSecs;
} else {
lateSecs = middle;
}
}
Q_ASSERT(nextDate != nil);
// ... and nextSecs < endSecs unless first transition ever was >= endSecs.
} // else: we have no data - prevSecs is still endSecs, nextDate is still nil
}
// Either nextDate is nil or nextSecs is at its transition.
// Wind through remaining transitions (spanning at most a year), one at a time:
while (nextDate != nil && nextSecs < endSecs) {
prevSecs = nextSecs;
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
nextSecs = nextDate.timeIntervalSince1970;
if (nextSecs <= prevSecs) // presumably no later data available
break;
}
if (prevSecs < endSecs) // i.e. we did make it into that while loop
return data(qint64(prevSecs * 1e3));
// No transition data; or first transition later than requested time.
return invalidData();
}
QByteArray QMacTimeZonePrivate::systemTimeZoneId() const
{
// Reset the cached system tz then return the name
[NSTimeZone resetSystemTimeZone];
Q_ASSERT(NSTimeZone.systemTimeZone);
return QString::fromNSString(NSTimeZone.systemTimeZone.name).toUtf8();
}
QList<QByteArray> QMacTimeZonePrivate::availableTimeZoneIds() const
{
NSEnumerator *enumerator = NSTimeZone.knownTimeZoneNames.objectEnumerator;
QByteArray tzid = QString::fromNSString(enumerator.nextObject).toUtf8();
QList<QByteArray> list;
while (!tzid.isEmpty()) {
list << tzid;
tzid = QString::fromNSString(enumerator.nextObject).toUtf8();
}
std::sort(list.begin(), list.end());
list.erase(std::unique(list.begin(), list.end()), list.end());
return list;
}
NSTimeZone *QMacTimeZonePrivate::nsTimeZone() const
{
return m_nstz;
}
QT_END_NAMESPACE