10 #include "icalformat_p.h"
11 #include "icaltimezones_p.h"
12 #include "recurrence.h"
13 #include "recurrencehelper_p.h"
14 #include "recurrencerule.h"
16 #include "kcalendarcore_debug.h"
22 #include <libical/ical.h>
23 #include <libical/icaltimezone.h>
29 static const int minRuleCount = 5;
30 static const int minPhaseCount = 8;
33 static QDateTime toQDateTime(
const icaltimetype &t)
36 QTime(t.hour, t.minute, t.second),
52 static icaltimetype writeLocalICalDateTime(
const QDateTime &utc,
int offset)
55 icaltimetype t = icaltime_null_time();
69 void ICalTimeZonePhase::dump()
71 qDebug() <<
" ~~~ ICalTimeZonePhase ~~~";
72 qDebug() <<
" Abbreviations:" << abbrevs;
73 qDebug() <<
" UTC offset:" << utcOffset;
74 qDebug() <<
" Transitions:" << transitions;
75 qDebug() <<
" ~~~~~~~~~~~~~~~~~~~~~~~~~";
78 void ICalTimeZone::dump()
80 qDebug() <<
"~~~ ICalTimeZone ~~~";
81 qDebug() <<
"ID:" << id;
82 qDebug() <<
"QZONE:" << qZone.id();
87 qDebug() <<
"~~~~~~~~~~~~~~~~~~~~";
90 ICalTimeZoneCache::ICalTimeZoneCache()
94 void ICalTimeZoneCache::insert(
const QByteArray &
id,
const ICalTimeZone &tz)
96 mCache.insert(
id, tz);
102 typename T::const_iterator greatestSmallerThan(
const T &c,
const typename T::value_type &v)
104 auto it = std::lower_bound(c.cbegin(), c.cend(), v);
105 if (it != c.cbegin()) {
119 const ICalTimeZone tz = mCache.value(tzid);
120 if (!tz.qZone.isValid()) {
129 if (tz.qZone.id().startsWith(
"UTC")) {
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) {
138 auto dtsTzId = std::find_if(tzids.cbegin(), tzids.cend(), [](
const QByteArray &
id) {
139 return id.startsWith(
"UTC");
141 if (dtsTzId != tzids.cend()) {
151 ICalTimeZoneParser::ICalTimeZoneParser(ICalTimeZoneCache *cache)
156 void ICalTimeZoneParser::updateTzEarliestDate(
const IncidenceBase::Ptr &incidence, TimeZoneEarliestDate *earliest)
159 const auto dt =
incidence->dateTime(role);
164 const auto prev = earliest->value(
incidence->dtStart().timeZone());
165 if (!prev.isValid() ||
incidence->dtStart() < prev) {
166 earliest->insert(
incidence->dtStart().timeZone(), prev);
172 icalcomponent *ICalTimeZoneParser::icalcomponentFromQTimeZone(
const QTimeZone &tz,
const QDateTime &earliest)
177 WEEKDAY_OF_MONTH = 0x02,
178 LAST_WEEKDAY_OF_MONTH = 0x04,
182 icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
183 icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.
id().
constData()));
189 if (transits.isEmpty()) {
193 qCDebug(KCALCORE_LOG) <<
"No transition information available VTIMEZONE will be invalid.";
197 for (
int i = 0, end = transits.count(); i < end; ++i) {
198 if (transits.at(i).atUtc >= earliest) {
200 transits.erase(transits.begin(), transits.begin() + i);
206 int trcount = transits.count();
211 icaldatetimeperiodtype dtperiod;
212 dtperiod.period = icalperiodtype_null_period();
215 for (; i < trcount && transitionsDone[i]; ++i) {
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) {
231 transitionsDone[i] =
true;
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())));
240 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset));
241 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(transit.offsetFromUtc));
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;
256 int nthFromStart = 0;
264 transitionsDone[i] =
true;
268 rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
272 month = date.
month();
275 dayOfMonth = date.
day();
276 nthFromStart = (dayOfMonth - 1) / 7 + 1;
277 nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1;
279 if (++i >= trcount) {
283 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
284 || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
287 transitionsDone[i] =
true;
288 qdt = transits.at(i).atUtc;
295 if (qdt.
time() != time || date.
month() != month || date.
year() != ++year) {
298 const int day = date.
day();
299 if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
300 newRule &= ~DAY_OF_MONTH;
302 if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
304 newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
306 if ((newRule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) {
307 newRule &= ~WEEKDAY_OF_MONTH;
309 if ((newRule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd) {
310 newRule &= ~LAST_WEEKDAY_OF_MONTH;
320 int yr = times[0].date().year();
324 if (qdt.
time() != time || date.
month() != month || date.
year() != --yr) {
327 const int day = date.
day();
328 if (
rule & DAY_OF_MONTH) {
329 if (day != dayOfMonth) {
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)) {
341 if (times.
count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
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);
351 }
else if (
rule & LAST_WEEKDAY_OF_MONTH) {
352 r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8);
354 r.until = writeLocalICalDateTime(times.
takeAt(times.
size() - 1), preOffset);
355 icalproperty *prop = icalproperty_new_rrule(r);
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);
364 icalcomponent_add_property(phaseComp1, prop);
368 for (
int t = 0, tend = times.
count() - 1; t < tend; ++t) {
380 }
while (i < trcount);
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));
387 icalcomponent_add_component(tzcomp, phaseComp1);
388 icalcomponent_free(phaseComp);
394 icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(
const QTimeZone &tz,
const QDateTime &earliest)
396 auto itz = icaltimezone_new();
397 icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest));
401 void ICalTimeZoneParser::parse(icalcomponent *calendar)
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);
407 if (!icalZone.id.isEmpty()) {
408 if (!icalZone.qZone.isValid()) {
409 icalZone.qZone = resolveICalTimeZone(icalZone);
411 if (!icalZone.qZone.isValid()) {
412 qCWarning(KCALCORE_LOG) <<
"Failed to map" << icalZone.id <<
"to a known IANA timezone";
415 mCache->insert(icalZone.id, icalZone);
420 QTimeZone ICalTimeZoneParser::resolveICalTimeZone(
const ICalTimeZone &icalZone)
422 const auto phase = icalZone.standard;
427 for (
const auto &tzid : candidates) {
430 if (candidate.hasTransitions() == phase.transitions.isEmpty()) {
431 matchedCandidates.
insert(0, candidate);
437 if (!candidate.hasTransitions() && phase.transitions.isEmpty()) {
443 auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20));
445 if (begin == phase.transitions.cend()) {
446 begin = phase.transitions.cbegin();
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;
453 if (candidateTransitions.isEmpty()) {
456 ++matchedTransitions;
457 const auto candidateTransition = candidateTransitions[0];
460 const auto abvs = phase.abbrevs;
461 for (
const auto &abv : abvs) {
463 matchedTransitions += 1024;
468 matchedCandidates.
insert(matchedTransitions, candidate);
471 if (!matchedCandidates.
isEmpty()) {
472 return matchedCandidates.
value(matchedCandidates.
lastKey());
478 ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone)
482 if (
auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) {
483 icalTz.id = icalproperty_get_value_as_string(tzidProp);
493 if (!ianaTzid.isEmpty()) {
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);
504 case ICAL_XSTANDARD_COMPONENT:
506 parsePhase(c,
false, icalTz.standard);
508 case ICAL_XDAYLIGHT_COMPONENT:
510 parsePhase(c,
true, icalTz.daylight);
514 qCDebug(KCALCORE_LOG) <<
"Unknown component:" << int(kind);
522 bool ICalTimeZoneParser::parsePhase(icalcomponent *c,
bool daylight, ICalTimeZonePhase &phase)
528 bool found_dtstart =
false;
529 bool found_tzoffsetfrom =
false;
530 bool found_tzoffsetto =
false;
531 icaltimetype dtstart = icaltime_null_time();
535 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
537 icalproperty_kind kind = icalproperty_isa(p);
539 case ICAL_TZNAME_PROPERTY: {
547 if ((!daylight && name ==
"Standard Time") || (daylight && name ==
"Daylight Time")) {
553 case ICAL_DTSTART_PROPERTY:
554 dtstart = icalproperty_get_dtstart(p);
555 found_dtstart =
true;
558 case ICAL_TZOFFSETFROM_PROPERTY:
559 prevOffset = icalproperty_get_tzoffsetfrom(p);
560 found_tzoffsetfrom =
true;
563 case ICAL_TZOFFSETTO_PROPERTY:
564 utcOffset = icalproperty_get_tzoffsetto(p);
565 found_tzoffsetto =
true;
568 case ICAL_RDATE_PROPERTY:
569 case ICAL_RRULE_PROPERTY:
576 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
580 if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
581 qCDebug(KCALCORE_LOG) <<
"DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
586 dtstart.second -= prevOffset;
587 dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone());
588 const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart));
590 phase.abbrevs.unite(abbrevs);
591 phase.utcOffset = utcOffset;
592 phase.transitions += utcStart;
602 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
604 icalproperty_kind kind = icalproperty_isa(p);
606 case ICAL_RDATE_PROPERTY: {
607 icaltimetype t = icalproperty_get_rdate(p).time;
608 if (icaltime_is_date(t)) {
610 t.hour = dtstart.hour;
611 t.minute = dtstart.minute;
612 t.second = dtstart.second;
617 if (!icaltime_is_utc(t)) {
618 t.second -= prevOffset;
619 t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone());
620 t = icaltime_normalize(t);
622 phase.transitions += toQDateTime(t);
625 case ICAL_RRULE_PROPERTY: {
628 ICalFormatImpl impl(&icf);
629 impl.readRecurrence(icalproperty_get_rrule(p), &r);
634 qCWarning(KCALCORE_LOG) <<
"UNTIL in RRULE must be specified in UTC";
638 for (
int i = 0, end = dts.count(); i < end; ++i) {
639 phase.transitions += dts[i];
646 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
648 sortAndRemoveDuplicates(phase.transitions);
656 auto icalTz = icalcomponentFromQTimeZone(qtz, earliest);
657 const QByteArray result(icalcomponent_as_ical_string(icalTz));
658 icalmemory_free_ring();
659 icalcomponent_free(icalTz);