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"
23#include <libical/ical.h>
24#include <libical/icaltimezone.h>
30static const int minRuleCount = 5;
31static const int minPhaseCount = 8;
34static QDateTime toQDateTime(
const icaltimetype &t)
51static icaltimetype writeLocalICalDateTime(
const QDateTime &utc,
int offset)
54 icaltimetype t = icaltime_null_time();
68void ICalTimeZonePhase::dump()
70 qDebug() <<
" ~~~ ICalTimeZonePhase ~~~";
71 qDebug() <<
" Abbreviations:" << abbrevs;
72 qDebug() <<
" UTC offset:" << utcOffset;
73 qDebug() <<
" Transitions:" << transitions;
74 qDebug() <<
" ~~~~~~~~~~~~~~~~~~~~~~~~~";
77void ICalTimeZone::dump()
79 qDebug() <<
"~~~ ICalTimeZone ~~~";
80 qDebug() <<
"ID:" << id;
81 qDebug() <<
"QZONE:" << qZone.id();
86 qDebug() <<
"~~~~~~~~~~~~~~~~~~~~";
89ICalTimeZoneCache::ICalTimeZoneCache()
93void ICalTimeZoneCache::insert(
const QByteArray &
id,
const ICalTimeZone &tz)
95 mCache.insert(
id, tz);
101typename T::const_iterator greatestSmallerThan(
const T &c,
const typename T::value_type &v)
103 auto it = std::lower_bound(c.cbegin(), c.cend(), v);
104 if (it != c.cbegin()) {
118 const ICalTimeZone tz = mCache.value(tzid);
119 if (!tz.qZone.isValid()) {
128 if (tz.qZone.id().startsWith(
"UTC")) {
130 const auto stdPrev = greatestSmallerThan(tz.standard.transitions, dt);
131 const auto dstPrev = greatestSmallerThan(tz.daylight.transitions, dt);
132 if (stdPrev != tz.standard.transitions.cend() && dstPrev != tz.daylight.transitions.cend()) {
133 if (*dstPrev > *stdPrev) {
137 auto dtsTzId = std::find_if(tzids.cbegin(), tzids.cend(), [](
const QByteArray &
id) {
138 return id.startsWith(
"UTC");
140 if (dtsTzId != tzids.cend()) {
150ICalTimeZoneParser::ICalTimeZoneParser(ICalTimeZoneCache *cache)
155void ICalTimeZoneParser::updateTzEarliestDate(
const IncidenceBase::Ptr &incidence, TimeZoneEarliestDate *earliest)
158 const auto dt =
incidence->dateTime(role);
163 const auto prev = earliest->value(
incidence->dtStart().timeZone());
164 if (!prev.isValid() ||
incidence->dtStart() < prev) {
165 earliest->insert(
incidence->dtStart().timeZone(), prev);
171icalcomponent *ICalTimeZoneParser::icalcomponentFromQTimeZone(
const QTimeZone &tz,
const QDateTime &earliest)
176 WEEKDAY_OF_MONTH = 0x02,
177 LAST_WEEKDAY_OF_MONTH = 0x04,
181 icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
182 icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.
id().
constData()));
188 if (transits.isEmpty()) {
192 qCDebug(KCALCORE_LOG) <<
"No transition information available VTIMEZONE will be invalid.";
196 for (
int i = 0, end = transits.count(); i < end; ++i) {
197 if (transits.at(i).atUtc >= earliest) {
199 transits.erase(transits.begin(), transits.begin() + i);
205 int trcount = transits.count();
210 icaldatetimeperiodtype dtperiod;
211 dtperiod.period = icalperiodtype_null_period();
214 for (; i < trcount && transitionsDone[i]; ++i) {
221 const int preOffset = (i > 0) ? transits.at(i - 1).offsetFromUtc : 0;
222 const auto &transit = transits.at(i);
223 if (transit.offsetFromUtc == preOffset) {
224 transitionsDone[i] =
true;
225 while (++i < trcount) {
226 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
227 || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
230 transitionsDone[i] =
true;
234 const bool isDst = transit.daylightTimeOffset > 0;
235 icalcomponent *phaseComp = icalcomponent_new(isDst ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT);
236 if (!transit.abbreviation.isEmpty()) {
237 icalcomponent_add_property(phaseComp, icalproperty_new_tzname(
static_cast<const char *
>(transit.abbreviation.toUtf8().constData())));
239 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset));
240 icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(transit.offsetFromUtc));
242 icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp);
243 icalcomponent_add_property(phaseComp1, icalproperty_new_dtstart(writeLocalICalDateTime(transits.at(i).atUtc, preOffset)));
244 bool useNewRRULE =
false;
255 int nthFromStart = 0;
263 transitionsDone[i] =
true;
267 rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
271 month = date.
month();
274 dayOfMonth = date.
day();
275 nthFromStart = (dayOfMonth - 1) / 7 + 1;
276 nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1;
278 if (++i >= trcount) {
282 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
283 || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
286 transitionsDone[i] =
true;
287 qdt = transits.at(i).atUtc;
294 if (qdt.
time() != time || date.
month() != month || date.
year() != ++year) {
297 const int day = date.
day();
298 if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
299 newRule &= ~DAY_OF_MONTH;
301 if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
303 newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
305 if ((newRule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) {
306 newRule &= ~WEEKDAY_OF_MONTH;
308 if ((newRule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd) {
309 newRule &= ~LAST_WEEKDAY_OF_MONTH;
319 int yr = times[0].date().year();
323 if (qdt.
time() != time || date.
month() != month || date.
year() != --yr) {
326 const int day = date.
day();
327 if (rule & DAY_OF_MONTH) {
328 if (day != dayOfMonth) {
332 if (date.
dayOfWeek() != dayOfWeek || ((rule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart)
333 || ((rule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd)) {
340 if (times.
count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
342 icalrecurrencetype r;
343 icalrecurrencetype_clear(&r);
344 r.freq = ICAL_YEARLY_RECURRENCE;
345 r.by_month[0] = month;
346 if (rule & DAY_OF_MONTH) {
347 r.by_month_day[0] = dayOfMonth;
348 }
else if (rule & WEEKDAY_OF_MONTH) {
349 r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8);
350 }
else if (rule & LAST_WEEKDAY_OF_MONTH) {
351 r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8);
353 r.until = writeLocalICalDateTime(times.
takeAt(times.
size() - 1), preOffset);
354 icalproperty *prop = icalproperty_new_rrule(r);
358 icalcomponent *c = icalcomponent_new_clone(phaseComp);
359 icalcomponent_add_property(c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset)));
360 icalcomponent_add_property(c, prop);
361 icalcomponent_add_component(tzcomp, c);
363 icalcomponent_add_property(phaseComp1, prop);
367 for (
int t = 0, tend = times.
count() - 1; t < tend; ++t) {
379 }
while (i < trcount);
382 for (
int rd = 0, rdend = rdates.
count(); rd < rdend; ++rd) {
383 dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset);
384 icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod));
386 icalcomponent_add_component(tzcomp, phaseComp1);
387 icalcomponent_free(phaseComp);
393icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(
const QTimeZone &tz,
const QDateTime &earliest)
395 auto itz = icaltimezone_new();
396 icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest));
400void ICalTimeZoneParser::parse(icalcomponent *calendar)
402 for (
auto *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT); c;
403 c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) {
404 auto icalZone = parseTimeZone(c);
406 if (!icalZone.id.isEmpty()) {
407 if (!icalZone.qZone.isValid()) {
408 icalZone.qZone = resolveICalTimeZone(icalZone);
410 if (!icalZone.qZone.isValid()) {
411 qCWarning(KCALCORE_LOG) <<
"Failed to map" << icalZone.id <<
"to a known IANA timezone";
414 mCache->insert(icalZone.id, icalZone);
419QTimeZone ICalTimeZoneParser::resolveICalTimeZone(
const ICalTimeZone &icalZone)
421 const auto phase = icalZone.standard;
426 for (
const auto &tzid : candidates) {
429 if (candidate.hasTransitions() == phase.transitions.isEmpty()) {
430 matchedCandidates.
insert(0, candidate);
436 if (!candidate.hasTransitions() && phase.transitions.isEmpty()) {
442 auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20));
444 if (begin == phase.transitions.cend()) {
445 begin = phase.transitions.cbegin();
447 auto end = std::upper_bound(begin, phase.transitions.cend(), now);
448 int matchedTransitions = 0;
449 for (
auto it = begin; it !=
end; ++it) {
450 const auto &transition = *it;
452 if (candidateTransitions.isEmpty()) {
455 ++matchedTransitions;
456 const auto candidateTransition = candidateTransitions[0];
459 const auto abvs = phase.abbrevs;
460 for (
const auto &abv : abvs) {
462 matchedTransitions += 1024;
467 matchedCandidates.
insert(matchedTransitions, candidate);
470 if (!matchedCandidates.
isEmpty()) {
471 return matchedCandidates.
value(matchedCandidates.
lastKey());
477ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone)
481 if (
auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) {
482 icalTz.id = icalproperty_get_value_as_string(tzidProp);
492 if (!ianaTzid.isEmpty()) {
499 for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT); c;
500 c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) {
501 icalcomponent_kind kind = icalcomponent_isa(c);
503 case ICAL_XSTANDARD_COMPONENT:
505 parsePhase(c,
false, icalTz.standard);
507 case ICAL_XDAYLIGHT_COMPONENT:
509 parsePhase(c,
true, icalTz.daylight);
513 qCDebug(KCALCORE_LOG) <<
"Unknown component:" << int(kind);
521bool ICalTimeZoneParser::parsePhase(icalcomponent *c,
bool daylight, ICalTimeZonePhase &phase)
527 bool found_dtstart =
false;
528 bool found_tzoffsetfrom =
false;
529 bool found_tzoffsetto =
false;
530 icaltimetype dtstart = icaltime_null_time();
534 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
536 icalproperty_kind kind = icalproperty_isa(p);
538 case ICAL_TZNAME_PROPERTY: {
546 if ((!daylight && name ==
"Standard Time") || (daylight && name ==
"Daylight Time")) {
552 case ICAL_DTSTART_PROPERTY:
553 dtstart = icalproperty_get_dtstart(p);
554 found_dtstart =
true;
557 case ICAL_TZOFFSETFROM_PROPERTY:
558 prevOffset = icalproperty_get_tzoffsetfrom(p);
559 found_tzoffsetfrom =
true;
562 case ICAL_TZOFFSETTO_PROPERTY:
563 utcOffset = icalproperty_get_tzoffsetto(p);
564 found_tzoffsetto =
true;
567 case ICAL_RDATE_PROPERTY:
568 case ICAL_RRULE_PROPERTY:
575 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
579 if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
580 qCDebug(KCALCORE_LOG) <<
"DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
585 dtstart.second -= prevOffset;
586 dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone());
587 const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart));
589 phase.abbrevs.unite(abbrevs);
590 phase.utcOffset = utcOffset;
591 phase.transitions += utcStart;
601 icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
603 icalproperty_kind kind = icalproperty_isa(p);
605 case ICAL_RDATE_PROPERTY: {
606 icaltimetype t = icalproperty_get_rdate(p).time;
607 if (icaltime_is_date(t)) {
609 t.hour = dtstart.hour;
610 t.minute = dtstart.minute;
611 t.second = dtstart.second;
616 if (!icaltime_is_utc(t)) {
617 t.second -= prevOffset;
618 t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone());
619 t = icaltime_normalize(t);
621 phase.transitions += toQDateTime(t);
624 case ICAL_RRULE_PROPERTY: {
627 ICalFormatImpl impl(&icf);
628 impl.readRecurrence(icalproperty_get_rrule(p), &r);
633 qCWarning(KCALCORE_LOG) <<
"UNTIL in RRULE must be specified in UTC";
637 for (
int i = 0, end = dts.count(); i < end; ++i) {
638 phase.transitions += dts[i];
645 p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
647 sortAndRemoveDuplicates(phase.transitions);
655 auto icalTz = icalcomponentFromQTimeZone(qtz, earliest);
656 const QByteArray result(icalcomponent_as_ical_string(icalTz));
657 icalmemory_free_ring();
658 icalcomponent_free(icalTz);
@ RoleEndTimeZone
Role for determining an incidence's ending timezone.
@ RoleStartTimeZone
Role for determining an incidence's starting timezone.
This class represents a recurrence rule for a calendar incidence.
QDateTime endDt(bool *result=nullptr) const
Returns the date and time of the last recurrence.
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.
void setStartDt(const QDateTime &start)
Sets the recurrence start date/time.
This class represents a recurrence rule for a calendar incidence.
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
Namespace for all KCalendarCore types.
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
const QList< QKeySequence > & begin()
const char * constData() const const
int dayOfWeek() const const
int daysInMonth() const const
QDateTime addSecs(qint64 s) const const
QDateTime currentDateTimeUtc()
bool isValid() const const
QTimeZone timeZone() const const
qsizetype count() const const
bool isEmpty() const const
void prepend(parameter_type value)
qsizetype size() const const
iterator insert(const Key &key, const T &value)
bool isEmpty() const const
const Key & lastKey() const const
T value(const Key &key, const T &defaultValue) const const
iterator insert(const T &value)
QString fromUtf8(QByteArrayView str)
QList< QByteArray > availableTimeZoneIds()
QByteArray id() const const
bool isTimeZoneIdAvailable(const QByteArray &ianaId)
OffsetDataList transitions(const QDateTime &fromDateTime, const QDateTime &toDateTime) const const
QByteArray windowsIdToDefaultIanaId(const QByteArray &windowsId)