From f03ffa48fb78e52a24139ef5943cd59a755fd405 Mon Sep 17 00:00:00 2001 From: Aptivi Date: Mon, 30 Sep 2024 12:49:12 +0300 Subject: [PATCH] add - doc|prt - Added recurrence v2 support --- We've finally added the second version of the recurrence rule parser which supports vCalendar v2.0. --- Type: add Breaking: False Doc Required: True Backport Required: False Part: 2/2 --- .../Parsers/Recurrence/RecurrenceParserV2.cs | 359 ++++++++- .../Parsers/Recurrence/RecurrenceRule.cs | 18 +- .../Recurrence/RecurrenceRuleFrequency.cs | 10 +- .../Recurrence/RecurrenceV2ParseTests.cs | 762 ++++++++++++++++++ 4 files changed, 1137 insertions(+), 12 deletions(-) create mode 100644 VisualCard.Tests/Recurrence/RecurrenceV2ParseTests.cs diff --git a/VisualCard.Calendar/Parsers/Recurrence/RecurrenceParserV2.cs b/VisualCard.Calendar/Parsers/Recurrence/RecurrenceParserV2.cs index 4fe5bce..763cee0 100644 --- a/VisualCard.Calendar/Parsers/Recurrence/RecurrenceParserV2.cs +++ b/VisualCard.Calendar/Parsers/Recurrence/RecurrenceParserV2.cs @@ -19,6 +19,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using VisualCard.Parsers; namespace VisualCard.Calendar.Parsers.Recurrence { @@ -28,21 +30,362 @@ namespace VisualCard.Calendar.Parsers.Recurrence public static partial class RecurrenceParser { /// - /// Parses the recurrence rule that is formatted with Basic Recurrence Rule Grammar of XAPIA's CSA + /// Parses the recurrence rule that is formatted with vCalendar's recurrence rule syntax in section 3.3.10 /// /// Recurrence rule - /// Parsed recurrence rules - public static RecurrenceRule[] ParseRuleV2(string rule) + /// Parsed recurrence rule + public static RecurrenceRule ParseRuleV2(string rule) { - List rules = []; - // Sanity check if (string.IsNullOrEmpty(rule)) throw new ArgumentNullException(nameof(rule), "There is no rule."); - // Return the rules - // TODO: Fill this up! - return [.. rules]; + // Split the semicolons to represent part of the rule and check for frequency + string[] ruleParts = rule.Split(';'); + bool hasFreq = ruleParts.Any((part) => part.Contains("FREQ")); + if (!hasFreq) + throw new ArgumentException("Frequency is not specified."); + + // Make a rule instance + RecurrenceRule recurrenceRule = new() + { + ruleVersion = new(2, 0) + }; + + // Loop for each rule part + List processedKeys = []; + foreach (string part in ruleParts) + { + // Check for equals sign + if (!part.Contains("=")) + throw new ArgumentException($"A rule part needs an equal sign, {part}."); + + // Get the key name and the value representation + // - ( "FREQ" "=" freq ), and so on. + string keyName = part.Substring(0, part.IndexOf("=")); + string valueRepresentation = keyName.Length + 2 > part.Length ? "" : part.Substring(keyName.Length + 1); + + // Add this key to the processed keys list after checking for duplicates, but before checking for + // UNTIL and COUNT occurrences since they can't coexist with each other. + if (processedKeys.Contains(keyName)) + throw new ArgumentException($"Key {keyName} already exists, {part}"); + processedKeys.Add(keyName); + if (processedKeys.Contains("UNTIL") && processedKeys.Contains("COUNT")) + throw new ArgumentException($"Keys UNTIL and COUNT can't coexist with each other, {part}"); + + // Parse everything according to the key name + switch (keyName) + { + case "FREQ": + // ( "FREQ" "=" freq ) + // freq = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY" + // / "WEEKLY" / "MONTHLY" / "YEARLY" + RecurrenceRuleFrequency freq = + valueRepresentation == "SECONDLY" ? RecurrenceRuleFrequency.Second : + valueRepresentation == "MINUTELY" ? RecurrenceRuleFrequency.Minute : + valueRepresentation == "HOURLY" ? RecurrenceRuleFrequency.Hourly : + valueRepresentation == "DAILY" ? RecurrenceRuleFrequency.Daily : + valueRepresentation == "WEEKLY" ? RecurrenceRuleFrequency.Weekly : + valueRepresentation == "MONTHLY" ? RecurrenceRuleFrequency.Monthly : + valueRepresentation == "YEARLY" ? RecurrenceRuleFrequency.Yearly : + throw new ArgumentException($"Frequency {valueRepresentation} is invalid, {part}"); + recurrenceRule.frequency = freq; + break; + case "UNTIL": + // ( "UNTIL" "=" enddate ) + // enddate = date / date-time + DateTimeOffset dateTime = VcardCommonTools.ParsePosixDate(valueRepresentation); + recurrenceRule.endDate = dateTime; + break; + case "COUNT": + // ( "COUNT" "=" 1*DIGIT ) + if (!int.TryParse(valueRepresentation, out int duration)) + throw new ArgumentException($"Duration {valueRepresentation} is invalid, {part}"); + recurrenceRule.duration = duration; + break; + case "INTERVAL": + // ( "INTERVAL" "=" 1*DIGIT ) + if (!int.TryParse(valueRepresentation, out int interval)) + throw new ArgumentException($"Interval {valueRepresentation} is invalid, {part}"); + recurrenceRule.interval = interval; + break; + case "BYSECOND": + // ( "BYSECOND" "=" byseclist ) + // byseclist = ( seconds *("," seconds) ) + // seconds = 1*2DIGIT ;0 to 60 + string[] secondsList = valueRepresentation.Split(','); + foreach (string secondStr in secondsList) + { + if (!int.TryParse(secondStr, out int seconds)) + throw new ArgumentException($"Seconds {secondStr} is invalid, {part}"); + if (seconds < 0 || seconds > 60) + throw new ArgumentException($"Seconds {seconds} is out of range [0-60], {part}"); + recurrenceRule.secondsList.Add(seconds); + } + break; + case "BYMINUTE": + // ( "BYMINUTE" "=" byminlist ) + // byminlist = ( minutes *("," minutes) ) + // minutes = 1*2DIGIT ;0 to 59 + string[] minutesList = valueRepresentation.Split(','); + foreach (string minuteStr in minutesList) + { + if (!int.TryParse(minuteStr, out int minutes)) + throw new ArgumentException($"Minutes {minuteStr} is invalid, {part}"); + if (minutes < 0 || minutes > 59) + throw new ArgumentException($"Minutes {minutes} is out of range [0-59], {part}"); + recurrenceRule.minutesList.Add(minutes); + } + break; + case "BYHOUR": + // ( "BYHOUR" "=" byhrlist ) + // byhrlist = ( hour *("," hour) ) + // hour = 1*2DIGIT ;0 to 23 + string[] hoursList = valueRepresentation.Split(','); + foreach (string hourStr in hoursList) + { + if (!int.TryParse(hourStr, out int hours)) + throw new ArgumentException($"Hours {hourStr} is invalid, {part}"); + if (hours < 0 || hours > 23) + throw new ArgumentException($"Hours {hours} is out of range [0-23], {part}"); + recurrenceRule.hoursList.Add(hours); + } + break; + case "BYDAY": + // ( "BYDAY" "=" bywdaylist ) + // bywdaylist = ( weekdaynum *("," weekdaynum) ) + // weekdaynum = [[plus / minus] ordwk] weekday + // ordwk = 1*2DIGIT ;1 to 53 + // weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" + string[] daysList = valueRepresentation.Split(','); + foreach (string dayStr in daysList) + { + int weekNum = 0; + DayOfWeek dayOfWeek; + + // We could have an ordwk instance that could have been prefixed by a plus or a minus sign. + // Check to see if we have a plus or a minus sign, then check the digit after. + string finalDayStr = dayStr; + bool isNegative = finalDayStr[0] == '-'; + bool hasSignAtFirst = finalDayStr[0] == '+' || isNegative; + if (hasSignAtFirst) + { + if (!char.IsNumber(finalDayStr[1])) + throw new ArgumentException($"Not a number {finalDayStr[1]} after sign {finalDayStr[0]}, {finalDayStr}, {part}"); + finalDayStr = finalDayStr.Substring(1); + } + + // Check for week number + if (char.IsNumber(finalDayStr[0])) + { + // Get the split index between digit and the week number + int splitIdx; + for (splitIdx = 0; splitIdx < finalDayStr.Length; splitIdx++) + { + if (!char.IsNumber(finalDayStr[splitIdx])) + break; + } + string weekNumStr = finalDayStr.Substring(0, splitIdx); + if (!int.TryParse(weekNumStr, out weekNum)) + throw new ArgumentException($"Not a week number {weekNumStr}, {finalDayStr}, {part}"); + if (weekNum < 1 || weekNum > 53) + throw new ArgumentException($"Week number {weekNum} is out of range [1-53], {part}"); + finalDayStr = finalDayStr.Substring(splitIdx); + } + + // Check for weekday + if (finalDayStr.Length != 2) + throw new ArgumentException($"Week day needs to have exactly 2 characters {finalDayStr}, {part}"); + if (finalDayStr == "SU") + dayOfWeek = DayOfWeek.Sunday; + else if (finalDayStr == "MO") + dayOfWeek = DayOfWeek.Monday; + else if (finalDayStr == "TU") + dayOfWeek = DayOfWeek.Tuesday; + else if (finalDayStr == "WE") + dayOfWeek = DayOfWeek.Wednesday; + else if (finalDayStr == "TH") + dayOfWeek = DayOfWeek.Thursday; + else if (finalDayStr == "FR") + dayOfWeek = DayOfWeek.Friday; + else if (finalDayStr == "SA") + dayOfWeek = DayOfWeek.Saturday; + else + throw new ArgumentException($"Not a week day {finalDayStr}, {part}"); + + // Add the result + recurrenceRule.daysList.Add((isNegative, weekNum, dayOfWeek)); + } + break; + case "BYMONTHDAY": + // ( "BYMONTHDAY" "=" bymodaylist ) + // bymodaylist = ( monthdaynum *("," monthdaynum) ) + // monthdaynum = [plus / minus] ordmoday + // ordmoday = 1*2DIGIT ;1 to 31 + string[] monthDaysList = valueRepresentation.Split(','); + foreach (string monthDayStr in monthDaysList) + { + // We could have an ordmoday instance that could have been prefixed by a plus or a minus sign. + // Check to see if we have a plus or a minus sign, then check the digit after. + string finalMonthStr = monthDayStr; + bool isNegative = finalMonthStr[0] == '-'; + bool hasSignAtFirst = finalMonthStr[0] == '+' || isNegative; + if (hasSignAtFirst) + { + if (!char.IsNumber(finalMonthStr[1])) + throw new ArgumentException($"Not a number {finalMonthStr[1]} after sign {finalMonthStr[0]}, {finalMonthStr}, {part}"); + finalMonthStr = finalMonthStr.Substring(1); + } + + // Check for day of month number + if (!int.TryParse(finalMonthStr, out int dayOfMonth)) + throw new ArgumentException($"Not a day of month number {finalMonthStr}, {part}"); + if (dayOfMonth < 1 || dayOfMonth > 31) + throw new ArgumentException($"Day of year {dayOfMonth} is out of range [1-31], {part}"); + + // Add the result + recurrenceRule.daysOfMonthList.Add((isNegative, dayOfMonth)); + } + break; + case "BYYEARDAY": + // ( "BYYEARDAY" "=" byyrdaylist ) + // byyrdaylist = ( yeardaynum *("," yeardaynum) ) + // yeardaynum = [plus / minus] ordyrday + // ordyrday = 1*3DIGIT ;1 to 366 + string[] yearsList = valueRepresentation.Split(','); + foreach (string yearStr in yearsList) + { + // We could have an ordyrday instance that could have been prefixed by a plus or a minus sign. + // Check to see if we have a plus or a minus sign, then check the digit after. + string finalYearStr = yearStr; + bool isNegative = finalYearStr[0] == '-'; + bool hasSignAtFirst = finalYearStr[0] == '+' || isNegative; + if (hasSignAtFirst) + { + if (!char.IsNumber(finalYearStr[1])) + throw new ArgumentException($"Not a number {finalYearStr[1]} after sign {finalYearStr[0]}, {finalYearStr}, {part}"); + finalYearStr = finalYearStr.Substring(1); + } + + // Check for day of year number + if (!int.TryParse(finalYearStr, out int dayOfYear)) + throw new ArgumentException($"Not a day of year number {finalYearStr}, {part}"); + if (dayOfYear < 1 || dayOfYear > 366) + throw new ArgumentException($"Day of year {dayOfYear} is out of range [1-366], {part}"); + + // Add the result + recurrenceRule.daysOfYearList.Add((isNegative, dayOfYear)); + } + break; + case "BYWEEKNO": + // ( "BYWEEKNO" "=" bywknolist ) + // bywknolist = ( weeknum *("," weeknum) ) + // weeknum = [plus / minus] ordwk + // ordwk = 1*2DIGIT ;1 to 53 + string[] weeksList = valueRepresentation.Split(','); + foreach (string weekStr in weeksList) + { + // We could have an ordwk instance that could have been prefixed by a plus or a minus sign. + // Check to see if we have a plus or a minus sign, then check the digit after. + string finalWeekStr = weekStr; + bool isNegative = finalWeekStr[0] == '-'; + bool hasSignAtFirst = finalWeekStr[0] == '+' || isNegative; + if (hasSignAtFirst) + { + if (!char.IsNumber(finalWeekStr[1])) + throw new ArgumentException($"Not a number {finalWeekStr[1]} after sign {finalWeekStr[0]}, {finalWeekStr}, {part}"); + finalWeekStr = finalWeekStr.Substring(1); + } + + // Check for week number + if (!int.TryParse(finalWeekStr, out int weekNum)) + throw new ArgumentException($"Not a week number {finalWeekStr}, {part}"); + if (weekNum < 1 || weekNum > 53) + throw new ArgumentException($"Week number {weekNum} is out of range [1-53], {part}"); + + // Add the result + recurrenceRule.weeksList.Add((isNegative, weekNum)); + } + break; + case "BYMONTH": + // ( "BYMONTH" "=" bymolist ) + // bymolist = ( monthnum *("," monthnum) ) + // monthnum = 1*2DIGIT ;1 to 12 + string[] monthsList = valueRepresentation.Split(','); + foreach (string monthStr in monthsList) + { + // Check for month number + if (!int.TryParse(monthStr, out int monthNum)) + throw new ArgumentException($"Not a month number {monthStr}, {part}"); + if (monthNum < 1 || monthNum > 12) + throw new ArgumentException($"Month number {monthNum} is out of range [1-12], {part}"); + + // Add the result + recurrenceRule.monthsList.Add(monthNum); + } + break; + case "BYSETPOS": + // ( "BYSETPOS" "=" bysplist ) + // bysplist = ( setposday *("," setposday) ) + // setposday = yeardaynum + // yeardaynum = [plus / minus] ordyrday + // ordyrday = 1*3DIGIT ;1 to 366 + string[] positionsList = valueRepresentation.Split(','); + foreach (string positionStr in positionsList) + { + // We could have an ordyrday instance that could have been prefixed by a plus or a minus sign. + // Check to see if we have a plus or a minus sign, then check the digit after. + string finalPositionStr = positionStr; + bool isNegative = finalPositionStr[0] == '-'; + bool hasSignAtFirst = finalPositionStr[0] == '+' || isNegative; + if (hasSignAtFirst) + { + if (!char.IsNumber(finalPositionStr[1])) + throw new ArgumentException($"Not a number {finalPositionStr[1]} after sign {finalPositionStr[0]}, {finalPositionStr}, {part}"); + finalPositionStr = finalPositionStr.Substring(1); + } + + // Check for position number + if (!int.TryParse(finalPositionStr, out int position)) + throw new ArgumentException($"Not a position number {finalPositionStr}, {part}"); + if (position < 1 || position > 366) + throw new ArgumentException($"Position {position} is out of range [1-366], {part}"); + + // Add the result + recurrenceRule.positionsList.Add((isNegative, position)); + } + break; + case "WKST": + // ( "WKST" "=" weekday ) + // weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" + DayOfWeek start; + if (valueRepresentation.Length != 2) + throw new ArgumentException($"Week day needs to have exactly 2 characters {valueRepresentation}, {part}"); + if (valueRepresentation == "SU") + start = DayOfWeek.Sunday; + else if (valueRepresentation == "MO") + start = DayOfWeek.Monday; + else if (valueRepresentation == "TU") + start = DayOfWeek.Tuesday; + else if (valueRepresentation == "WE") + start = DayOfWeek.Wednesday; + else if (valueRepresentation == "TH") + start = DayOfWeek.Thursday; + else if (valueRepresentation == "FR") + start = DayOfWeek.Friday; + else if (valueRepresentation == "SA") + start = DayOfWeek.Saturday; + else + throw new ArgumentException($"Not a week day {valueRepresentation}, {part}"); + recurrenceRule.weekStart = start; + break; + default: + throw new ArgumentException($"Not a valid key name {keyName}, {part}"); + } + } + + // Return the rule, because v2.0 doesn't support nested rules within the same rule string + return recurrenceRule; } } } diff --git a/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRule.cs b/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRule.cs index 59f934b..c5957cd 100644 --- a/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRule.cs +++ b/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRule.cs @@ -29,12 +29,14 @@ public class RecurrenceRule { // Frequency and interval internal RecurrenceRuleFrequency frequency; - internal int interval; + internal int interval = 1; // General + internal Version ruleVersion = new(1, 0); internal int duration = 2; internal DateTimeOffset endDate; + #region Version 1.0 rules // Time period (daily) internal List<(bool isEnd, TimeSpan time)> timePeriods = []; @@ -48,5 +50,19 @@ public class RecurrenceRule // Yearly (in a month and in a day) internal List<(bool isEnd, int monthNum)> yearlyMonthNumbers = []; internal List<(bool isEnd, int dayNum)> yearlyDayNumbers = []; + #endregion + + #region Version 2.0 rules + internal List secondsList = []; + internal List minutesList = []; + internal List hoursList = []; + internal List<(bool negative, int weekNum, DayOfWeek time)> daysList = []; + internal List<(bool negative, int dayOfMonth)> daysOfMonthList = []; + internal List<(bool negative, int dayOfYear)> daysOfYearList = []; + internal List<(bool negative, int weekNum)> weeksList = []; + internal List monthsList = []; + internal List<(bool negative, int position)> positionsList = []; + internal DayOfWeek weekStart = DayOfWeek.Sunday; + #endregion } } diff --git a/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRuleFrequency.cs b/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRuleFrequency.cs index 26820ff..b221f77 100644 --- a/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRuleFrequency.cs +++ b/VisualCard.Calendar/Parsers/Recurrence/RecurrenceRuleFrequency.cs @@ -41,13 +41,17 @@ public enum RecurrenceRuleFrequency /// Daily, /// - /// Weekly repetition (v1) + /// Weekly repetition (v1 and v2) /// Weekly, /// - /// Week number repetition (v2) + /// Monthly repetition (v2) /// - WeekNo, + Monthly, + /// + /// Yearly repetition (v2) + /// + Yearly, /// /// Monthly repetition based on a relative day (v1 and v2) /// diff --git a/VisualCard.Tests/Recurrence/RecurrenceV2ParseTests.cs b/VisualCard.Tests/Recurrence/RecurrenceV2ParseTests.cs new file mode 100644 index 0000000..7a82d94 --- /dev/null +++ b/VisualCard.Tests/Recurrence/RecurrenceV2ParseTests.cs @@ -0,0 +1,762 @@ +// +// VisualCard Copyright (C) 2021-2024 Aptivi +// +// This file is part of VisualCard +// +// VisualCard is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// VisualCard is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; +using VisualCard.Calendar.Parsers.Recurrence; + +namespace VisualCard.Tests.Recurrence +{ + [TestClass] + public class RecurrenceV2ParseTests + { + [TestMethod] + [DataRow("FREQ=DAILY;COUNT=10")] + [DataRow("FREQ=DAILY;UNTIL=19971224T000000Z")] + [DataRow("FREQ=DAILY;INTERVAL=2")] + [DataRow("FREQ=DAILY;INTERVAL=10;COUNT=5")] + [DataRow("FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA")] + [DataRow("FREQ=WEEKLY;COUNT=10")] + [DataRow("FREQ=WEEKLY;UNTIL=19971224T000000Z")] + [DataRow("FREQ=WEEKLY;INTERVAL=2;WKST=SU")] + [DataRow("FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH")] + [DataRow("FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR")] + [DataRow("FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH")] + [DataRow("FREQ=MONTHLY;COUNT=10;BYDAY=1FR")] + [DataRow("FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR")] + [DataRow("FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU")] + [DataRow("FREQ=MONTHLY;COUNT=6;BYDAY=-2MO")] + [DataRow("FREQ=MONTHLY;BYMONTHDAY=-3")] + [DataRow("FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15")] + [DataRow("FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1")] + [DataRow("FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15")] + [DataRow("FREQ=MONTHLY;INTERVAL=2;BYDAY=TU")] + [DataRow("FREQ=YEARLY;COUNT=10;BYMONTH=6,7")] + [DataRow("FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3")] + [DataRow("FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200")] + [DataRow("FREQ=YEARLY;BYDAY=20MO")] + [DataRow("FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO")] + [DataRow("FREQ=YEARLY;BYMONTH=3;BYDAY=TH")] + [DataRow("FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8")] + [DataRow("FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13")] + [DataRow("FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13")] + [DataRow("FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8")] + [DataRow("FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3")] + [DataRow("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2")] + [DataRow("FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z")] + [DataRow("FREQ=MINUTELY;INTERVAL=15;COUNT=6")] + [DataRow("FREQ=MINUTELY;INTERVAL=90;COUNT=4")] + [DataRow("FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16")] + [DataRow("FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO")] + [DataRow("FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU")] + [DataRow("FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5")] + public void ParseV2Recurrence(string rule) + { + var ruleInstance = RecurrenceParser.ParseRuleV2(rule); + ruleInstance.ShouldNotBeNull(); + } + + [TestMethod] + public void ParseV2Recurrence01() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=DAILY;COUNT=10"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Daily); + rule.interval.ShouldBe(1); + } + + [TestMethod] + public void ParseV2Recurrence02() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=DAILY;UNTIL=19971224T000000Z"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Daily); + rule.interval.ShouldBe(1); + rule.endDate.ShouldBe(new(1997, 12, 24, 0, 0, 0, new())); + } + + [TestMethod] + public void ParseV2Recurrence03() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=DAILY;INTERVAL=2"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Daily); + rule.interval.ShouldBe(2); + } + + [TestMethod] + public void ParseV2Recurrence04() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=DAILY;INTERVAL=10;COUNT=5"); + + // Check the first rule + rule.duration.ShouldBe(5); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Daily); + rule.interval.ShouldBe(10); + } + + [TestMethod] + public void ParseV2Recurrence05() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(1); + rule.endDate.ShouldBe(new(2000, 1, 31, 14, 0, 0, new())); + rule.monthsList.Count.ShouldBe(1); + rule.monthsList[0].ShouldBe(1); + rule.daysList.Count.ShouldBe(7); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Sunday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Monday); + rule.daysList[1].weekNum.ShouldBe(0); + rule.daysList[2].negative.ShouldBeFalse(); + rule.daysList[2].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[2].weekNum.ShouldBe(0); + rule.daysList[3].negative.ShouldBeFalse(); + rule.daysList[3].time.ShouldBe(DayOfWeek.Wednesday); + rule.daysList[3].weekNum.ShouldBe(0); + rule.daysList[4].negative.ShouldBeFalse(); + rule.daysList[4].time.ShouldBe(DayOfWeek.Thursday); + rule.daysList[4].weekNum.ShouldBe(0); + rule.daysList[5].negative.ShouldBeFalse(); + rule.daysList[5].time.ShouldBe(DayOfWeek.Friday); + rule.daysList[5].weekNum.ShouldBe(0); + rule.daysList[6].negative.ShouldBeFalse(); + rule.daysList[6].time.ShouldBe(DayOfWeek.Saturday); + rule.daysList[6].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence06() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;COUNT=10"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(1); + } + + [TestMethod] + public void ParseV2Recurrence07() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;UNTIL=19971224T000000Z"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(1); + rule.endDate.ShouldBe(new(1997, 12, 24, 0, 0, 0, new())); + } + + [TestMethod] + public void ParseV2Recurrence08() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;INTERVAL=2;WKST=SU"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(2); + rule.weekStart.ShouldBe(DayOfWeek.Sunday); + } + + [TestMethod] + public void ParseV2Recurrence09() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(1); + rule.weekStart.ShouldBe(DayOfWeek.Sunday); + rule.endDate.ShouldBe(new(1997, 10, 7, 0, 0, 0, new())); + rule.daysList.Count.ShouldBe(2); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Thursday); + rule.daysList[1].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence10() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(2); + rule.weekStart.ShouldBe(DayOfWeek.Sunday); + rule.endDate.ShouldBe(new(1997, 12, 24, 0, 0, 0, new())); + rule.daysList.Count.ShouldBe(3); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Monday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Wednesday); + rule.daysList[1].weekNum.ShouldBe(0); + rule.daysList[2].negative.ShouldBeFalse(); + rule.daysList[2].time.ShouldBe(DayOfWeek.Friday); + rule.daysList[2].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence11() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH"); + + // Check the first rule + rule.duration.ShouldBe(8); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(2); + rule.weekStart.ShouldBe(DayOfWeek.Sunday); + rule.daysList.Count.ShouldBe(2); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Thursday); + rule.daysList[1].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence12() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;COUNT=10;BYDAY=1FR"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Friday); + rule.daysList[0].weekNum.ShouldBe(1); + } + + [TestMethod] + public void ParseV2Recurrence13() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.endDate.ShouldBe(new(1997, 12, 24, 0, 0, 0, new())); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Friday); + rule.daysList[0].weekNum.ShouldBe(1); + } + + [TestMethod] + public void ParseV2Recurrence14() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(2); + rule.daysList.Count.ShouldBe(2); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Sunday); + rule.daysList[0].weekNum.ShouldBe(1); + rule.daysList[1].negative.ShouldBeTrue(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Sunday); + rule.daysList[1].weekNum.ShouldBe(1); + } + + [TestMethod] + public void ParseV2Recurrence15() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;COUNT=6;BYDAY=-2MO"); + + // Check the first rule + rule.duration.ShouldBe(6); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeTrue(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Monday); + rule.daysList[0].weekNum.ShouldBe(2); + } + + [TestMethod] + public void ParseV2Recurrence16() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;BYMONTHDAY=-3"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.daysOfMonthList.Count.ShouldBe(1); + rule.daysOfMonthList[0].negative.ShouldBeTrue(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(3); + } + + [TestMethod] + public void ParseV2Recurrence17() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.daysOfMonthList.Count.ShouldBe(2); + rule.daysOfMonthList[0].negative.ShouldBeFalse(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(2); + rule.daysOfMonthList[1].negative.ShouldBeFalse(); + rule.daysOfMonthList[1].dayOfMonth.ShouldBe(15); + } + + [TestMethod] + public void ParseV2Recurrence18() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.daysOfMonthList.Count.ShouldBe(2); + rule.daysOfMonthList[0].negative.ShouldBeFalse(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(1); + rule.daysOfMonthList[1].negative.ShouldBeTrue(); + rule.daysOfMonthList[1].dayOfMonth.ShouldBe(1); + } + + [TestMethod] + public void ParseV2Recurrence19() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(18); + rule.daysOfMonthList.Count.ShouldBe(6); + rule.daysOfMonthList[0].negative.ShouldBeFalse(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(10); + rule.daysOfMonthList[1].negative.ShouldBeFalse(); + rule.daysOfMonthList[1].dayOfMonth.ShouldBe(11); + rule.daysOfMonthList[2].negative.ShouldBeFalse(); + rule.daysOfMonthList[2].dayOfMonth.ShouldBe(12); + rule.daysOfMonthList[3].negative.ShouldBeFalse(); + rule.daysOfMonthList[3].dayOfMonth.ShouldBe(13); + rule.daysOfMonthList[4].negative.ShouldBeFalse(); + rule.daysOfMonthList[4].dayOfMonth.ShouldBe(14); + rule.daysOfMonthList[5].negative.ShouldBeFalse(); + rule.daysOfMonthList[5].dayOfMonth.ShouldBe(15); + } + + [TestMethod] + public void ParseV2Recurrence20() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;INTERVAL=2;BYDAY=TU"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(2); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[0].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence21() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;COUNT=10;BYMONTH=6,7"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(1); + rule.monthsList.Count.ShouldBe(2); + rule.monthsList[0].ShouldBe(6); + rule.monthsList[1].ShouldBe(7); + } + + [TestMethod] + public void ParseV2Recurrence22() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(2); + rule.monthsList.Count.ShouldBe(3); + rule.monthsList[0].ShouldBe(1); + rule.monthsList[1].ShouldBe(2); + rule.monthsList[2].ShouldBe(3); + } + + [TestMethod] + public void ParseV2Recurrence23() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200"); + + // Check the first rule + rule.duration.ShouldBe(10); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(3); + rule.daysOfYearList.Count.ShouldBe(3); + rule.daysOfYearList[0].negative.ShouldBeFalse(); + rule.daysOfYearList[0].dayOfYear.ShouldBe(1); + rule.daysOfYearList[1].negative.ShouldBeFalse(); + rule.daysOfYearList[1].dayOfYear.ShouldBe(100); + rule.daysOfYearList[2].negative.ShouldBeFalse(); + rule.daysOfYearList[2].dayOfYear.ShouldBe(200); + } + + [TestMethod] + public void ParseV2Recurrence24() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;BYDAY=20MO"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(1); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Monday); + rule.daysList[0].weekNum.ShouldBe(20); + } + + [TestMethod] + public void ParseV2Recurrence25() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(1); + rule.weeksList.Count.ShouldBe(1); + rule.weeksList[0].negative.ShouldBeFalse(); + rule.weeksList[0].weekNum.ShouldBe(20); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Monday); + rule.daysList[0].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence26() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;BYMONTH=3;BYDAY=TH"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(1); + rule.monthsList.Count.ShouldBe(1); + rule.monthsList[0].ShouldBe(3); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Thursday); + rule.daysList[0].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence27() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(1); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Thursday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.monthsList.Count.ShouldBe(3); + rule.monthsList[0].ShouldBe(6); + rule.monthsList[1].ShouldBe(7); + rule.monthsList[2].ShouldBe(8); + } + + [TestMethod] + public void ParseV2Recurrence28() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.daysOfMonthList.Count.ShouldBe(1); + rule.daysOfMonthList[0].negative.ShouldBeFalse(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(13); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Friday); + rule.daysList[0].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence29() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.daysOfMonthList.Count.ShouldBe(7); + rule.daysOfMonthList[0].negative.ShouldBeFalse(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(7); + rule.daysOfMonthList[1].negative.ShouldBeFalse(); + rule.daysOfMonthList[1].dayOfMonth.ShouldBe(8); + rule.daysOfMonthList[2].negative.ShouldBeFalse(); + rule.daysOfMonthList[2].dayOfMonth.ShouldBe(9); + rule.daysOfMonthList[3].negative.ShouldBeFalse(); + rule.daysOfMonthList[3].dayOfMonth.ShouldBe(10); + rule.daysOfMonthList[4].negative.ShouldBeFalse(); + rule.daysOfMonthList[4].dayOfMonth.ShouldBe(11); + rule.daysOfMonthList[5].negative.ShouldBeFalse(); + rule.daysOfMonthList[5].dayOfMonth.ShouldBe(12); + rule.daysOfMonthList[6].negative.ShouldBeFalse(); + rule.daysOfMonthList[6].dayOfMonth.ShouldBe(13); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Saturday); + rule.daysList[0].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence30() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Yearly); + rule.interval.ShouldBe(4); + rule.daysOfMonthList.Count.ShouldBe(7); + rule.daysOfMonthList[0].negative.ShouldBeFalse(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(2); + rule.daysOfMonthList[1].negative.ShouldBeFalse(); + rule.daysOfMonthList[1].dayOfMonth.ShouldBe(3); + rule.daysOfMonthList[2].negative.ShouldBeFalse(); + rule.daysOfMonthList[2].dayOfMonth.ShouldBe(4); + rule.daysOfMonthList[3].negative.ShouldBeFalse(); + rule.daysOfMonthList[3].dayOfMonth.ShouldBe(5); + rule.daysOfMonthList[4].negative.ShouldBeFalse(); + rule.daysOfMonthList[4].dayOfMonth.ShouldBe(6); + rule.daysOfMonthList[5].negative.ShouldBeFalse(); + rule.daysOfMonthList[5].dayOfMonth.ShouldBe(7); + rule.daysOfMonthList[6].negative.ShouldBeFalse(); + rule.daysOfMonthList[6].dayOfMonth.ShouldBe(8); + rule.daysList.Count.ShouldBe(1); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.monthsList.Count.ShouldBe(1); + rule.monthsList[0].ShouldBe(11); + } + + [TestMethod] + public void ParseV2Recurrence31() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3"); + + // Check the first rule + rule.duration.ShouldBe(3); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.positionsList.Count.ShouldBe(1); + rule.positionsList[0].negative.ShouldBeFalse(); + rule.positionsList[0].position.ShouldBe(3); + rule.daysList.Count.ShouldBe(3); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Wednesday); + rule.daysList[1].weekNum.ShouldBe(0); + rule.daysList[2].negative.ShouldBeFalse(); + rule.daysList[2].time.ShouldBe(DayOfWeek.Thursday); + rule.daysList[2].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence32() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.positionsList.Count.ShouldBe(1); + rule.positionsList[0].negative.ShouldBeTrue(); + rule.positionsList[0].position.ShouldBe(2); + rule.daysList.Count.ShouldBe(5); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Monday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[1].weekNum.ShouldBe(0); + rule.daysList[2].negative.ShouldBeFalse(); + rule.daysList[2].time.ShouldBe(DayOfWeek.Wednesday); + rule.daysList[2].weekNum.ShouldBe(0); + rule.daysList[3].negative.ShouldBeFalse(); + rule.daysList[3].time.ShouldBe(DayOfWeek.Thursday); + rule.daysList[3].weekNum.ShouldBe(0); + rule.daysList[4].negative.ShouldBeFalse(); + rule.daysList[4].time.ShouldBe(DayOfWeek.Friday); + rule.daysList[4].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence33() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Hourly); + rule.interval.ShouldBe(3); + rule.endDate.ShouldBe(new(1997, 9, 2, 17, 0, 0, new())); + } + + [TestMethod] + public void ParseV2Recurrence34() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MINUTELY;INTERVAL=15;COUNT=6"); + + // Check the first rule + rule.duration.ShouldBe(6); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Minute); + rule.interval.ShouldBe(15); + } + + [TestMethod] + public void ParseV2Recurrence35() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MINUTELY;INTERVAL=90;COUNT=4"); + + // Check the first rule + rule.duration.ShouldBe(4); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Minute); + rule.interval.ShouldBe(90); + } + + [TestMethod] + public void ParseV2Recurrence36() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16"); + + // Check the first rule + rule.duration.ShouldBe(2); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Minute); + rule.interval.ShouldBe(20); + rule.hoursList.Count.ShouldBe(8); + rule.hoursList[0].ShouldBe(9); + rule.hoursList[1].ShouldBe(10); + rule.hoursList[2].ShouldBe(11); + rule.hoursList[3].ShouldBe(12); + rule.hoursList[4].ShouldBe(13); + rule.hoursList[5].ShouldBe(14); + rule.hoursList[6].ShouldBe(15); + rule.hoursList[7].ShouldBe(16); + } + + [TestMethod] + public void ParseV2Recurrence37() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO"); + + // Check the first rule + rule.duration.ShouldBe(4); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(2); + rule.weekStart.ShouldBe(DayOfWeek.Monday); + rule.daysList.Count.ShouldBe(2); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Sunday); + rule.daysList[1].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence38() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU"); + + // Check the first rule + rule.duration.ShouldBe(4); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Weekly); + rule.interval.ShouldBe(2); + rule.weekStart.ShouldBe(DayOfWeek.Sunday); + rule.daysList.Count.ShouldBe(2); + rule.daysList[0].negative.ShouldBeFalse(); + rule.daysList[0].time.ShouldBe(DayOfWeek.Tuesday); + rule.daysList[0].weekNum.ShouldBe(0); + rule.daysList[1].negative.ShouldBeFalse(); + rule.daysList[1].time.ShouldBe(DayOfWeek.Sunday); + rule.daysList[1].weekNum.ShouldBe(0); + } + + [TestMethod] + public void ParseV2Recurrence39() + { + var rule = RecurrenceParser.ParseRuleV2("FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5"); + + // Check the first rule + rule.duration.ShouldBe(5); + rule.frequency.ShouldBe(RecurrenceRuleFrequency.Monthly); + rule.interval.ShouldBe(1); + rule.weekStart.ShouldBe(DayOfWeek.Sunday); + rule.daysOfMonthList.Count.ShouldBe(2); + rule.daysOfMonthList[0].negative.ShouldBeFalse(); + rule.daysOfMonthList[0].dayOfMonth.ShouldBe(15); + rule.daysOfMonthList[1].negative.ShouldBeFalse(); + rule.daysOfMonthList[1].dayOfMonth.ShouldBe(30); + } + } +}