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 if (transits.isEmpty()) {
194 qCDebug(KCALCORE_LOG) <<
"No transition information available VTIMEZONE will be invalid.";
199 for (
int i = 0, end = transits.count(); i < end; ++i) {
200 if (transits.at(i).atUtc >= earliest) {
202 transits.erase(transits.begin(), transits.begin() + i);
208 int trcount = transits.count();
213 icaldatetimeperiodtype dtperiod;
214 dtperiod.period = icalperiodtype_null_period();
217 for (; i < trcount && transitionsDone[i]; ++i) {
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) {
233 transitionsDone[i] =
true;
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())));
242 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset));
243 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(transit.offsetFromUtc));
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;
258 int nthFromStart = 0;
266 transitionsDone[i] =
true;
270 rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
274 month = date.
month();
277 dayOfMonth = date.
day();
278 nthFromStart = (dayOfMonth - 1) / 7 + 1;
279 nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1;
281 if (++i >= trcount) {
285 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
286 || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
289 transitionsDone[i] =
true;
290 qdt = transits.at(i).atUtc;
297 if (qdt.
time() != time || date.
month() != month || date.
year() != ++year) {
300 const int day = date.
day();
301 if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
302 newRule &= ~DAY_OF_MONTH;
304 if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
306 newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
308 if ((newRule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) {
309 newRule &= ~WEEKDAY_OF_MONTH;
311 if ((newRule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd) {
312 newRule &= ~LAST_WEEKDAY_OF_MONTH;
322 int yr = times[0].date().year();
326 if (qdt.
time() != time || date.
month() != month || date.
year() != --yr) {
329 const int day = date.
day();
330 if (
rule & DAY_OF_MONTH) {
331 if (day != dayOfMonth) {
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)) {
343 if (times.
count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
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);
353 }
else if (
rule & LAST_WEEKDAY_OF_MONTH) {
354 r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8);
356 r.until = writeLocalICalDateTime(times.
takeAt(times.
size() - 1), preOffset);
357 icalproperty *prop = icalproperty_new_rrule(r);
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);
366 icalcomponent_add_property(phaseComp1, prop);
370 for (
int t = 0, tend = times.
count() - 1; t < tend; ++t) {
382 }
while (i < trcount);
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));
389 icalcomponent_add_component(tzcomp, phaseComp1);
390 icalcomponent_free(phaseComp);
396 icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(
const QTimeZone &tz,
const QDateTime &earliest)
398 auto itz = icaltimezone_new();
399 icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest));
403 void ICalTimeZoneParser::parse(icalcomponent *calendar)
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);
409 if (!icalZone.id.isEmpty()) {
410 if (!icalZone.qZone.isValid()) {
411 icalZone.qZone = resolveICalTimeZone(icalZone);
413 if (!icalZone.qZone.isValid()) {
414 qCWarning(KCALCORE_LOG) <<
"Failed to map" << icalZone.id <<
"to a known IANA timezone";
417 mCache->insert(icalZone.id, icalZone);
422 QTimeZone ICalTimeZoneParser::resolveICalTimeZone(
const ICalTimeZone &icalZone)
424 const auto phase = icalZone.standard;
429 for (
const auto &tzid : candidates) {
432 if (candidate.hasTransitions() == phase.transitions.isEmpty()) {
433 matchedCandidates.
insert(0, candidate);
439 if (!candidate.hasTransitions() && phase.transitions.isEmpty()) {
445 auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20));
447 if (begin == phase.transitions.cend()) {
448 begin = phase.transitions.cbegin();
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;
455 if (candidateTransitions.isEmpty()) {
458 ++matchedTransitions;
459 const auto candidateTransition = candidateTransitions[0];
462 const auto abvs = phase.abbrevs;
463 for (
const auto &abv : abvs) {
465 matchedTransitions += 1024;
470 matchedCandidates.
insert(matchedTransitions, candidate);
473 if (!matchedCandidates.
isEmpty()) {
474 return matchedCandidates.
value(matchedCandidates.
lastKey());
480 ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone)
484 if (
auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) {
485 icalTz.id = icalproperty_get_value_as_string(tzidProp);
495 if (!ianaTzid.isEmpty()) {
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);
506 case ICAL_XSTANDARD_COMPONENT:
508 parsePhase(c,
false, icalTz.standard);
510 case ICAL_XDAYLIGHT_COMPONENT:
512 parsePhase(c,
true, icalTz.daylight);
516 qCDebug(KCALCORE_LOG) <<
"Unknown component:" << int(kind);
524 bool ICalTimeZoneParser::parsePhase(icalcomponent *c,
bool daylight, ICalTimeZonePhase &phase)
530 bool found_dtstart =
false;
531 bool found_tzoffsetfrom =
false;
532 bool found_tzoffsetto =
false;
533 icaltimetype dtstart = icaltime_null_time();
537 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
539 icalproperty_kind kind = icalproperty_isa(p);
541 case ICAL_TZNAME_PROPERTY: {
549 if ((!daylight && name ==
"Standard Time") || (daylight && name ==
"Daylight Time")) {
555 case ICAL_DTSTART_PROPERTY:
556 dtstart = icalproperty_get_dtstart(p);
557 found_dtstart =
true;
560 case ICAL_TZOFFSETFROM_PROPERTY:
561 prevOffset = icalproperty_get_tzoffsetfrom(p);
562 found_tzoffsetfrom =
true;
565 case ICAL_TZOFFSETTO_PROPERTY:
566 utcOffset = icalproperty_get_tzoffsetto(p);
567 found_tzoffsetto =
true;
570 case ICAL_RDATE_PROPERTY:
571 case ICAL_RRULE_PROPERTY:
578 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
582 if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
583 qCDebug(KCALCORE_LOG) <<
"DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
588 dtstart.second -= prevOffset;
589 dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone());
590 const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart));
592 phase.abbrevs.unite(abbrevs);
593 phase.utcOffset = utcOffset;
594 phase.transitions += utcStart;
604 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
606 icalproperty_kind kind = icalproperty_isa(p);
608 case ICAL_RDATE_PROPERTY: {
609 icaltimetype t = icalproperty_get_rdate(p).time;
610 if (icaltime_is_date(t)) {
612 t.hour = dtstart.hour;
613 t.minute = dtstart.minute;
614 t.second = dtstart.second;
619 if (!icaltime_is_utc(t)) {
620 t.second -= prevOffset;
621 t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone());
622 t = icaltime_normalize(t);
624 phase.transitions += toQDateTime(t);
627 case ICAL_RRULE_PROPERTY: {
630 ICalFormatImpl impl(&icf);
631 impl.readRecurrence(icalproperty_get_rrule(p), &r);
636 qCWarning(KCALCORE_LOG) <<
"UNTIL in RRULE must be specified in UTC";
640 for (
int i = 0, end = dts.count(); i < end; ++i) {
641 phase.transitions += dts[i];
648 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
650 sortAndRemoveDuplicates(phase.transitions);
658 auto icalTz = icalcomponentFromQTimeZone(qtz, earliest);
659 const QByteArray result(icalcomponent_as_ical_string(icalTz));
660 icalmemory_free_ring();
661 icalcomponent_free(icalTz);