KCalendarCore

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

KDE's Doxygen guidelines are available online.