KCalendarCore

icaltimezones.cpp
1/*
2 This file is part of the kcalcore library.
3
4 SPDX-FileCopyrightText: 2005-2007 David Jarvie <djarvie@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "icalformat.h"
10#include "icalformat_p.h"
11#include "icaltimezones_p.h"
12#include "recurrence.h"
13#include "recurrencehelper_p.h"
14#include "recurrencerule.h"
15
16#include "kcalendarcore_debug.h"
17
18#include <QByteArray>
19#include <QDateTime>
20#include <QTimeZone>
21
22extern "C" {
23#include <libical/ical.h>
24#include <libical/icaltimezone.h>
25}
26
27using namespace KCalendarCore;
28
29// Minimum repetition counts for VTIMEZONE RRULEs
30static const int minRuleCount = 5; // for any RRULE
31static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component
32
33// Convert an ical time to QDateTime, preserving the UTC indicator
34static QDateTime toQDateTime(const icaltimetype &t)
35{
36 return QDateTime(QDate(t.year, t.month, t.day), QTime(t.hour, t.minute, t.second), (icaltime_is_utc(t) ? QTimeZone::UTC : QTimeZone::LocalTime));
37}
38
39// Maximum date for time zone data.
40// It's not sensible to try to predict them very far in advance, because
41// they can easily change. Plus, it limits the processing required.
42static QDateTime MAX_DATE()
43{
44 static QDateTime dt;
45 if (!dt.isValid()) {
46 dt = QDateTime(QDate::currentDate().addYears(20), QTime(0, 0, 0));
47 }
48 return dt;
49}
50
51static icaltimetype writeLocalICalDateTime(const QDateTime &utc, int offset)
52{
53 const QDateTime local = utc.addSecs(offset);
54 icaltimetype t = icaltime_null_time();
55 t.year = local.date().year();
56 t.month = local.date().month();
57 t.day = local.date().day();
58 t.hour = local.time().hour();
59 t.minute = local.time().minute();
60 t.second = local.time().second();
61 t.is_date = 0;
62 t.zone = nullptr;
63 return t;
64}
65
66namespace KCalendarCore
67{
68void ICalTimeZonePhase::dump()
69{
70 qDebug() << " ~~~ ICalTimeZonePhase ~~~";
71 qDebug() << " Abbreviations:" << abbrevs;
72 qDebug() << " UTC offset:" << utcOffset;
73 qDebug() << " Transitions:" << transitions;
74 qDebug() << " ~~~~~~~~~~~~~~~~~~~~~~~~~";
75}
76
77void ICalTimeZone::dump()
78{
79 qDebug() << "~~~ ICalTimeZone ~~~";
80 qDebug() << "ID:" << id;
81 qDebug() << "QZONE:" << qZone.id();
82 qDebug() << "STD:";
83 standard.dump();
84 qDebug() << "DST:";
85 daylight.dump();
86 qDebug() << "~~~~~~~~~~~~~~~~~~~~";
87}
88
89ICalTimeZoneCache::ICalTimeZoneCache()
90{
91}
92
93void ICalTimeZoneCache::insert(const QByteArray &id, const ICalTimeZone &tz)
94{
95 mCache.insert(id, tz);
96}
97
98namespace
99{
100template<typename T>
101typename T::const_iterator greatestSmallerThan(const T &c, const typename T::value_type &v)
102{
103 auto it = std::lower_bound(c.cbegin(), c.cend(), v);
104 if (it != c.cbegin()) {
105 return --it;
106 }
107 return c.cend();
108}
109
110}
111
112QTimeZone ICalTimeZoneCache::tzForTime(const QDateTime &dt, const QByteArray &tzid) const
113{
115 return QTimeZone(tzid);
116 }
117
118 const ICalTimeZone tz = mCache.value(tzid);
119 if (!tz.qZone.isValid()) {
120 return QTimeZone();
121 }
122
123 // If the matched timezone is one of the UTC offset timezones, we need to make
124 // sure it's in the correct DTS.
125 // The lookup in ICalTimeZoneParser will only find TZ in standard time, but
126 // if the datetim in question fits in the DTS zone, we need to use another UTC
127 // offset timezone
128 if (tz.qZone.id().startsWith("UTC")) { // krazy:exclude=strings
129 // Find the nearest standard and DST transitions that occur BEFORE the "dt"
130 const auto stdPrev = greatestSmallerThan(tz.standard.transitions, dt);
131 const auto dstPrev = greatestSmallerThan(tz.daylight.transitions, dt);
132 if (stdPrev != tz.standard.transitions.cend() && dstPrev != tz.daylight.transitions.cend()) {
133 if (*dstPrev > *stdPrev) {
134 // Previous DTS is closer to "dt" than previous standard, which
135 // means we are in DTS right now
136 const auto tzids = QTimeZone::availableTimeZoneIds(tz.daylight.utcOffset);
137 auto dtsTzId = std::find_if(tzids.cbegin(), tzids.cend(), [](const QByteArray &id) {
138 return id.startsWith("UTC"); // krazy:exclude=strings
139 });
140 if (dtsTzId != tzids.cend()) {
141 return QTimeZone(*dtsTzId);
142 }
143 }
144 }
145 }
146
147 return tz.qZone;
148}
149
150ICalTimeZoneParser::ICalTimeZoneParser(ICalTimeZoneCache *cache)
151 : mCache(cache)
152{
153}
154
155void ICalTimeZoneParser::updateTzEarliestDate(const IncidenceBase::Ptr &incidence, TimeZoneEarliestDate *earliest)
156{
158 const auto dt = incidence->dateTime(role);
159 if (dt.isValid()) {
160 if (dt.timeZone() == QTimeZone::utc()) {
161 continue;
162 }
163 const auto prev = earliest->value(incidence->dtStart().timeZone());
164 if (!prev.isValid() || incidence->dtStart() < prev) {
165 earliest->insert(incidence->dtStart().timeZone(), prev);
166 }
167 }
168 }
169}
170
171icalcomponent *ICalTimeZoneParser::icalcomponentFromQTimeZone(const QTimeZone &tz, const QDateTime &earliest)
172{
173 // VTIMEZONE RRULE types
174 enum {
175 DAY_OF_MONTH = 0x01,
176 WEEKDAY_OF_MONTH = 0x02,
177 LAST_WEEKDAY_OF_MONTH = 0x04,
178 };
179
180 // Write the time zone data into an iCal component
181 icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
182 icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.id().constData()));
183 // icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() ));
184
185 // Compile an ordered list of transitions so that we can know the phases
186 // which occur before and after each transition.
187 QTimeZone::OffsetDataList transits = tz.transitions(QDateTime(), MAX_DATE());
188 if (transits.isEmpty()) {
189 // If there is no way to compile a complete list of transitions
190 // transitions() can return an empty list
191 // In that case try get one transition to write a valid VTIMEZONE entry.
192 qCDebug(KCALCORE_LOG) << "No transition information available VTIMEZONE will be invalid.";
193 }
194 if (earliest.isValid()) {
195 // Remove all transitions earlier than those we are interested in
196 for (int i = 0, end = transits.count(); i < end; ++i) {
197 if (transits.at(i).atUtc >= earliest) {
198 if (i > 0) {
199 transits.erase(transits.begin(), transits.begin() + i);
200 }
201 break;
202 }
203 }
204 }
205 int trcount = transits.count();
206 QList<bool> transitionsDone(trcount, false);
207
208 // Go through the list of transitions and create an iCal component for each
209 // distinct combination of phase after and UTC offset before the transition.
210 icaldatetimeperiodtype dtperiod;
211 dtperiod.period = icalperiodtype_null_period();
212 for (;;) {
213 int i = 0;
214 for (; i < trcount && transitionsDone[i]; ++i) {
215 ;
216 }
217 if (i >= trcount) {
218 break;
219 }
220 // Found a phase combination which hasn't yet been processed
221 const int preOffset = (i > 0) ? transits.at(i - 1).offsetFromUtc : 0;
222 const auto &transit = transits.at(i);
223 if (transit.offsetFromUtc == preOffset) {
224 transitionsDone[i] = true;
225 while (++i < trcount) {
226 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
227 || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
228 continue;
229 }
230 transitionsDone[i] = true;
231 }
232 continue;
233 }
234 const bool isDst = transit.daylightTimeOffset > 0;
235 icalcomponent *phaseComp = icalcomponent_new(isDst ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT);
236 if (!transit.abbreviation.isEmpty()) {
237 icalcomponent_add_property(phaseComp, icalproperty_new_tzname(static_cast<const char *>(transit.abbreviation.toUtf8().constData())));
238 }
239 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset));
240 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(transit.offsetFromUtc));
241 // Create a component to hold initial RRULE if any, plus all RDATEs
242 icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp);
243 icalcomponent_add_property(phaseComp1, icalproperty_new_dtstart(writeLocalICalDateTime(transits.at(i).atUtc, preOffset)));
244 bool useNewRRULE = false;
245
246 // Compile the list of UTC transition dates/times, and check
247 // if the list can be reduced to an RRULE instead of multiple RDATEs.
248 QTime time;
249 QDate date;
250 int year = 0;
251 int month = 0;
252 int daysInMonth = 0;
253 int dayOfMonth = 0; // avoid compiler warnings
254 int dayOfWeek = 0; // Monday = 1
255 int nthFromStart = 0; // nth (weekday) of month
256 int nthFromEnd = 0; // nth last (weekday) of month
257 int newRule;
258 int rule = 0;
259 QList<QDateTime> rdates; // dates which (probably) need to be written as RDATEs
260 QList<QDateTime> times;
261 QDateTime qdt = transits.at(i).atUtc; // set 'qdt' for start of loop
262 times += qdt;
263 transitionsDone[i] = true;
264 do {
265 if (!rule) {
266 // Initialise data for detecting a new rule
267 rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
268 time = qdt.time();
269 date = qdt.date();
270 year = date.year();
271 month = date.month();
272 daysInMonth = date.daysInMonth();
273 dayOfWeek = date.dayOfWeek(); // Monday = 1
274 dayOfMonth = date.day();
275 nthFromStart = (dayOfMonth - 1) / 7 + 1; // nth (weekday) of month
276 nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1; // nth last (weekday) of month
277 }
278 if (++i >= trcount) {
279 newRule = 0;
280 times += QDateTime(); // append a dummy value since last value in list is ignored
281 } else {
282 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
283 || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
284 continue;
285 }
286 transitionsDone[i] = true;
287 qdt = transits.at(i).atUtc;
288 if (!qdt.isValid()) {
289 continue;
290 }
291 newRule = rule;
292 times += qdt;
293 date = qdt.date();
294 if (qdt.time() != time || date.month() != month || date.year() != ++year) {
295 newRule = 0;
296 } else {
297 const int day = date.day();
298 if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
299 newRule &= ~DAY_OF_MONTH;
300 }
301 if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
302 if (date.dayOfWeek() != dayOfWeek) {
303 newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
304 } else {
305 if ((newRule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) {
306 newRule &= ~WEEKDAY_OF_MONTH;
307 }
308 if ((newRule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd) {
309 newRule &= ~LAST_WEEKDAY_OF_MONTH;
310 }
311 }
312 }
313 }
314 }
315 if (!newRule) {
316 // The previous rule (if any) no longer applies.
317 // Write all the times up to but not including the current one.
318 // First check whether any of the last RDATE values fit this rule.
319 int yr = times[0].date().year();
320 while (!rdates.isEmpty()) {
321 qdt = rdates.last();
322 date = qdt.date();
323 if (qdt.time() != time || date.month() != month || date.year() != --yr) {
324 break;
325 }
326 const int day = date.day();
327 if (rule & DAY_OF_MONTH) {
328 if (day != dayOfMonth) {
329 break;
330 }
331 } else {
332 if (date.dayOfWeek() != dayOfWeek || ((rule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart)
333 || ((rule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd)) {
334 break;
335 }
336 }
337 times.prepend(qdt);
338 rdates.pop_back();
339 }
340 if (times.count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
341 // There are enough dates to combine into an RRULE
342 icalrecurrencetype r;
343 icalrecurrencetype_clear(&r);
344 r.freq = ICAL_YEARLY_RECURRENCE;
345 r.by_month[0] = month;
346 if (rule & DAY_OF_MONTH) {
347 r.by_month_day[0] = dayOfMonth;
348 } else if (rule & WEEKDAY_OF_MONTH) {
349 r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8); // Sunday = 1
350 } else if (rule & LAST_WEEKDAY_OF_MONTH) {
351 r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8); // Sunday = 1
352 }
353 r.until = writeLocalICalDateTime(times.takeAt(times.size() - 1), preOffset);
354 icalproperty *prop = icalproperty_new_rrule(r);
355 if (useNewRRULE) {
356 // This RRULE doesn't start from the phase start date, so set it into
357 // a new STANDARD/DAYLIGHT component in the VTIMEZONE.
358 icalcomponent *c = icalcomponent_new_clone(phaseComp);
359 icalcomponent_add_property(c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset)));
360 icalcomponent_add_property(c, prop);
361 icalcomponent_add_component(tzcomp, c);
362 } else {
363 icalcomponent_add_property(phaseComp1, prop);
364 }
365 } else {
366 // Save dates for writing as RDATEs
367 for (int t = 0, tend = times.count() - 1; t < tend; ++t) {
368 rdates += times[t];
369 }
370 }
371 useNewRRULE = true;
372 // All date/time values but the last have been added to the VTIMEZONE.
373 // Remove them from the list.
374 qdt = times.last(); // set 'qdt' for start of loop
375 times.clear();
376 times += qdt;
377 }
378 rule = newRule;
379 } while (i < trcount);
380
381 // Write remaining dates as RDATEs
382 for (int rd = 0, rdend = rdates.count(); rd < rdend; ++rd) {
383 dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset);
384 icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod));
385 }
386 icalcomponent_add_component(tzcomp, phaseComp1);
387 icalcomponent_free(phaseComp);
388 }
389
390 return tzcomp;
391}
392
393icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(const QTimeZone &tz, const QDateTime &earliest)
394{
395 auto itz = icaltimezone_new();
396 icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest));
397 return itz;
398}
399
400void ICalTimeZoneParser::parse(icalcomponent *calendar)
401{
402 for (auto *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT); c;
403 c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) {
404 auto icalZone = parseTimeZone(c);
405 // icalZone.dump();
406 if (!icalZone.id.isEmpty()) {
407 if (!icalZone.qZone.isValid()) {
408 icalZone.qZone = resolveICalTimeZone(icalZone);
409 }
410 if (!icalZone.qZone.isValid()) {
411 qCWarning(KCALCORE_LOG) << "Failed to map" << icalZone.id << "to a known IANA timezone";
412 continue;
413 }
414 mCache->insert(icalZone.id, icalZone);
415 }
416 }
417}
418
419QTimeZone ICalTimeZoneParser::resolveICalTimeZone(const ICalTimeZone &icalZone)
420{
421 const auto phase = icalZone.standard;
422 const auto now = QDateTime::currentDateTimeUtc();
423
424 const auto candidates = QTimeZone::availableTimeZoneIds(phase.utcOffset);
425 QMap<int, QTimeZone> matchedCandidates;
426 for (const auto &tzid : candidates) {
427 const QTimeZone candidate(tzid);
428 // This would be a fallback, candidate has transitions, but the phase does not
429 if (candidate.hasTransitions() == phase.transitions.isEmpty()) {
430 matchedCandidates.insert(0, candidate);
431 continue;
432 }
433
434 // Without transitions, we can't do any more precise matching, so just
435 // accept this candidate and be done with it
436 if (!candidate.hasTransitions() && phase.transitions.isEmpty()) {
437 return candidate;
438 }
439
440 // Calculate how many transitions this candidate shares with the phase.
441 // The candidate with the most matching transitions will win.
442 auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20));
443 // If no transition older than 20 years is found, we will start from beginning
444 if (begin == phase.transitions.cend()) {
445 begin = phase.transitions.cbegin();
446 }
447 auto end = std::upper_bound(begin, phase.transitions.cend(), now);
448 int matchedTransitions = 0;
449 for (auto it = begin; it != end; ++it) {
450 const auto &transition = *it;
451 const QTimeZone::OffsetDataList candidateTransitions = candidate.transitions(transition, transition);
452 if (candidateTransitions.isEmpty()) {
453 continue;
454 }
455 ++matchedTransitions; // 1 point for a matching transition
456 const auto candidateTransition = candidateTransitions[0];
457 // FIXME: THIS IS HOW IT SHOULD BE:
458 // const auto abvs = transition.abbreviations();
459 const auto abvs = phase.abbrevs;
460 for (const auto &abv : abvs) {
461 if (candidateTransition.abbreviation == QString::fromUtf8(abv)) {
462 matchedTransitions += 1024; // lots of points for a transition with a matching abbreviation
463 break;
464 }
465 }
466 }
467 matchedCandidates.insert(matchedTransitions, candidate);
468 }
469
470 if (!matchedCandidates.isEmpty()) {
471 return matchedCandidates.value(matchedCandidates.lastKey());
472 }
473
474 return {};
475}
476
477ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone)
478{
479 ICalTimeZone icalTz;
480
481 if (auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) {
482 icalTz.id = icalproperty_get_value_as_string(tzidProp);
483
484 // If the VTIMEZONE is a known IANA time zone don't bother parsing the rest
485 // of the VTIMEZONE, get QTimeZone directly from Qt
486 if (QTimeZone::isTimeZoneIdAvailable(icalTz.id) || icalTz.id.startsWith("UTC")) {
487 icalTz.qZone = QTimeZone(icalTz.id);
488 return icalTz;
489 } else {
490 // Not IANA, but maybe we can match it from Windows ID?
491 const auto ianaTzid = QTimeZone::windowsIdToDefaultIanaId(icalTz.id);
492 if (!ianaTzid.isEmpty()) {
493 icalTz.qZone = QTimeZone(ianaTzid);
494 return icalTz;
495 }
496 }
497 }
498
499 for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT); c;
500 c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) {
501 icalcomponent_kind kind = icalcomponent_isa(c);
502 switch (kind) {
503 case ICAL_XSTANDARD_COMPONENT:
504 // qCDebug(KCALCORE_LOG) << "---standard phase: found";
505 parsePhase(c, false, icalTz.standard);
506 break;
507 case ICAL_XDAYLIGHT_COMPONENT:
508 // qCDebug(KCALCORE_LOG) << "---daylight phase: found";
509 parsePhase(c, true, icalTz.daylight);
510 break;
511
512 default:
513 qCDebug(KCALCORE_LOG) << "Unknown component:" << int(kind);
514 break;
515 }
516 }
517
518 return icalTz;
519}
520
521bool ICalTimeZoneParser::parsePhase(icalcomponent *c, bool daylight, ICalTimeZonePhase &phase)
522{
523 // Read the observance data for this standard/daylight savings phase
524 int utcOffset = 0;
525 int prevOffset = 0;
526 bool recurs = false;
527 bool found_dtstart = false;
528 bool found_tzoffsetfrom = false;
529 bool found_tzoffsetto = false;
530 icaltimetype dtstart = icaltime_null_time();
531 QSet<QByteArray> abbrevs;
532
533 // Now do the ical reading.
534 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
535 while (p) {
536 icalproperty_kind kind = icalproperty_isa(p);
537 switch (kind) {
538 case ICAL_TZNAME_PROPERTY: { // abbreviated name for this time offset
539 // TZNAME can appear multiple times in order to provide language
540 // translations of the time zone offset name.
541
542 // TODO: Does this cope with multiple language specifications?
543 QByteArray name = icalproperty_get_tzname(p);
544 // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME
545 // strings, which is totally useless. So ignore those.
546 if ((!daylight && name == "Standard Time") || (daylight && name == "Daylight Time")) {
547 break;
548 }
549 abbrevs.insert(name);
550 break;
551 }
552 case ICAL_DTSTART_PROPERTY: // local time at which phase starts
553 dtstart = icalproperty_get_dtstart(p);
554 found_dtstart = true;
555 break;
556
557 case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase
558 prevOffset = icalproperty_get_tzoffsetfrom(p);
559 found_tzoffsetfrom = true;
560 break;
561
562 case ICAL_TZOFFSETTO_PROPERTY:
563 utcOffset = icalproperty_get_tzoffsetto(p);
564 found_tzoffsetto = true;
565 break;
566
567 case ICAL_RDATE_PROPERTY:
568 case ICAL_RRULE_PROPERTY:
569 recurs = true;
570 break;
571
572 default:
573 break;
574 }
575 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
576 }
577
578 // Validate the phase data
579 if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
580 qCDebug(KCALCORE_LOG) << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
581 return false;
582 }
583
584 // Convert DTSTART to QDateTime, and from local time to UTC
585 dtstart.second -= prevOffset;
586 dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone());
587 const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart)); // UTC
588
589 phase.abbrevs.unite(abbrevs);
590 phase.utcOffset = utcOffset;
591 phase.transitions += utcStart;
592
593 if (recurs) {
594 /* RDATE or RRULE is specified. There should only be one or the other, but
595 * it doesn't really matter - the code can cope with both.
596 * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading
597 * recurrences.
598 */
599 const QDateTime maxTime(MAX_DATE());
600 Recurrence recur;
601 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
602 while (p) {
603 icalproperty_kind kind = icalproperty_isa(p);
604 switch (kind) {
605 case ICAL_RDATE_PROPERTY: {
606 icaltimetype t = icalproperty_get_rdate(p).time;
607 if (icaltime_is_date(t)) {
608 // RDATE with a DATE value inherits the (local) time from DTSTART
609 t.hour = dtstart.hour;
610 t.minute = dtstart.minute;
611 t.second = dtstart.second;
612 t.is_date = 0;
613 }
614 // RFC2445 states that RDATE must be in local time,
615 // but we support UTC as well to be safe.
616 if (!icaltime_is_utc(t)) {
617 t.second -= prevOffset; // convert to UTC
618 t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone());
619 t = icaltime_normalize(t);
620 }
621 phase.transitions += toQDateTime(t);
622 break;
623 }
624 case ICAL_RRULE_PROPERTY: {
626 ICalFormat icf;
627 ICalFormatImpl impl(&icf);
628 impl.readRecurrence(icalproperty_get_rrule(p), &r);
629 r.setStartDt(utcStart);
630 // The end date time specified in an RRULE must be in UTC.
631 // We can not guarantee correctness if this is not the case.
632 if (r.duration() == 0 && r.endDt().timeZone() != QTimeZone::utc()) {
633 qCWarning(KCALCORE_LOG) << "UNTIL in RRULE must be specified in UTC";
634 break;
635 }
636 const auto dts = r.timesInInterval(utcStart, maxTime);
637 for (int i = 0, end = dts.count(); i < end; ++i) {
638 phase.transitions += dts[i];
639 }
640 break;
641 }
642 default:
643 break;
644 }
645 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
646 }
647 sortAndRemoveDuplicates(phase.transitions);
648 }
649
650 return true;
651}
652
653QByteArray ICalTimeZoneParser::vcaltimezoneFromQTimeZone(const QTimeZone &qtz, const QDateTime &earliest)
654{
655 auto icalTz = icalcomponentFromQTimeZone(qtz, earliest);
656 const QByteArray result(icalcomponent_as_ical_string(icalTz));
657 icalmemory_free_ring();
658 icalcomponent_free(icalTz);
659 return result;
660}
661
662} // namespace KCalendarCore
iCalendar format implementation.
Definition icalformat.h:45
@ RoleEndTimeZone
Role for determining an incidence's ending timezone.
@ RoleStartTimeZone
Role for determining an incidence's starting timezone.
This class represents a recurrence rule for a calendar incidence.
QDateTime endDt(bool *result=nullptr) const
Returns the date and time of the last recurrence.
int duration() const
Returns -1 if the event recurs infinitely, 0 if the end date is set, otherwise the total number of re...
QList< QDateTime > timesInInterval(const QDateTime &start, const QDateTime &end) const
Returns a list of all the times at which the recurrence will occur between two specified times.
void setStartDt(const QDateTime &start)
Sets the recurrence start date/time.
This class represents a recurrence rule for a calendar incidence.
Definition recurrence.h:77
This file is part of the API for handling calendar data and defines the ICalFormat class.
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
Namespace for all KCalendarCore types.
Definition alarm.h:37
const QList< QKeySequence > & begin()
const QList< QKeySequence > & end()
QString name(StandardShortcut id)
const char * constData() const const
QDate currentDate()
int day() const const
int dayOfWeek() const const
int daysInMonth() const const
int month() const const
int year() const const
QDateTime addSecs(qint64 s) const const
QDateTime currentDateTimeUtc()
QDate date() const const
bool isValid() const const
QTime time() const const
QTimeZone timeZone() const const
void clear()
qsizetype count() const const
bool isEmpty() const const
T & last()
void pop_back()
void prepend(parameter_type value)
qsizetype size() const const
T takeAt(qsizetype i)
iterator insert(const Key &key, const T &value)
bool isEmpty() const const
const Key & lastKey() const const
T value(const Key &key, const T &defaultValue) const const
iterator insert(const T &value)
QString fromUtf8(QByteArrayView str)
int hour() const const
int minute() const const
int second() const const
typedef OffsetDataList
QList< QByteArray > availableTimeZoneIds()
QByteArray id() const const
bool isTimeZoneIdAvailable(const QByteArray &ianaId)
OffsetDataList transitions(const QDateTime &fromDateTime, const QDateTime &toDateTime) const const
QTimeZone utc()
QByteArray windowsIdToDefaultIanaId(const QByteArray &windowsId)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 3 2024 11:44:31 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.