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 <icaltimezone.h>
23 #include <libical/ical.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)) {
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
void clear()
int minute() const const
QString name(const QVariant &location)
void setStartDt(const QDateTime &start)
Sets the recurrence start date/time.
const Key & lastKey() const const
QTime time() const const
T takeAt(int i)
int day() const const
QDateTime endDt(bool *result=nullptr) const
Returns the date and time of the last recurrence.
QSet::iterator insert(const T &value)
const QList< QKeySequence > & begin()
int second() const const
int size() const const
QByteArray windowsIdToDefaultIanaId(const QByteArray &windowsId)
QList< QByteArray > availableTimeZoneIds()
int dayOfWeek() const const
QTimeZone::OffsetDataList transitions(const QDateTime &fromDateTime, const QDateTime &toDateTime) const const
int count(const T &value) const const
Role for determining an incidence&#39;s starting timezone.
QString fromUtf8(const char *str, int size)
Role for determining an incidence&#39;s ending timezone.
QTimeZone utc()
bool isEmpty() const const
const char * constData() const const
int daysInMonth() const const
void pop_back()
int hour() const const
Qt::TimeSpec timeSpec() const const
QByteArray id() const const
bool isValid() const const
const QList< QKeySequence > & end()
This class represents a recurrence rule for a calendar incidence.
Definition: recurrence.h:76
QDate date() const const
iCalendar format implementation.
Definition: icalformat.h:43
QDate currentDate()
T & last()
void prepend(const T &value)
QMap::iterator insert(const Key &key, const T &value)
bool isEmpty() 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...
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...
This class represents a recurrence rule for a calendar incidence.
QDateTime addSecs(qint64 s) const const
bool isTimeZoneIdAvailable(const QByteArray &ianaId)
int year() const const
typedef OffsetDataList
int month() const const
QDateTime currentDateTimeUtc()
This file is part of the API for handling calendar data and defines the ICalFormat class...
Namespace for all KCalendarCore types.
Definition: alarm.h:36
QTimeZone timeZone() const const
const T value(const Key &key, const T &defaultValue) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Thu Sep 23 2021 22:51:47 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.