private async Task UpdateExceptionsAsync(string id, IAppointmentSchedule schedule, bool force)
        {
            var exceptions = schedule.Recurrence.Exceptions.Where(kv => kv.Value != null);

            foreach (var kv in exceptions)
            {
                using (Logger.Scope($"GoogleCalendar.UpdateException({kv.Key.Date})"))
                {
                    EventsResource.InstancesRequest instancesRequest = _service.Events.Instances(CalendarId, id);
                    var originalStart = new DateTime(kv.Key.Year, kv.Key.Month, kv.Key.Day,
                                                     schedule.Start.Hour, schedule.Start.Minute, schedule.Start.Second);
                    TimeSpan utcOffset    = schedule.StartTimeZone.GetUtcOffset(originalStart);
                    string   offsetFormat = (utcOffset < TimeSpan.Zero ? "\\-" : "") + "hh\\:mm";
                    instancesRequest.OriginalStart = $"{originalStart:yyyy-MM-ddTHH:mm:ss}{utcOffset.ToString(offsetFormat)}";
                    Events instances = await instancesRequest.ExecuteAsync().ConfigureAwait(false);

                    Event instance = instances.Items.FirstOrDefault();
                    if (instance == null)
                    {
                        Logger.Warning($"Failed to find instance of \"{kv.Value}\" at {instancesRequest.OriginalStart} to create an exception");
                        continue;
                    }

                    if (force || kv.Value.LastModifiedDateTime > (instance.Updated ?? DateTime.MinValue))
                    {
                        await UpdateAppointmentAsync(instance.Id, instance.Sequence, kv.Value, force).ConfigureAwait(false);
                    }
                }
            }
        }
        public static IEnumerable <string> ToRfc5545Rules(this IAppointmentSchedule schedule)
        {
            var recurrence = schedule.Recurrence;

            if (recurrence == null)
            {
                yield break;
            }

            var sb = new StringBuilder();

            sb.Append("RRULE:FREQ=");
            switch (recurrence.Frequency)
            {
            case RecurrenceFrequency.Daily:
                sb.Append("DAILY");
                break;

            case RecurrenceFrequency.Weekly:
                sb.Append("WEEKLY");
                break;

            case RecurrenceFrequency.Monthly:
                sb.Append("MONTHLY");
                break;

            case RecurrenceFrequency.Yearly:
                sb.Append("YEARLY");
                break;
            }
            if (recurrence.Count.HasValue)
            {
                sb.Append($";COUNT={recurrence.Count.Value}");
            }
            if (recurrence.Interval.HasValue)
            {
                sb.Append($";INTERVAL={recurrence.Interval.Value}");
            }
            if (recurrence.Until.HasValue)
            {
                sb.Append($";UNTIL={recurrence.Until.Value.Date:yyyyMMddT000000Z}");                 // TODO: is UTC okay?
            }
            if (recurrence.WeekDay.HasValue)
            {
                sb.Append($";BYDAY={ToRfc5545String(recurrence.WeekDay.Value)}");
            }
            if (recurrence.YearMonth.HasValue)
            {
                sb.Append($";BYMONTH={recurrence.YearMonth.Value}");
            }
            if (recurrence.MonthDay.HasValue)
            {
                sb.Append($";BYMONTHDAY={recurrence.MonthDay.Value}");
            }
            if (recurrence.Exceptions != null)
            {
                string startTime     = schedule.Start.ToString("HHmmss");
                string startTimezone = TZConvert.WindowsToIana(schedule.StartTimeZone.Id);
                foreach (DateTime exceptionDate in recurrence.Exceptions.Where(kv => kv.Value == null).Select(kv => kv.Key))
                {
                    yield return($"EXDATE;TZID={startTimezone}:{exceptionDate:yyyyMMdd}T{startTime}");
                }
            }

            yield return(sb.ToString());
        }