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  if (transits.isEmpty()) {
194  qCDebug(KCALCORE_LOG) << "No transition information available VTIMEZONE will be invalid.";
195  }
196  }
197  if (earliest.isValid()) {
198  // Remove all transitions earlier than those we are interested in
199  for (int i = 0, end = transits.count(); i < end; ++i) {
200  if (transits.at(i).atUtc >= earliest) {
201  if (i > 0) {
202  transits.erase(transits.begin(), transits.begin() + i);
203  }
204  break;
205  }
206  }
207  }
208  int trcount = transits.count();
209  QVector<bool> transitionsDone(trcount, false);
210 
211  // Go through the list of transitions and create an iCal component for each
212  // distinct combination of phase after and UTC offset before the transition.
213  icaldatetimeperiodtype dtperiod;
214  dtperiod.period = icalperiodtype_null_period();
215  for (;;) {
216  int i = 0;
217  for (; i < trcount && transitionsDone[i]; ++i) {
218  ;
219  }
220  if (i >= trcount) {
221  break;
222  }
223  // Found a phase combination which hasn't yet been processed
224  const int preOffset = (i > 0) ? transits.at(i - 1).offsetFromUtc : 0;
225  const auto &transit = transits.at(i);
226  if (transit.offsetFromUtc == preOffset) {
227  transitionsDone[i] = true;
228  while (++i < trcount) {
229  if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
230  || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
231  continue;
232  }
233  transitionsDone[i] = true;
234  }
235  continue;
236  }
237  const bool isDst = transit.daylightTimeOffset > 0;
238  icalcomponent *phaseComp = icalcomponent_new(isDst ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT);
239  if (!transit.abbreviation.isEmpty()) {
240  icalcomponent_add_property(phaseComp, icalproperty_new_tzname(static_cast<const char *>(transit.abbreviation.toUtf8().constData())));
241  }
242  icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset));
243  icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(transit.offsetFromUtc));
244  // Create a component to hold initial RRULE if any, plus all RDATEs
245  icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp);
246  icalcomponent_add_property(phaseComp1, icalproperty_new_dtstart(writeLocalICalDateTime(transits.at(i).atUtc, preOffset)));
247  bool useNewRRULE = false;
248 
249  // Compile the list of UTC transition dates/times, and check
250  // if the list can be reduced to an RRULE instead of multiple RDATEs.
251  QTime time;
252  QDate date;
253  int year = 0;
254  int month = 0;
255  int daysInMonth = 0;
256  int dayOfMonth = 0; // avoid compiler warnings
257  int dayOfWeek = 0; // Monday = 1
258  int nthFromStart = 0; // nth (weekday) of month
259  int nthFromEnd = 0; // nth last (weekday) of month
260  int newRule;
261  int rule = 0;
262  QList<QDateTime> rdates; // dates which (probably) need to be written as RDATEs
263  QList<QDateTime> times;
264  QDateTime qdt = transits.at(i).atUtc; // set 'qdt' for start of loop
265  times += qdt;
266  transitionsDone[i] = true;
267  do {
268  if (!rule) {
269  // Initialise data for detecting a new rule
270  rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
271  time = qdt.time();
272  date = qdt.date();
273  year = date.year();
274  month = date.month();
275  daysInMonth = date.daysInMonth();
276  dayOfWeek = date.dayOfWeek(); // Monday = 1
277  dayOfMonth = date.day();
278  nthFromStart = (dayOfMonth - 1) / 7 + 1; // nth (weekday) of month
279  nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1; // nth last (weekday) of month
280  }
281  if (++i >= trcount) {
282  newRule = 0;
283  times += QDateTime(); // append a dummy value since last value in list is ignored
284  } else {
285  if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
286  || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
287  continue;
288  }
289  transitionsDone[i] = true;
290  qdt = transits.at(i).atUtc;
291  if (!qdt.isValid()) {
292  continue;
293  }
294  newRule = rule;
295  times += qdt;
296  date = qdt.date();
297  if (qdt.time() != time || date.month() != month || date.year() != ++year) {
298  newRule = 0;
299  } else {
300  const int day = date.day();
301  if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
302  newRule &= ~DAY_OF_MONTH;
303  }
304  if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
305  if (date.dayOfWeek() != dayOfWeek) {
306  newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
307  } else {
308  if ((newRule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) {
309  newRule &= ~WEEKDAY_OF_MONTH;
310  }
311  if ((newRule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd) {
312  newRule &= ~LAST_WEEKDAY_OF_MONTH;
313  }
314  }
315  }
316  }
317  }
318  if (!newRule) {
319  // The previous rule (if any) no longer applies.
320  // Write all the times up to but not including the current one.
321  // First check whether any of the last RDATE values fit this rule.
322  int yr = times[0].date().year();
323  while (!rdates.isEmpty()) {
324  qdt = rdates.last();
325  date = qdt.date();
326  if (qdt.time() != time || date.month() != month || date.year() != --yr) {
327  break;
328  }
329  const int day = date.day();
330  if (rule & DAY_OF_MONTH) {
331  if (day != dayOfMonth) {
332  break;
333  }
334  } else {
335  if (date.dayOfWeek() != dayOfWeek || ((rule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart)
336  || ((rule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd)) {
337  break;
338  }
339  }
340  times.prepend(qdt);
341  rdates.pop_back();
342  }
343  if (times.count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
344  // There are enough dates to combine into an RRULE
345  icalrecurrencetype r;
346  icalrecurrencetype_clear(&r);
347  r.freq = ICAL_YEARLY_RECURRENCE;
348  r.by_month[0] = month;
349  if (rule & DAY_OF_MONTH) {
350  r.by_month_day[0] = dayOfMonth;
351  } else if (rule & WEEKDAY_OF_MONTH) {
352  r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8); // Sunday = 1
353  } else if (rule & LAST_WEEKDAY_OF_MONTH) {
354  r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8); // Sunday = 1
355  }
356  r.until = writeLocalICalDateTime(times.takeAt(times.size() - 1), preOffset);
357  icalproperty *prop = icalproperty_new_rrule(r);
358  if (useNewRRULE) {
359  // This RRULE doesn't start from the phase start date, so set it into
360  // a new STANDARD/DAYLIGHT component in the VTIMEZONE.
361  icalcomponent *c = icalcomponent_new_clone(phaseComp);
362  icalcomponent_add_property(c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset)));
363  icalcomponent_add_property(c, prop);
364  icalcomponent_add_component(tzcomp, c);
365  } else {
366  icalcomponent_add_property(phaseComp1, prop);
367  }
368  } else {
369  // Save dates for writing as RDATEs
370  for (int t = 0, tend = times.count() - 1; t < tend; ++t) {
371  rdates += times[t];
372  }
373  }
374  useNewRRULE = true;
375  // All date/time values but the last have been added to the VTIMEZONE.
376  // Remove them from the list.
377  qdt = times.last(); // set 'qdt' for start of loop
378  times.clear();
379  times += qdt;
380  }
381  rule = newRule;
382  } while (i < trcount);
383 
384  // Write remaining dates as RDATEs
385  for (int rd = 0, rdend = rdates.count(); rd < rdend; ++rd) {
386  dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset);
387  icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod));
388  }
389  icalcomponent_add_component(tzcomp, phaseComp1);
390  icalcomponent_free(phaseComp);
391  }
392 
393  return tzcomp;
394 }
395 
396 icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(const QTimeZone &tz, const QDateTime &earliest)
397 {
398  auto itz = icaltimezone_new();
399  icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest));
400  return itz;
401 }
402 
403 void ICalTimeZoneParser::parse(icalcomponent *calendar)
404 {
405  for (auto *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT); c;
406  c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) {
407  auto icalZone = parseTimeZone(c);
408  // icalZone.dump();
409  if (!icalZone.id.isEmpty()) {
410  if (!icalZone.qZone.isValid()) {
411  icalZone.qZone = resolveICalTimeZone(icalZone);
412  }
413  if (!icalZone.qZone.isValid()) {
414  qCWarning(KCALCORE_LOG) << "Failed to map" << icalZone.id << "to a known IANA timezone";
415  continue;
416  }
417  mCache->insert(icalZone.id, icalZone);
418  }
419  }
420 }
421 
422 QTimeZone ICalTimeZoneParser::resolveICalTimeZone(const ICalTimeZone &icalZone)
423 {
424  const auto phase = icalZone.standard;
425  const auto now = QDateTime::currentDateTimeUtc();
426 
427  const auto candidates = QTimeZone::availableTimeZoneIds(phase.utcOffset);
428  QMap<int, QTimeZone> matchedCandidates;
429  for (const auto &tzid : candidates) {
430  const QTimeZone candidate(tzid);
431  // This would be a fallback, candidate has transitions, but the phase does not
432  if (candidate.hasTransitions() == phase.transitions.isEmpty()) {
433  matchedCandidates.insert(0, candidate);
434  continue;
435  }
436 
437  // Without transitions, we can't do any more precise matching, so just
438  // accept this candidate and be done with it
439  if (!candidate.hasTransitions() && phase.transitions.isEmpty()) {
440  return candidate;
441  }
442 
443  // Calculate how many transitions this candidate shares with the phase.
444  // The candidate with the most matching transitions will win.
445  auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20));
446  // If no transition older than 20 years is found, we will start from beginning
447  if (begin == phase.transitions.cend()) {
448  begin = phase.transitions.cbegin();
449  }
450  auto end = std::upper_bound(begin, phase.transitions.cend(), now);
451  int matchedTransitions = 0;
452  for (auto it = begin; it != end; ++it) {
453  const auto &transition = *it;
454  const QTimeZone::OffsetDataList candidateTransitions = candidate.transitions(transition, transition);
455  if (candidateTransitions.isEmpty()) {
456  continue;
457  }
458  ++matchedTransitions; // 1 point for a matching transition
459  const auto candidateTransition = candidateTransitions[0];
460  // FIXME: THIS IS HOW IT SHOULD BE:
461  // const auto abvs = transition.abbreviations();
462  const auto abvs = phase.abbrevs;
463  for (const auto &abv : abvs) {
464  if (candidateTransition.abbreviation == QString::fromUtf8(abv)) {
465  matchedTransitions += 1024; // lots of points for a transition with a matching abbreviation
466  break;
467  }
468  }
469  }
470  matchedCandidates.insert(matchedTransitions, candidate);
471  }
472 
473  if (!matchedCandidates.isEmpty()) {
474  return matchedCandidates.value(matchedCandidates.lastKey());
475  }
476 
477  return {};
478 }
479 
480 ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone)
481 {
482  ICalTimeZone icalTz;
483 
484  if (auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) {
485  icalTz.id = icalproperty_get_value_as_string(tzidProp);
486 
487  // If the VTIMEZONE is a known IANA time zone don't bother parsing the rest
488  // of the VTIMEZONE, get QTimeZone directly from Qt
489  if (QTimeZone::isTimeZoneIdAvailable(icalTz.id) || icalTz.id.startsWith("UTC")) {
490  icalTz.qZone = QTimeZone(icalTz.id);
491  return icalTz;
492  } else {
493  // Not IANA, but maybe we can match it from Windows ID?
494  const auto ianaTzid = QTimeZone::windowsIdToDefaultIanaId(icalTz.id);
495  if (!ianaTzid.isEmpty()) {
496  icalTz.qZone = QTimeZone(ianaTzid);
497  return icalTz;
498  }
499  }
500  }
501 
502  for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT); c;
503  c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) {
504  icalcomponent_kind kind = icalcomponent_isa(c);
505  switch (kind) {
506  case ICAL_XSTANDARD_COMPONENT:
507  // qCDebug(KCALCORE_LOG) << "---standard phase: found";
508  parsePhase(c, false, icalTz.standard);
509  break;
510  case ICAL_XDAYLIGHT_COMPONENT:
511  // qCDebug(KCALCORE_LOG) << "---daylight phase: found";
512  parsePhase(c, true, icalTz.daylight);
513  break;
514 
515  default:
516  qCDebug(KCALCORE_LOG) << "Unknown component:" << int(kind);
517  break;
518  }
519  }
520 
521  return icalTz;
522 }
523 
524 bool ICalTimeZoneParser::parsePhase(icalcomponent *c, bool daylight, ICalTimeZonePhase &phase)
525 {
526  // Read the observance data for this standard/daylight savings phase
527  int utcOffset = 0;
528  int prevOffset = 0;
529  bool recurs = false;
530  bool found_dtstart = false;
531  bool found_tzoffsetfrom = false;
532  bool found_tzoffsetto = false;
533  icaltimetype dtstart = icaltime_null_time();
534  QSet<QByteArray> abbrevs;
535 
536  // Now do the ical reading.
537  icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
538  while (p) {
539  icalproperty_kind kind = icalproperty_isa(p);
540  switch (kind) {
541  case ICAL_TZNAME_PROPERTY: { // abbreviated name for this time offset
542  // TZNAME can appear multiple times in order to provide language
543  // translations of the time zone offset name.
544 
545  // TODO: Does this cope with multiple language specifications?
546  QByteArray name = icalproperty_get_tzname(p);
547  // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME
548  // strings, which is totally useless. So ignore those.
549  if ((!daylight && name == "Standard Time") || (daylight && name == "Daylight Time")) {
550  break;
551  }
552  abbrevs.insert(name);
553  break;
554  }
555  case ICAL_DTSTART_PROPERTY: // local time at which phase starts
556  dtstart = icalproperty_get_dtstart(p);
557  found_dtstart = true;
558  break;
559 
560  case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase
561  prevOffset = icalproperty_get_tzoffsetfrom(p);
562  found_tzoffsetfrom = true;
563  break;
564 
565  case ICAL_TZOFFSETTO_PROPERTY:
566  utcOffset = icalproperty_get_tzoffsetto(p);
567  found_tzoffsetto = true;
568  break;
569 
570  case ICAL_RDATE_PROPERTY:
571  case ICAL_RRULE_PROPERTY:
572  recurs = true;
573  break;
574 
575  default:
576  break;
577  }
578  p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
579  }
580 
581  // Validate the phase data
582  if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
583  qCDebug(KCALCORE_LOG) << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
584  return false;
585  }
586 
587  // Convert DTSTART to QDateTime, and from local time to UTC
588  dtstart.second -= prevOffset;
589  dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone());
590  const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart)); // UTC
591 
592  phase.abbrevs.unite(abbrevs);
593  phase.utcOffset = utcOffset;
594  phase.transitions += utcStart;
595 
596  if (recurs) {
597  /* RDATE or RRULE is specified. There should only be one or the other, but
598  * it doesn't really matter - the code can cope with both.
599  * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading
600  * recurrences.
601  */
602  const QDateTime maxTime(MAX_DATE());
603  Recurrence recur;
604  icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
605  while (p) {
606  icalproperty_kind kind = icalproperty_isa(p);
607  switch (kind) {
608  case ICAL_RDATE_PROPERTY: {
609  icaltimetype t = icalproperty_get_rdate(p).time;
610  if (icaltime_is_date(t)) {
611  // RDATE with a DATE value inherits the (local) time from DTSTART
612  t.hour = dtstart.hour;
613  t.minute = dtstart.minute;
614  t.second = dtstart.second;
615  t.is_date = 0;
616  }
617  // RFC2445 states that RDATE must be in local time,
618  // but we support UTC as well to be safe.
619  if (!icaltime_is_utc(t)) {
620  t.second -= prevOffset; // convert to UTC
621  t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone());
622  t = icaltime_normalize(t);
623  }
624  phase.transitions += toQDateTime(t);
625  break;
626  }
627  case ICAL_RRULE_PROPERTY: {
628  RecurrenceRule r;
629  ICalFormat icf;
630  ICalFormatImpl impl(&icf);
631  impl.readRecurrence(icalproperty_get_rrule(p), &r);
632  r.setStartDt(utcStart);
633  // The end date time specified in an RRULE must be in UTC.
634  // We can not guarantee correctness if this is not the case.
635  if (r.duration() == 0 && r.endDt().timeSpec() != Qt::UTC) {
636  qCWarning(KCALCORE_LOG) << "UNTIL in RRULE must be specified in UTC";
637  break;
638  }
639  const auto dts = r.timesInInterval(utcStart, maxTime);
640  for (int i = 0, end = dts.count(); i < end; ++i) {
641  phase.transitions += dts[i];
642  }
643  break;
644  }
645  default:
646  break;
647  }
648  p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
649  }
650  sortAndRemoveDuplicates(phase.transitions);
651  }
652 
653  return true;
654 }
655 
656 QByteArray ICalTimeZoneParser::vcaltimezoneFromQTimeZone(const QTimeZone &qtz, const QDateTime &earliest)
657 {
658  auto icalTz = icalcomponentFromQTimeZone(qtz, earliest);
659  const QByteArray result(icalcomponent_as_ical_string(icalTz));
660  icalmemory_free_ring();
661  icalcomponent_free(icalTz);
662  return result;
663 }
664 
665 } // 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
QString name(StandardShortcut id)
@ RoleEndTimeZone
Role for determining an incidence's ending timezone.
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-2022 The KDE developers.
Generated on Wed Aug 10 2022 04:02:54 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.