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