/* * * (c) Copyright Ascensio System Limited 2010-2018 * * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. * * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html * * You can contact Ascensio System SIA by email at sales@onlyoffice.com * * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. * * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains * relevant author attributions when distributing the software. If the display of the logo in its graphic * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" * in every copy of the program you distribute. * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. * */ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; namespace ASC.Web.Core.Calendars { internal static class DateTimeExtension { public static int GetWeekOfYear(this DateTime date, DayOfWeek firstDayOfWeek) { return System.Threading.Thread.CurrentThread.CurrentCulture.Calendar.GetWeekOfYear(date, System.Globalization.CalendarWeekRule.FirstFourDayWeek, firstDayOfWeek); } public static int GetWeekOfYearCount(this DateTime date, DayOfWeek firstDayOfWeek) { return new DateTime(date.Year, 12, 31).GetWeekOfYear(firstDayOfWeek); } public static int GetDaysInMonth(this DateTime date) { return System.Threading.Thread.CurrentThread.CurrentCulture.Calendar.GetDaysInMonth(date.Year, date.Month); } public static int GetDaysInYear(this DateTime date) { return System.Threading.Thread.CurrentThread.CurrentCulture.Calendar.GetDaysInYear(date.Year); } public static int GetDayOfWeekInMonth(this DateTime date) { var count = 0; var d = date; while (date.Month == d.Month) { count++; d = d.AddDays(-7); } d = date.AddDays(7); while (date.Month == d.Month) { count++; d = d.AddDays(7); } return count; } } public enum Frequency { Never = 0, Daily = 3, Weekly = 4, Monthly = 5, Yearly = 6, Secondly = 7, Minutely = 8, Hourly = 9 } public class RecurrenceRule : IiCalFormatView, ICloneable { public static RecurrenceRule Parse(EventRepeatType repeatType) { return repeatType switch { EventRepeatType.EveryDay => new RecurrenceRule() { Freq = Frequency.Daily }, EventRepeatType.EveryMonth => new RecurrenceRule() { Freq = Frequency.Monthly }, EventRepeatType.EveryWeek => new RecurrenceRule() { Freq = Frequency.Weekly }, EventRepeatType.EveryYear => new RecurrenceRule() { Freq = Frequency.Yearly }, EventRepeatType.Never => new RecurrenceRule() { Freq = Frequency.Never }, _ => new RecurrenceRule() { Freq = Frequency.Never }, }; } public class WeekDay { public string Id { get; set; } public int? Number { get; set; } public DayOfWeek DayOfWeek { get { return ((Id ?? "").ToLower()) switch { "su" => DayOfWeek.Sunday, "mo" => DayOfWeek.Monday, "tu" => DayOfWeek.Tuesday, "we" => DayOfWeek.Wednesday, "th" => DayOfWeek.Thursday, "fr" => DayOfWeek.Friday, "sa" => DayOfWeek.Saturday, _ => DayOfWeek.Monday, }; } } private WeekDay() { } public List GetDates(DateTime startDate, bool monthly) { var dates = new List(); var date = startDate; var count = 0; while ((monthly && date.Month == startDate.Month) || (!monthly && date.Year == startDate.Year)) { if (date.DayOfWeek == this.DayOfWeek) { count++; if (!monthly && (!this.Number.HasValue || count == this.Number || this.Number == (count - (date.GetWeekOfYearCount(DayOfWeek.Monday) + 1)))) dates.Add(date); else if (monthly && (!this.Number.HasValue || count == this.Number || this.Number == (count - (date.GetDayOfWeekInMonth() + 1)))) dates.Add(date); date = date.AddDays(7); } else date = date.AddDays(1); } return dates; } public static WeekDay Parse(string iCalStrValue) { var d = new WeekDay(); if (iCalStrValue.Length > 2) { d.Id = iCalStrValue.Substring(iCalStrValue.Length - 2).ToLower(); d.Number = Convert.ToInt32(iCalStrValue[0..^2]); } else { d.Id = iCalStrValue; d.Number = null; } return d; } public override string ToString() { return (Number.HasValue ? Number.ToString() : "") + this.Id; } } public Frequency Freq { get; set; } public DateTime Until { get; set; } public int Count { get; set; } public int Interval { get; set; } public int[] BySecond { get; set; } //0 - 59 public int[] ByMinute { get; set; } //0 - 59 public int[] ByHour { get; set; } //0 - 23 public WeekDay[] ByDay { get; set; } // public int[] ByMonthDay { get; set; } //1 -31 +- public int[] ByYearDay { get; set; } //1 -366 +- public int[] ByWeekNo { get; set; } // 1 - 53 +- public int[] ByMonth { get; set; } // 1 - 12 public int[] BySetPos { get; set; } //- 366 +- public WeekDay WKST { get; set; } public struct ExDate { public DateTime Date { get; set; } public bool isDateTime { get; set; } } public List ExDates { get; set; } public RecurrenceRule() { this.Freq = Frequency.Never; this.Until = DateTime.MinValue; this.Count = -1; this.Interval = 1; this.WKST = WeekDay.Parse("mo"); this.ExDates = new List(); } private bool CheckDate(DateTime d) { if (ByMonth != null && !ByMonth.Contains(d.Month)) return false; //only for YEARLY if (ByWeekNo != null) { var weekOfYear = d.GetWeekOfYear(this.WKST.DayOfWeek); if (!ByWeekNo.Contains(weekOfYear) && !ByWeekNo.Contains(weekOfYear - (d.GetWeekOfYearCount(this.WKST.DayOfWeek) + 1))) return false; } if (ByYearDay != null && !ByYearDay.Contains(d.DayOfYear) && !ByYearDay.Contains(d.DayOfYear - (d.GetDaysInYear() + 1))) return false; if (ByMonthDay != null && !ByMonthDay.Contains(d.Day) && !ByMonthDay.Contains(d.Day - d.GetDaysInMonth() + 1)) return false; if (ByDay != null && !ByDay.ToList().Exists(item => item.DayOfWeek == d.DayOfWeek)) return false; return true; } public List GetDates(DateTime utcStartDate, DateTime fromDate, int maxCount) { return GetDates(utcStartDate, fromDate, DateTime.MaxValue, maxCount); } public List GetDates(DateTime utcStartDate, DateTime fromDate, DateTime toDate) { return GetDates(utcStartDate, fromDate, toDate, int.MaxValue); } public List GetDates(DateTime utcStartDate, DateTime fromDate, DateTime toDate, int maxCount, bool removeExDates = true) { var dates = new List(); var endDate = (this.Until == DateTime.MinValue ? toDate : (toDate > this.Until ? this.Until : toDate)); //push start date dates.Add(utcStartDate); DateTime d; switch (Freq) { case Frequency.Secondly: #region Secondly d = utcStartDate.AddSeconds(this.Interval); while (d <= endDate && CheckCount(dates, fromDate, maxCount)) { if (CheckDate(d) && (ByHour == null || (ByHour != null && ByHour.Contains(d.Hour))) && (ByMinute == null || (ByMinute != null && ByMinute.Contains(d.Minute))) && (BySecond == null || (BySecond != null && BySecond.Contains(d.Second)))) { if (d >= utcStartDate && d <= endDate && !dates.Contains(d)) dates.Add(d); } d = d.AddMinutes(this.Interval); } break; #endregion case Frequency.Minutely: #region Minutely d = utcStartDate.AddMinutes(this.Interval); while (d <= endDate && CheckCount(dates, fromDate, maxCount)) { if (CheckDate(d) && (ByHour == null || (ByHour != null && ByHour.Contains(d.Hour))) && (ByMinute == null || (ByMinute != null && ByMinute.Contains(d.Minute)))) { //seconds var seconds = new List(); if (BySecond != null) seconds.AddRange(BySecond); else seconds.Add(d.Second); foreach (var s in seconds) { var newDate = new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, s); if (newDate >= utcStartDate && newDate <= endDate && !dates.Contains(newDate)) dates.Add(newDate); } } d = d.AddMinutes(this.Interval); } break; #endregion case Frequency.Hourly: #region Hourly d = utcStartDate.AddHours(this.Interval); while (d <= endDate && CheckCount(dates, fromDate, maxCount)) { if (CheckDate(d) && (ByHour == null || (ByHour != null && ByHour.Contains(d.Hour)))) { //minutes var minutes = new List(); if (ByMinute != null) minutes.AddRange(ByMinute); else minutes.Add(d.Minute); //seconds var seconds = new List(); if (BySecond != null) seconds.AddRange(BySecond); else seconds.Add(d.Second); foreach (var m in minutes) { foreach (var s in seconds) { var newDate = new DateTime(d.Year, d.Month, d.Day, d.Hour, m, s); if (newDate >= utcStartDate && newDate <= endDate && !dates.Contains(newDate)) dates.Add(newDate); } } } d = d.AddHours(this.Interval); } break; #endregion case Frequency.Daily: #region Daily d = utcStartDate.AddDays(this.Interval); while (d <= endDate && CheckCount(dates, fromDate, maxCount)) { if (CheckDate(d)) GetDatesWithTime(ref dates, utcStartDate, endDate, d, new List() { d }); d = d.AddDays(this.Interval); } break; #endregion case Frequency.Weekly: #region Weekly d = utcStartDate; while (d <= endDate && CheckCount(dates, fromDate, maxCount)) { var dateRange = new List(); for (var i = 0; i < 7; i++) dateRange.Add(d.AddDays(i)); if (ByMonth != null) dateRange.RemoveAll(date => !ByMonth.Contains(date.Month)); if (ByYearDay != null) dateRange.RemoveAll(date => (!ByYearDay.Contains(date.DayOfYear) && !ByYearDay.Contains(date.DayOfYear - (date.GetDaysInYear() + 1)))); if (ByMonthDay != null) dateRange.RemoveAll(date => (!ByMonthDay.Contains(date.Day) && !ByMonthDay.Contains(date.Day - (date.GetDaysInMonth() + 1)))); if (ByDay != null) dateRange.RemoveAll(date => !ByDay.ToList().Exists(wd => wd.DayOfWeek == date.DayOfWeek)); if (ByDay == null && ByMonthDay == null && ByYearDay == null) dateRange.RemoveAll(date => date.Day != d.Day); GetDatesWithTime(ref dates, utcStartDate, endDate, d, dateRange); d = d.AddDays(7 * this.Interval); } break; #endregion case Frequency.Monthly: #region Monthly d = utcStartDate; while (d <= endDate && CheckCount(dates, fromDate, maxCount)) { var dateRange = new List(); if (ByMonth != null && !ByMonth.Contains(d.Month)) { d = d.AddMonths(this.Interval); continue; } var day = new DateTime(d.Year, d.Month, 1); while (day.Month == d.Month) { dateRange.Add(day); day = day.AddDays(1); } if (ByYearDay != null) dateRange.RemoveAll(date => (!ByYearDay.Contains(date.DayOfYear) && !ByYearDay.Contains(date.DayOfYear - (date.GetDaysInYear() + 1)))); if (ByMonthDay != null) dateRange.RemoveAll(date => (!ByMonthDay.Contains(date.Day) && !ByMonthDay.Contains(date.Day - (date.GetDaysInMonth() + 1)))); //only for MONTHLY or YEARLY if (ByDay != null) { var listDates = new List(); foreach (var date in ByDay) listDates.AddRange(date.GetDates(new DateTime(d.Year, d.Month, 1), true)); dateRange.RemoveAll(date => !listDates.Contains(date)); } if (ByDay == null && ByMonthDay == null && ByYearDay == null) dateRange.RemoveAll(date => date.Day != d.Day); GetDatesWithTime(ref dates, utcStartDate, endDate, d, dateRange); d = d.AddMonths(this.Interval); } break; #endregion case Frequency.Yearly: #region Yearly d = utcStartDate; if (d.Month == 2 && d.Day == 29) { if (Interval == 1 && ByMonth == null && ByWeekNo == null && ByYearDay == null && ByMonthDay == null && ByDay == null) { Interval = 4; } } while (d.Year <= endDate.Year && CheckCount(dates, fromDate, maxCount)) { var dateRange = new List(); var isFirst = true; if (ByMonth != null) { foreach (var m in ByMonth) { var date = new DateTime(d.Year, m, 1); while (date.Month == m) { dateRange.Add(date); date = date.AddDays(1); } } isFirst = false; } //only for YEARLY if (ByWeekNo != null) { if (isFirst) { var date = new DateTime(d.Year, 1, 1); while (date.Year == d.Year) { var weekOfYear = date.GetWeekOfYear(this.WKST.DayOfWeek); if (ByWeekNo.Contains(weekOfYear) || ByWeekNo.Contains(weekOfYear - (date.GetWeekOfYearCount(this.WKST.DayOfWeek) + 1))) dateRange.Add(date); date = date.AddDays(1); } } else { dateRange.RemoveAll(date => { var weekOfYear = date.GetWeekOfYear(this.WKST.DayOfWeek); return ((!ByWeekNo.Contains(weekOfYear) && !ByWeekNo.Contains(weekOfYear - (date.GetWeekOfYearCount(this.WKST.DayOfWeek) + 1)))); }); } isFirst = false; } if (ByYearDay != null) { if (isFirst) { foreach (var yearDay in ByYearDay) dateRange.Add(new DateTime(d.Year, 1, 1).AddDays((yearDay > 0 ? yearDay : (d.GetDaysInYear() + yearDay)) - 1)); } else dateRange.RemoveAll(date => (!ByYearDay.Contains(date.DayOfYear) && !ByYearDay.Contains(date.DayOfYear - (date.GetDaysInYear() + 1)))); isFirst = false; } if (ByMonthDay != null) { if (isFirst) { for (var m = 1; m <= 12; m++) { foreach (var day in ByMonthDay) { var dd = new DateTime(d.Year, m, 1); dateRange.Add(dd.AddDays((day > 0 ? day : (dd.GetDaysInMonth() + day)) - 1)); } } } else dateRange.RemoveAll(date => (!ByMonthDay.Contains(date.Day) && !ByMonthDay.Contains(date.Day - (date.GetDaysInMonth() + 1)))); isFirst = false; } //only for MONTHLY or YEARLY if (ByDay != null) { var listDates = new List(); foreach (var day in ByDay) listDates.AddRange(day.GetDates(new DateTime(d.Year, 1, 1), false)); listDates.Sort(); if (isFirst) dateRange.AddRange(listDates); else dateRange.RemoveAll(date => !listDates.Contains(date)); isFirst = false; } if (ByDay == null && ByMonthDay == null && ByYearDay == null && ByWeekNo == null) dateRange.RemoveAll(date => date.Day != d.Day); //add yearly same date if (isFirst) dateRange.Add(d); GetDatesWithTime(ref dates, utcStartDate, endDate, d, dateRange); d = d.AddYears(this.Interval); } break; #endregion } if (Count >= 0) { var count = this.Count; dates = dates.FindAll(date => (--count) >= 0); } if (removeExDates && ExDates != null) { foreach (var exDate in ExDates) dates.RemoveAll(dt => (exDate.isDateTime && dt == exDate.Date) || (!exDate.isDateTime && dt.Date == exDate.Date)); } dates.RemoveAll(dt => dt < fromDate || dt > endDate); return dates; } private bool CheckCount(List dates, DateTime fromDate, int maxCount) { if (Count >= 0) return dates.Count <= this.Count; if (maxCount != int.MaxValue) { if (ExDates != null && ExDates.Count > 0) { return dates.FindAll(dt => dt >= fromDate && !ExDates.Exists(exDate => (exDate.isDateTime && dt == exDate.Date) || (!exDate.isDateTime && dt.Date == exDate.Date))) .Count < maxCount; } else return dates.FindAll(d => d >= fromDate).Count < maxCount; } return true; } private void GetDatesWithTime(ref List dates, DateTime startDate, DateTime endDate, DateTime d, List dateRange) { //hours var hours = new List(); if (ByHour != null) hours.AddRange(ByHour); else hours.Add(d.Hour); //minutes var minutes = new List(); if (ByMinute != null) minutes.AddRange(ByMinute); else minutes.Add(d.Minute); //seconds var seconds = new List(); if (BySecond != null) seconds.AddRange(BySecond); else seconds.Add(d.Second); if (BySetPos != null && (ByDay != null || ByMonth != null || ByMonthDay != null || ByWeekNo != null || ByYearDay != null)) { var newDateRange = new List(); foreach (var pos in BySetPos) { if (pos >= 1 && pos <= dateRange.Count) newDateRange.Add(dateRange[pos - 1]); else if (pos < 0 && (pos * (-1)) <= dateRange.Count) newDateRange.Add(dateRange[dateRange.Count + pos]); } newDateRange.Sort(); dateRange = newDateRange; } foreach (var date in dateRange) { foreach (var h in hours) { foreach (var m in minutes) { foreach (var s in seconds) { var newDate = new DateTime(date.Year, date.Month, date.Day, h, m, s); if (newDate >= startDate && newDate <= endDate && !dates.Contains(newDate)) dates.Add(newDate); } } } } } public override string ToString() { return ToString(false); } public string ToString(bool iCal) { var sb = new StringBuilder(); switch (Freq) { case Frequency.Secondly: sb.Append("secondly"); break; case Frequency.Minutely: sb.Append("minutely"); break; case Frequency.Hourly: sb.Append("hourly"); break; case Frequency.Daily: sb.Append("daily"); break; case Frequency.Weekly: sb.Append("weekly"); break; case Frequency.Monthly: sb.Append("monthly"); break; case Frequency.Yearly: sb.Append("yearly"); break; } if (Until != DateTime.MinValue) { sb.AppendFormat(";until={0}", Until.ToString("yyyyMMdd'T'HHmmss'Z'")); } else if (Count >= 0) { sb.AppendFormat(";count={0}", Count); } if (Interval > 1) { sb.AppendFormat(";interval={0}", Interval); } if (BySecond != null && BySecond.Length > 0) { sb.Append(";bysecond="); foreach (var s in BySecond) sb.AppendFormat("{0},", s); sb.Remove(sb.Length - 1, 1); } if (ByMinute != null && ByMinute.Length > 0) { sb.Append(";byminute="); foreach (var m in ByMinute) sb.AppendFormat("{0},", m); sb.Remove(sb.Length - 1, 1); } if (ByHour != null && ByHour.Length > 0) { sb.Append(";byhour="); foreach (var h in ByHour) sb.AppendFormat("{0},", h); sb.Remove(sb.Length - 1, 1); } if (ByDay != null && ByDay.Length > 0) { sb.Append(";byday="); foreach (var d in ByDay) sb.AppendFormat("{0},", d); sb.Remove(sb.Length - 1, 1); } if (ByMonthDay != null && ByMonthDay.Length > 0) { sb.Append(";bymonthday="); foreach (var d in ByMonthDay) sb.AppendFormat("{0},", d); sb.Remove(sb.Length - 1, 1); } if (ByYearDay != null && ByYearDay.Length > 0) { sb.Append(";byyearday="); foreach (var d in ByYearDay) sb.AppendFormat("{0},", d); sb.Remove(sb.Length - 1, 1); } if (ByWeekNo != null && ByWeekNo.Length > 0) { sb.Append(";byweekno="); foreach (var w in ByWeekNo) sb.AppendFormat("{0},", w); sb.Remove(sb.Length - 1, 1); } if (ByMonth != null && ByMonth.Length > 0) { sb.Append(";bymonth="); foreach (var m in ByMonth) sb.AppendFormat("{0},", m); sb.Remove(sb.Length - 1, 1); } if (BySetPos != null && BySetPos.Length > 0) { sb.Append(";bysetpos="); foreach (var p in BySetPos) sb.AppendFormat("{0},", p); sb.Remove(sb.Length - 1, 1); } if (WKST.DayOfWeek != DayOfWeek.Monday) sb.AppendFormat(";wkst={0}", WKST.Id); if (!iCal && ExDates != null && ExDates.Count > 0) { sb.AppendFormat(";exdates="); foreach (var d in this.ExDates) { if (d.isDateTime) sb.Append(d.Date.ToString("yyyyMMdd'T'HHmmssK") + ","); else sb.Append(d.Date.ToString("yyyyMMdd") + ","); } sb.Remove(sb.Length - 1, 1); } if (sb.Length > 0) sb.Insert(0, "freq="); return sb.ToString(); } private static readonly string[] _dateTimeFormats = new[] { "o", "yyyyMMdd'T'HHmmssK", "yyyyMMdd", "yyyy'-'MM'-'dd'T'HH'-'mm'-'ss'.'fffffffK", "yyyy'-'MM'-'dd'T'HH'-'mm'-'ss'.'fffK", "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK", "yyyy'-'MM'-'dd'T'HH'-'mm'-'ssK", "yyyy'-'MM'-'dd'T'HH':'mm':'ssK", "yyyy'-'MM'-'dd" }; public static Frequency ParseFrequency(string frequency) { return (frequency.ToLower()) switch { "monthly" => Frequency.Monthly, "secondly" => Frequency.Secondly, "daily" => Frequency.Daily, "hourly" => Frequency.Hourly, "minutely" => Frequency.Minutely, "weekly" => Frequency.Weekly, "yearly" => Frequency.Yearly, _ => Frequency.Never, }; } public static RecurrenceRule Parse(string serializedString) { var rr = new RecurrenceRule(); var parts = serializedString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); foreach (var pair in parts) { var name = pair.Split('=')[0].Trim().ToLower(); var val = pair.Split('=')[1].Trim().ToLower(); switch (name) { case "freq": rr.Freq = ParseFrequency(val); break; case "until": DateTime d; if (DateTime.TryParseExact(val.ToUpper(), _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out d)) rr.Until = d; break; case "count": rr.Count = Convert.ToInt32(val); break; case "interval": rr.Interval = Convert.ToInt32(val); break; case "bysecond": rr.BySecond = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "byminute": rr.ByMinute = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "byhour": rr.ByHour = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "byday": rr.ByDay = val.Split(',').Select(v => RecurrenceRule.WeekDay.Parse(v)).ToArray(); break; case "bymonthday": rr.ByMonthDay = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "byyearday": rr.ByYearDay = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "byweekno": rr.ByWeekNo = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "bymonth": rr.ByMonth = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "bysetpos": rr.BySetPos = val.Split(',').Select(v => Convert.ToInt32(v)).ToArray(); break; case "wkst": rr.WKST = RecurrenceRule.WeekDay.Parse(val); break; case "exdates": foreach (var date in val.Split(',')) { if (DateTime.TryParseExact(date.ToUpper(), _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var dt)) rr.ExDates.Add(new ExDate() { Date = dt, isDateTime = (date.ToLower().IndexOf('t') >= 0) }); } break; } } return rr; } #region IiCalFormatView Members public string ToiCalFormat() { if (this.Freq == Frequency.Never) return string.Empty; var sb = new StringBuilder(); sb.Append("RRULE:" + this.ToString(true)); if (this.ExDates.Count > 0) { sb.Append("\r\nEXDATE"); if (!this.ExDates[0].isDateTime) sb.Append(";VALUE=DATE"); sb.Append(":"); foreach (var d in this.ExDates) { if (d.isDateTime) sb.Append(d.Date.ToString("yyyyMMdd'T'HHmmssK")); else sb.Append(d.Date.ToString("yyyyMMdd")); sb.Append(","); } sb.Remove(sb.Length - 1, 1); } return sb.ToString().ToUpper(); } #endregion #region ICloneable Members public object Clone() { var o = (RecurrenceRule)this.MemberwiseClone(); o.ExDates = new List(); this.ExDates.ForEach(d => o.ExDates.Add(d)); if (ByDay != null) { var days = new List(); foreach (var d in ByDay) days.Add(WeekDay.Parse(d.ToString())); o.ByDay = days.ToArray(); } o.WKST = WeekDay.Parse(this.WKST.ToString()); return o; } #endregion } }