Skip to content

Instantly share code, notes, and snippets.

@blwinters
Created December 17, 2017 21:37

Revisions

  1. blwinters created this gist Dec 17, 2017.
    1,533 changes: 1,533 additions & 0 deletions SMRecurrenceRuleTests.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1533 @@
    //
    // SMRecurrenceRuleTests.swift
    // Summit_iOS_Tests
    //
    // Created by Ben Winters on 9/6/17.
    // Copyright © 2017 Goals LLC. All rights reserved.
    //

    import EventKit
    import Foundation
    import XCTest
    @testable import Summit_iOS

    struct RecurrenceRuleTestModel {

    let firstStart: Date
    let firstEnd: Date

    let rangeStart: Date
    let rangeEnd: Date

    let strictRange: Bool

    init(firstStart: Date, firstEnd: Date, rangeEnd: Date, strictRange: Bool = true, cal: Calendar) {
    self.firstStart = firstStart
    self.firstEnd = firstEnd
    self.rangeStart = cal.startOfDay(for: firstStart)
    self.rangeEnd = rangeEnd
    self.strictRange = strictRange
    }

    }

    struct StartDayCountTestModel {
    var startDay: EKWeekday
    var expectedCount: Int
    }

    struct OrdinalWeekdayTestModel {
    var frequency: SMRecurrenceFrequency
    var weekNumber = 0
    var setPositions: [Int]?
    }

    // swiftlint:disable type_body_length
    class SMRecurrenceRuleTests: XCTestCase {

    let cal = Calendar(identifier: .gregorian)

    //For asserting the equivalency of two dates if using timeIntervalSinceReferenceDate
    /*
    let accuracy: Double = 0.001 //millisecond accuracy
    */

    func defaultStart() -> Date {
    return Date().withoutNanoseconds()
    }

    func defaultEnd(for date: Date) -> Date {
    return date.addHours(1)
    }

    //Use this when not testing the occurrenceCount or endDate
    func defaultRangeStart() -> Date {
    return Date().addMonths(-12)
    }

    //Use this when not testing the occurrenceCount or endDate
    func defaultRangeEnd() -> Date {
    return Date().addMonths(24)
    }

    override func setUp() {
    super.setUp()
    }

    override func tearDown() {
    super.tearDown()
    }

    func testDailyRecurrenceEndCount() {
    let firstStart = defaultStart()
    let firstEnd = defaultEnd(for: firstStart)
    let count = 50
    let rangeEnd = firstStart.addMonths(3)
    let ruleEnd = SMRecurrenceEnd(occurrenceCount: count)
    let rule = SMRecurrenceRule(frequency: .daily, interval: 1, end: ruleEnd)

    let testModels = [RecurrenceRuleTestModel(firstStart: firstStart, firstEnd: firstEnd, rangeEnd: rangeEnd, cal: cal),
    ]

    for (i, model) in testModels.enumerated() {
    let occurrences = rule.generateOccurrenceDates(firstStart: model.firstStart, firstEnd: model.firstEnd, exceptionDates: nil, from: model.firstStart.dayStart, upTo: model.rangeEnd, strictRange: model.strictRange)
    //print("Model \(i) occurrences: \(occurrences)")
    let expectedLastStart = model.firstStart.addDays(count - 1)

    let msg = "Model \(i)"
    XCTAssertEqual(occurrences.count, ruleEnd.occurrenceCount, msg)
    XCTAssertEqual(model.firstStart, occurrences.first?.start, msg)
    XCTAssertEqual(model.firstEnd, occurrences.first?.end, msg)
    XCTAssertEqual(expectedLastStart, occurrences.last?.start, msg)

    let occurrenceStartDates = occurrences.map({$0.start})
    XCTAssertTrue(occurrenceStartDates.contains(firstStart))
    }
    }

    func testDailyRecurrenceEndDate() {
    let firstStart = defaultStart()
    let firstEnd = defaultEnd(for: firstStart)
    let days = 5
    let rangeEnd = firstStart.addMonths(1)
    let ruleEnd = SMRecurrenceEnd(end: firstStart.addDays(days).dayEnd)
    let rule = SMRecurrenceRule(frequency: .daily, interval: 1, end: ruleEnd)

    let testModels = [RecurrenceRuleTestModel(firstStart: firstStart, firstEnd: firstEnd, rangeEnd: rangeEnd, cal: cal),
    RecurrenceRuleTestModel(firstStart: firstStart.dayEnd, firstEnd: firstEnd, rangeEnd: rangeEnd, strictRange: false, cal: cal),
    RecurrenceRuleTestModel(firstStart: firstStart.dayStart, firstEnd: firstEnd, rangeEnd: rangeEnd, cal: cal),
    ]

    for (i, model) in testModels.enumerated() {
    let expectedLastEnd = model.firstEnd.addDays(days)

    let occurrences = rule.generateOccurrenceDates(firstStart: model.firstStart, firstEnd: model.firstEnd, exceptionDates: nil, from: model.firstStart.dayStart, upTo: model.rangeEnd, strictRange: model.strictRange)
    //print("Model \(i) occurrences: \(occurrences)")
    let msg = "Model \(i)"
    XCTAssertEqual(occurrences.count, days + 1, msg)
    XCTAssertEqual(model.firstStart, occurrences.first?.start, msg)
    XCTAssertEqual(expectedLastEnd, occurrences.last?.end, msg)
    if model.strictRange {
    XCTAssertTrue(expectedLastEnd <= ruleEnd.endDate!.dayEnd, msg)
    }
    }
    }

    func testDailyStrictRange() {
    let firstStart = defaultStart().nextDayStart.addMinutes(-30)
    let firstEnd = defaultEnd(for: firstStart)
    let days = 5
    let rangeEnd = firstStart.addMonths(1)
    let ruleEnd = SMRecurrenceEnd(end: firstStart.addDays(days).dayEnd)
    let rule = SMRecurrenceRule(frequency: .daily, interval: 1, end: ruleEnd)

    let testModels = [RecurrenceRuleTestModel(firstStart: firstStart, firstEnd: firstEnd, rangeEnd: rangeEnd, strictRange: true, cal: cal),
    RecurrenceRuleTestModel(firstStart: firstStart, firstEnd: firstEnd, rangeEnd: rangeEnd, strictRange: false, cal: cal),
    ]

    for (i, model) in testModels.enumerated() {

    let occurrences = rule.generateOccurrenceDates(firstStart: model.firstStart, firstEnd: model.firstEnd, exceptionDates: nil, from: model.firstStart.dayStart, upTo: model.rangeEnd, strictRange: model.strictRange)
    //print("Model \(i) occurrences: \(occurrences)")
    let expectedCount = model.strictRange ? days : days + 1
    let expectedLastStart = model.firstStart.addDays(expectedCount - 1)
    let expectedLastEnd = defaultEnd(for: expectedLastStart)

    let msg = "Model \(i)"
    XCTAssertEqual(occurrences.count, expectedCount, msg)
    XCTAssertEqual(model.firstStart, occurrences.first?.start, msg)
    XCTAssertEqual(expectedLastEnd, occurrences.last?.end, msg)
    }
    }

    func testEvery3Days() {
    let firstStart = defaultStart()
    let firstEnd = defaultEnd(for: firstStart)
    //print("First start: \(firstStart.debugDescription), first end: \(firstEnd.debugDescription), duration: \(firstEnd.timeIntervalSince(firstStart))")
    let interval = 3
    let expectedCount = 11
    let rangeDays = Int((expectedCount - 1) * interval)

    let rangeEnd = firstStart.addDays(rangeDays).dayEnd
    let rule = SMRecurrenceRule(frequency: .daily, interval: interval, end: nil)

    let testModels = [RecurrenceRuleTestModel(firstStart: firstStart, firstEnd: firstEnd, rangeEnd: rangeEnd, strictRange: true, cal: cal),
    ]

    for (i, model) in testModels.enumerated() {

    let occurrences = rule.generateOccurrenceDates(firstStart: model.firstStart, firstEnd: model.firstEnd, exceptionDates: nil, from: model.firstStart.dayStart, upTo: model.rangeEnd, strictRange: model.strictRange)
    //print("Model \(i) occurrences: \(occurrences)")
    let expectedLastStart = model.firstStart.addDays(rangeDays)
    let expectedLastEnd = defaultEnd(for: expectedLastStart)

    //print("Expected last start: \(expectedLastStart), end: \(expectedLastEnd)")

    let msg = "Model \(i)"
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceEnd = occurrences.last?.end else {
    XCTFail("\(msg) occurrence was nil")
    continue
    }

    XCTAssertEqual(occurrences.count, expectedCount, msg)
    XCTAssertEqual(model.firstStart, firstOccurrenceStart, msg)
    XCTAssertEqual(expectedLastEnd, lastOccurrenceEnd, msg)
    }
    }

    //For these tests with a weekly repeating rule, the first occurrence is not necessarily on the specified weekday.
    //However, all other generated dates should follow it chronologically and have the correct weekday.

    func testEverySundayStartingSunday() {
    //This tests a single-day date range, to focus on a bug discovered in the Planner
    //The bug was created by using Monday as the default firstDayOfTheWeek, fixed it by changing to Sunday as the default

    let dayOfWeek = EKWeekday.sunday
    let startDate = Date.date(2017, 10, 8, time: 10, 0) //a Sunday

    let firstStartComps = cal.dateComponents([.hour, .minute, .second, .weekday, .weekOfYear, .yearForWeekOfYear], from: startDate)
    let firstStart = cal.date(from: firstStartComps)!
    let firstEnd = defaultEnd(for: firstStart)
    print("First start: \(firstStart), \(firstStartComps)")

    let expectedStart = startDate.addDays(21)

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: nil, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .weekly, interval: 1, end: nil, unitArrays: unitArrays) //daysOfTheWeek is not specified when it matches the startDate

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: expectedStart.dayStart, upTo: expectedStart.dayEnd, strictRange: true)

    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    //firstStart should equal the generated firstOccurrenceStart
    XCTAssertEqual(expectedStart, firstOccurrenceStart)
    XCTAssertEqual(occurrences.count, 1)

    //The first and last generated occurrences should both have a weekday that matches the dayOfWeek
    let firstOccurrenceWeekday = cal.dateComponents([.weekday], from: firstOccurrenceStart)
    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)

    XCTAssertEqual(dayOfWeek.rawValue, firstOccurrenceWeekday.weekday ?? 0)
    XCTAssertEqual(dayOfWeek.rawValue, lastOccurrenceWeekday.weekday ?? 0)
    }

    func testEverySundayStartingNotSunday() {
    //This tests a single-day date range, to focus on a bug discovered in the Planner
    //The bug was created by using Monday as the default firstDayOfTheWeek, fixed it by changing to Sunday as the default

    let weekday = EKWeekday.sunday
    let startDate = Date.date(2017, 10, 9, time: 10, 0) //a Monday

    let firstStartComps = cal.dateComponents([.hour, .minute, .second, .weekday, .weekOfYear, .yearForWeekOfYear], from: startDate)

    let firstStart = cal.date(from: firstStartComps)!
    let firstEnd = defaultEnd(for: firstStart)
    print("First start: \(firstStart), \(firstStartComps)")

    let expectedStart = startDate.addDays(20) //-1 day to get the Sunday that precedes the Monday

    let daysOfTheWeek = SMRecurrenceDayOfWeek.createDays(for: [weekday]) //need to specify the daysOfTheWeek since they don't match the startDate
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfTheWeek, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .weekly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: expectedStart.dayStart, upTo: expectedStart.dayEnd, strictRange: true)

    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    //firstStart should equal the generated firstOccurrenceStart
    XCTAssertEqual(expectedStart, firstOccurrenceStart)
    XCTAssertEqual(occurrences.count, 1)

    //The first and last generated occurrences should both have a weekday that matches the dayOfWeek
    let firstOccurrenceWeekday = cal.dateComponents([.weekday], from: firstOccurrenceStart)
    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)

    XCTAssertEqual(weekday.rawValue, firstOccurrenceWeekday.weekday ?? 0)
    XCTAssertEqual(weekday.rawValue, lastOccurrenceWeekday.weekday ?? 0)
    }

    func testEveryTuesdayStartingTuesday() {
    let now = defaultStart()

    var firstStartComps = cal.dateComponents([.hour, .minute, .second, .weekday, .weekOfYear, .yearForWeekOfYear], from: now)
    firstStartComps.setValue(EKWeekday.tuesday.rawValue, for: .weekday)
    let firstStart = cal.date(from: firstStartComps)!
    let firstEnd = defaultEnd(for: firstStart)
    //print("First start: \(firstStart), \(firstStartComps)")

    let dayOfWeek = SMRecurrenceDayOfWeek(.tuesday)

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .weekly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: defaultRangeStart(), upTo: defaultRangeEnd(), strictRange: true)

    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    //firstStart should equal the generated firstOccurrenceStart
    XCTAssertEqual(firstStart, firstOccurrenceStart)

    //First and last occurrences should not be the same
    XCTAssertNotEqual(firstOccurrenceStart, lastOccurrenceStart)

    //The first and last generated occurrences should both have a weekday that matches the dayOfWeek
    let firstOccurrenceWeekday = cal.dateComponents([.weekday], from: firstOccurrenceStart)
    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)

    XCTAssertEqual(dayOfWeek.dayOfTheWeek.rawValue, firstOccurrenceWeekday.weekday ?? 0)
    XCTAssertEqual(dayOfWeek.dayOfTheWeek.rawValue, lastOccurrenceWeekday.weekday ?? 0)
    }

    func testEveryTuesdayStartingNotTuesday() {
    let now = defaultStart()

    let startingWeekdays: [EKWeekday] = [.sunday, .monday, .wednesday, .thursday, .friday, .saturday]

    for ekWeekday in startingWeekdays {
    //Initial occurrence is not on the same day as the recurrence rule
    var firstStartComps = cal.dateComponents([.hour, .minute, .second, .weekday, .weekOfYear, .yearForWeekOfYear], from: now)
    firstStartComps.setValue(ekWeekday.rawValue, for: .weekday)
    let firstStart = cal.date(from: firstStartComps)!
    let firstEnd = defaultEnd(for: firstStart)
    print("First start: \(firstStart), \(firstStartComps)")

    //Subsequent occurences should be on Tuesdays
    let dayOfWeek = SMRecurrenceDayOfWeek(.tuesday)

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .weekly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: defaultRangeStart(), upTo: defaultRangeEnd(), strictRange: true)

    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    let msg = "\(ekWeekday.description)"

    //firstStart should equal the generated firstOccurrenceStart
    XCTAssertEqual(firstStart, firstOccurrenceStart, msg)

    //The firstStart should always precede the second occurrence.
    //The second occurrence may be in the same week if ekWeekday is .sunday or .monday
    let secondStart = occurrences[1].start
    print("Second start: \(secondStart)")
    XCTAssertTrue(firstStart < secondStart, msg)

    //First and last occurrences should not be the same
    XCTAssertNotEqual(firstOccurrenceStart, lastOccurrenceStart, msg)

    //The first and last generated occurrences should both have a weekday that matches the dayOfWeek
    let firstOccurrenceWeekday = cal.dateComponents([.weekday], from: firstOccurrenceStart)
    let secondOccurrenceWeekday = cal.dateComponents([.weekday], from: secondStart)
    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)

    XCTAssertNotEqual(dayOfWeek.dayOfTheWeek.rawValue, firstOccurrenceWeekday.weekday ?? 0, msg)
    XCTAssertEqual(dayOfWeek.dayOfTheWeek.rawValue, secondOccurrenceWeekday.weekday ?? 0, msg)
    XCTAssertEqual(dayOfWeek.dayOfTheWeek.rawValue, lastOccurrenceWeekday.weekday ?? 0, msg)
    }
    }

    func testEveryTuesdayDayRangeTuesday() {
    let now = defaultStart()

    var firstStartComps = cal.dateComponents([.hour, .minute, .second, .weekday, .weekOfYear, .yearForWeekOfYear], from: now)
    firstStartComps.setValue(EKWeekday.tuesday.rawValue, for: .weekday)
    let firstStart = cal.date(from: firstStartComps)!
    let firstEnd = defaultEnd(for: firstStart)
    //print("First start: \(firstStart), \(firstStartComps)")

    let secondStart = firstStart.addDays(7)

    let dayOfWeek = SMRecurrenceDayOfWeek(.tuesday)

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .weekly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: secondStart.dayStart, upTo: secondStart.dayEnd, strictRange: true)

    guard let firstOccurrenceStart = occurrences.first?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertNotEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(secondStart, firstOccurrenceStart)
    XCTAssertEqual(occurrences.count, 1)

    let firstOccurrenceWeekday = cal.dateComponents([.weekday], from: firstOccurrenceStart)
    XCTAssertEqual(firstOccurrenceWeekday.weekday ?? 0, dayOfWeek.dayOfTheWeek.rawValue)
    }

    func testEveryMWF() {
    let now = defaultStart()

    let units: [Calendar.Component] = [.hour, .minute, .second, .weekday, .weekOfYear, .yearForWeekOfYear]

    let rangeStart = now.addDays(-14) //arbitrary
    let unadjustedRangeEnd = now.addDays(21) //add three weeks to setup four-week range
    var rangeEndComps = cal.dateComponents(Set(units), from: unadjustedRangeEnd)
    rangeEndComps.setValue(EKWeekday.saturday.rawValue, for: .weekday) //end of week
    let rangeEnd = cal.date(from: rangeEndComps)!.dayEnd //end of day

    let testModels: [StartDayCountTestModel] = [
    StartDayCountTestModel(startDay: .sunday, expectedCount: 13),
    StartDayCountTestModel(startDay: .monday, expectedCount: 12),
    StartDayCountTestModel(startDay: .tuesday, expectedCount: 12),
    StartDayCountTestModel(startDay: .wednesday, expectedCount: 11),
    StartDayCountTestModel(startDay: .thursday, expectedCount: 11),
    StartDayCountTestModel(startDay: .friday, expectedCount: 10),
    StartDayCountTestModel(startDay: .saturday, expectedCount: 10),
    ]

    for model in testModels {
    var firstStartComps = cal.dateComponents(Set(units), from: now)
    firstStartComps.setValue(model.startDay.rawValue, for: .weekday)
    let firstStart = cal.date(from: firstStartComps)!
    let firstEnd = defaultEnd(for: firstStart)
    //print("First start: \(firstStart), \(firstStartComps)")

    let daysOfWeek = SMRecurrenceDayOfWeek.createDays(for: [.monday, .wednesday, .friday])

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .weekly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of occurrences starting \(model.startDay.description), \(firstStart)")

    guard let firstOccurrenceStart = occurrences.first?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    let msg = model.startDay.description
    XCTAssertEqual(occurrences.count, model.expectedCount, msg)

    let firstOccurrenceWeekday = cal.dateComponents([.weekday], from: firstOccurrenceStart)
    XCTAssertEqual(firstOccurrenceWeekday.weekday!, model.startDay.rawValue, msg)
    }
    }

    /**
    Repeating was missing items in beginning of the final week in the range
    if the starting weekday value was greater than the range end weekday value.
    */
    func testEveryMTWThFSPartialWeek() {

    let rangeStart = Date.date(2017, 9, 1)
    let rangeEnd = Date.date(2017, 10, 3).dayEnd //Tuesday

    let firstStart = Date.date(2017, 9, 20, time: 6, 0) //Wednesday
    let firstEnd = defaultEnd(for: firstStart)

    let daysOfWeek = SMRecurrenceDayOfWeek.createDays(for: [.monday, .tuesday, .wednesday, .thursday, .friday, .saturday])

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .weekly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of occurrences starting \(firstStart)")

    guard let firstOccurrenceStart = occurrences.first?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(occurrences.count, 12)

    let firstOccurrenceComps = cal.dateComponents([.weekday], from: firstOccurrenceStart)
    XCTAssertEqual(firstOccurrenceComps.weekday!, SMWeekday.wednesday.rawValue)
    }

    func testDayOfWeekValues() {
    XCTAssertEqual(Array(1...7), Array(EKWeekday.sunday.rawValue...EKWeekday.saturday.rawValue))
    XCTAssertEqual(Array(1...7), Array(SMWeekday.sunday.rawValue...SMWeekday.saturday.rawValue))
    }

    func testSingleOrdinalWeekday() {
    //use this only for its time components in this test
    let now = defaultStart()
    let year = 2018
    let weekdayOrdinal = 1
    let weekdayValue = 7

    var firstStartComps = cal.dateComponents([.hour, .minute, .second], from: now)
    firstStartComps.setValue(year, for: .year)
    firstStartComps.setValue(weekdayValue, for: .weekday)

    let months = Array(1...12)

    let expectedStartDates: [Date] = months.map({ month in
    firstStartComps.setValue(month, for: .month)
    firstStartComps.setValue(weekdayOrdinal, for: .weekdayOrdinal)
    let startDate = cal.date(from: firstStartComps)!
    return startDate
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday) //Calendar.app creates this rule using setPositions instead of weekNumber

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: [weekdayOrdinal])
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    let testCase = "Week: \(weekdayOrdinal), \(smWeekday.description)"
    print("End of occurrences starting \(rangeStart), \(testCase)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart, testCase)

    XCTAssertEqual(expectedStartDates.count, months.count, testCase)
    XCTAssertEqual(occurrences.count, months.count, testCase)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "\(testCase), Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceWeekday.weekday!, firstStartComps.weekday!, testCase)
    }

    func test2ndTuesdaySeptemberWeekNumberSetPositions() {
    //use this only for its time components in this test
    let now = defaultStart()
    let years = [2018, 2019, 2020]
    let monthsOfYear = [9]
    let weekdayOrdinal = 2
    let weekdayValue = EKWeekday.tuesday.rawValue

    var firstStartComps = cal.dateComponents([.hour, .minute, .second], from: now)
    firstStartComps.setValue(weekdayValue, for: .weekday)

    let expectedStartDates: [Date] = years.reduce([], { results, year in
    firstStartComps.setValue(year, for: .year)

    return results + monthsOfYear.flatMap({ month in
    firstStartComps.setValue(month, for: .month)
    firstStartComps.setValue(weekdayOrdinal, for: .weekdayOrdinal)
    return cal.date(from: firstStartComps)
    })
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.addYears(2).yearEnd

    let testModels = [OrdinalWeekdayTestModel(frequency: .monthly, weekNumber: weekdayOrdinal, setPositions: nil),
    OrdinalWeekdayTestModel(frequency: .yearly, weekNumber: weekdayOrdinal, setPositions: nil),
    OrdinalWeekdayTestModel(frequency: .monthly, weekNumber: 0, setPositions: [weekdayOrdinal]),
    OrdinalWeekdayTestModel(frequency: .yearly, weekNumber: 0, setPositions: [weekdayOrdinal]),
    ]

    for model in testModels {
    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday, weekNumber: model.weekNumber)

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: monthsOfYear, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: model.setPositions)

    //Frequency needs to be monthly when a set position is applied to single or multiple days of the week.
    //The initial interface shows yearly, but the underlying logic creates a rule with monthly frequency.
    let rule = SMRecurrenceRule(frequency: model.frequency, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)

    let usingSetPositions = model.setPositions != nil
    let testCase = "Frequency: \(model.frequency.description), Using set positions: \(usingSetPositions)"
    print("End of occurrences starting \(rangeStart), \(testCase)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart, testCase)

    let expectedCount = monthsOfYear.count * years.count
    XCTAssertEqual(expectedStartDates.count, expectedCount, testCase)
    XCTAssertEqual(occurrences.count, expectedCount, testCase)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "\(testCase), Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceWeekday.weekday!, firstStartComps.weekday!, testCase)
    }
    }

    /**
    This should skip any months without a 5th Thursday. It's common for calendar apps to not offer a "fifth" option,
    but the Calendar app on macOS does.
    */
    func test5thThursdayOfMonth() {
    let now = defaultStart()
    let year = 2018
    let weekdayOrdinal = 5
    let weekdayValue = EKWeekday.thursday.rawValue

    var firstStartComps = cal.dateComponents([.hour, .minute, .second], from: now)
    firstStartComps.setValue(year, for: .year)
    firstStartComps.setValue(weekdayValue, for: .weekday)

    let months = Array(1...12)

    let expectedStartDates: [Date] = months.flatMap({ month in
    firstStartComps.setValue(month, for: .month)
    firstStartComps.setValue(weekdayOrdinal, for: .weekdayOrdinal)
    return firstStartComps.isValidDate(in: cal) ? cal.date(from: firstStartComps) : nil
    })

    print("Expected start dates: \(expectedStartDates)")

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday, weekNumber: weekdayOrdinal) //Calendar.app creates this rule using setPositions instead of weekNumber

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)

    print("End of 5th Thursday occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)

    XCTAssertEqual(expectedStartDates.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceWeekday.weekday!, firstStartComps.weekday!)
    }

    func testAllOrdinalWeekdaysAsSetPositions() {
    //use this only for its time components in this test
    let now = defaultStart()
    let year = 2018

    //If users selects 5th week it should be stored as last, i.e. setPostions: [-1], but both are supported for synced rules
    for weekdayOrdinal in [1, 2, 3, 4, 5, -1] {
    for weekdayValue in 1...7 {

    var firstStartComps = cal.dateComponents([.hour, .minute, .second], from: now)
    firstStartComps.setValue(year, for: .year)
    firstStartComps.setValue(weekdayValue, for: .weekday)

    let months = Array(1...12)

    let expectedStartDates: [Date] = months.flatMap({ month in
    firstStartComps.setValue(month, for: .month)
    firstStartComps.setValue(weekdayOrdinal, for: .weekdayOrdinal)

    if weekdayOrdinal < 0 {
    return cal.date(from: firstStartComps) //isValidDate() returns false for -1
    } else {
    return firstStartComps.isValidDate(in: cal) ? cal.date(from: firstStartComps) : nil
    }
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday) //Calendar.app creates this rule using setPositions instead of weekNumber

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: [weekdayOrdinal])
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    let testCase = "Week: \(weekdayOrdinal), \(smWeekday.description)"
    print("End of occurrences starting \(rangeStart), \(testCase)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil, \(testCase)")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart, testCase)

    XCTAssertEqual(occurrences.count, expectedStartDates.count, testCase)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "\(testCase), Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceWeekday.weekday!, firstStartComps.weekday!, testCase)
    }
    }
    }

    func testAllOrdinalWeekdaysAsWeekNumbers() {
    //use this only for its time components in this test
    let now = defaultStart()
    let year = 2018

    //If users selects 5th week it should be stored as last, i.e. setPostions: [-1]
    for weekdayOrdinal in [1, 2, 3, 4, 5, -1] {
    for weekdayValue in 1...7 {

    var firstStartComps = cal.dateComponents([.hour, .minute, .second], from: now)
    firstStartComps.setValue(year, for: .year)
    firstStartComps.setValue(weekdayValue, for: .weekday)

    let months = Array(1...12)

    let expectedStartDates: [Date] = months.flatMap({ month in
    firstStartComps.setValue(month, for: .month)
    firstStartComps.setValue(weekdayOrdinal, for: .weekdayOrdinal)

    if weekdayOrdinal < 0 {
    return cal.date(from: firstStartComps) //isValidDate() returns false for -1
    } else {
    return firstStartComps.isValidDate(in: cal) ? cal.date(from: firstStartComps) : nil
    }
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday, weekNumber: weekdayOrdinal) //Calendar.app creates this rule using setPositions instead of weekNumber

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    let testCase = "Week: \(weekdayOrdinal), \(smWeekday.description)"
    print("End of occurrences starting \(rangeStart), \(testCase)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil, \(testCase)")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart, testCase)

    XCTAssertEqual(expectedStartDates.count, occurrences.count, testCase)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "\(testCase), Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceWeekday.weekday!, firstStartComps.weekday!, testCase)
    }
    }
    }

    /**
    Tests each of the 7 days of the week independently.
    */
    func testLastSingleWeekday() {
    //use this only for its time components in this test
    let now = defaultStart()
    let year = 2018

    let yearStartComps = DateComponents(year: year, month: 1, day: 1, hour: 0, minute: 0, second: 0)
    let yearStart = cal.date(from: yearStartComps)!

    let months = Array(1...12)
    let monthStartDates: [Date] = months.map({ month in
    var monthStartComps = yearStartComps
    monthStartComps.setValue(month, for: .month)
    return cal.date(from: monthStartComps)!
    })

    for weekdayValue in 1...7 {

    //Use .nextMonthStart instead of .monthEnd so that enumerateDates() returns the correct date when match is on last day of month
    let nextMonthDates = monthStartDates.map({$0.nextMonthStart})
    print("Next month dates: \(nextMonthDates)")
    let monthDateRanges = zip(monthStartDates, nextMonthDates)

    var compsToMatch = cal.dateComponents([.hour, .minute, .second], from: now)
    compsToMatch.setValue(weekdayValue, for: .weekday)

    let expectedStartDates: [Date] = monthDateRanges.flatMap({ monthStart, nextMonthStart in

    var matchingDate: Date?
    cal.enumerateDates(startingAfter: nextMonthStart, matching: compsToMatch, matchingPolicy: .nextTimePreservingSmallerComponents, direction: .backward, using: { (date, isExact, stop) in
    if let foundDate = date {
    stop = true
    matchingDate = foundDate
    print("Found date: \(foundDate)")
    }
    })
    return matchingDate
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday) //Calendar.app creates this rule using setPositions instead of weekNumber

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: [-1])
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of \(smWeekday.description) occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)

    XCTAssertEqual(expectedStartDates.count, months.count)
    XCTAssertEqual(occurrences.count, months.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceWeekday.weekday!, weekdayValue)
    }
    }

    func testLastMondaySingleDayRange() {
    //use this only for its time components in this test
    let now = defaultStart()
    let year = 2017
    let weekdayValue = EKWeekday.monday.rawValue
    let setPositions = [-1]

    var timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedStartDates: [Date] = Array(1...12).flatMap({ month in
    let dateComps = DateComponents(year: year, month: month, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second, weekday: weekdayValue, weekdayOrdinal: setPositions.first!)
    return cal.date(from: dateComps)
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)

    //The test results depend on using a single day period
    let rangeStart = Date.date(year, 11, 27)
    let rangeEnd = rangeStart.dayEnd

    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday) //Calendar.app creates this rule using setPositions instead of weekNumber

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: false)
    print("End of \(smWeekday.description) occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(occurrences.count, 1)
    XCTAssertTrue(expectedStartDates.contains(firstOccurrenceStart))
    }

    func test1st27thLastMondaysOfYear() {
    let now = defaultStart()
    let year = 2018
    let weekdayValue = EKWeekday.monday.rawValue
    let setPositions = [1, 27, -1]

    let yearStartComps = DateComponents(year: year, month: 1, day: 1, hour: 0, minute: 0, second: 0)
    let yearStart = cal.date(from: yearStartComps)!

    var sharedComps = cal.dateComponents([.hour, .minute, .second], from: now)
    sharedComps.setValue(weekdayValue, for: .weekday)

    var firstStartComps = sharedComps
    firstStartComps.setValue(year, for: .yearForWeekOfYear)
    firstStartComps.setValue(1, for: .weekOfYear)

    let firstStart = cal.date(from: firstStartComps)!
    print("First start: \(firstStart)")
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = yearStart
    let rangeEnd = rangeStart.yearEnd

    //Manually checked values for 2018
    let expectedDateComps = [DateComponents(month: 1, day: 1),
    DateComponents(month: 7, day: 2),
    DateComponents(month: 12, day: 31),
    ]
    let expectedStartDates: [Date] = expectedDateComps.flatMap({ expectedComps in
    let combinedComps = DateComponents(year: year, month: expectedComps.month, day: expectedComps.day, hour: sharedComps.hour, minute: sharedComps.minute, second: sharedComps.second)
    return cal.date(from: combinedComps)
    })

    //Create rule
    let smWeekday = SMWeekday(rawValue: weekdayValue)!
    let dayOfWeek = SMRecurrenceDayOfWeek(smWeekday) //Calendar.app creates this rule using setPositions instead of weekNumber

    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)
    let rule = SMRecurrenceRule(frequency: .yearly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of \(smWeekday.description) occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)

    XCTAssertEqual(expectedDateComps.count, occurrences.count)
    XCTAssertEqual(occurrences.count, setPositions.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceWeekday.weekday!, weekdayValue)
    }

    func testLastWeekdayOfYear() {
    let now = defaultStart() //to get random time of day
    let firstYear = 2016
    let weekdays: [EKWeekday] = [.monday, .tuesday, .wednesday, .thursday, .friday]
    let setPositions = [-1]

    var timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let yearStartComps = DateComponents(year: firstYear, month: 1, day: 1, hour: 0, minute: 0, second: 0)
    let yearStart = cal.date(from: yearStartComps)!

    //Manually checked values
    let expectedDateComps = [DateComponents(year: 2016, month: 12, day: 30),
    DateComponents(year: 2017, month: 12, day: 29),
    DateComponents(year: 2018, month: 12, day: 31),
    DateComponents(year: 2019, month: 12, day: 31),
    ]

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ expectedComps in
    let combinedComps = DateComponents(year: expectedComps.year, month: expectedComps.month, day: expectedComps.day, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return cal.date(from: combinedComps)
    })

    let firstStart = expectedStartDates.first!
    print("First start: \(firstStart)")
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = yearStart
    let rangeEnd = yearStart.addYears(3).yearEnd //4-year range

    //Create rule
    let daysOfWeek = SMRecurrenceDayOfWeek.createDays(for: weekdays)
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)
    let rule = SMRecurrenceRule(frequency: .yearly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of \(weekdays.map({$0.description})) occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDateComps.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertTrue(weekdays.map({$0.rawValue}).contains(lastOccurrenceWeekday.weekday!))
    }

    func testLastWednesdayOfDecember() {
    let now = defaultStart() //to get random time of day
    let firstYear = 2017
    let weekdays: [EKWeekday] = [.wednesday]
    let monthsOfTheYear = [12]

    var timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let yearStartComps = DateComponents(year: firstYear, month: 1, day: 1, hour: 0, minute: 0, second: 0)
    let yearStart = cal.date(from: yearStartComps)!

    //Manually checked values
    let expectedDateComps = [DateComponents(year: 2017, month: 12, day: 27),
    DateComponents(year: 2018, month: 12, day: 26),
    DateComponents(year: 2019, month: 12, day: 25),
    ]

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ expectedComps in
    let combinedComps = DateComponents(year: expectedComps.year, month: expectedComps.month, day: expectedComps.day, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return cal.date(from: combinedComps)
    })

    let firstStart = expectedStartDates.first!
    print("First start: \(firstStart)")
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = yearStart
    let rangeEnd = yearStart.addYears(2).yearEnd //3-year range

    //Create rule
    let daysOfWeek = SMRecurrenceDayOfWeek(.wednesday, weekNumber: -1)
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: [daysOfWeek], daysOfTheMonth: nil, monthsOfTheYear: monthsOfTheYear, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .yearly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of \(weekdays.map({$0.description})) occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDateComps.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceWeekday = cal.dateComponents([.weekday], from: lastOccurrenceStart)
    XCTAssertTrue(weekdays.map({$0.rawValue}).contains(lastOccurrenceWeekday.weekday!))
    }

    func test1st15thMonthly() {
    let now = defaultStart()
    let year = now.yearInt
    let months = Array(1...12)
    let daysOfMonth = [1, 15]
    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps: [DateComponents] = months.reduce( [], { results, month in
    var dateComps = DateComponents(year: year, month: month, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return results + daysOfMonth.map({
    dateComps.setValue($0, for: .day)
    return dateComps
    })
    })

    let expectedStartDates = expectedDateComps.flatMap({cal.date(from: $0)})

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: nil, daysOfTheMonth: daysOfMonth, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of days of month \(daysOfMonth) occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedStartDates.count, occurrences.count)
    XCTAssertEqual(occurrences.count, (months.count * daysOfMonth.count))

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, daysOfMonth.last!)
    }

    func test1stOfJanuaryJuly() {
    let now = defaultStart()
    let year = now.yearInt
    let months = [1, 7]
    let daysOfMonth = [1]
    let frequencies: [SMRecurrenceFrequency] = [.monthly, .yearly]

    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps: [DateComponents] = months.reduce( [], { results, month in
    var dateComps = DateComponents(year: year, month: month, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return results + daysOfMonth.map({
    dateComps.setValue($0, for: .day)
    return dateComps
    })
    })

    let expectedStartDates = expectedDateComps.flatMap({cal.date(from: $0)})

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    for frequency in frequencies {
    //Create rule
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: nil, daysOfTheMonth: daysOfMonth, monthsOfTheYear: months, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: frequency, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of days of month \(daysOfMonth) occurrences starting \(rangeStart)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedStartDates.count, occurrences.count)
    XCTAssertEqual(occurrences.count, (months.count * daysOfMonth.count))

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, daysOfMonth.last!)
    }
    }

    func testLastDayOfTheMonth() {
    let now = defaultStart()
    let year = 2016 //to test leap day
    let weekdays: [EKWeekday] = [.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday]
    let setPositions = [-1]

    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps = [DateComponents(year: year, month: 1, day: 31),
    DateComponents(year: year, month: 2, day: 29),
    DateComponents(year: year, month: 3, day: 31),
    DateComponents(year: year, month: 4, day: 30),
    DateComponents(year: year, month: 5, day: 31),
    DateComponents(year: year, month: 6, day: 30),
    DateComponents(year: year, month: 7, day: 31),
    DateComponents(year: year, month: 8, day: 31),
    DateComponents(year: year, month: 9, day: 30),
    DateComponents(year: year, month: 10, day: 31),
    DateComponents(year: year, month: 11, day: 30),
    DateComponents(year: year, month: 12, day: 31),
    ]

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ expectedComps in
    let combinedComps = DateComponents(year: expectedComps.year, month: expectedComps.month, day: expectedComps.day, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return cal.date(from: combinedComps)
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let daysOfWeek = SMRecurrenceDayOfWeek.createDays(for: weekdays)
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of last day of month occurrences starting \(rangeStart), count: \(occurrences.count)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDateComps.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, expectedDateComps.last!.day!)
    }

    func testLastDayOfEachQuarter() {
    let now = defaultStart()
    let year = 2018
    let weekdays: [EKWeekday] = [.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday]
    let monthValues = [3, 6, 9, 12]
    let setPositions = [-1]

    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps = [DateComponents(year: year, month: 3, day: 31),
    DateComponents(year: year, month: 6, day: 30),
    DateComponents(year: year, month: 9, day: 30),
    DateComponents(year: year, month: 12, day: 31),
    ]

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ expectedComps in
    let combinedComps = DateComponents(year: expectedComps.year, month: expectedComps.month, day: expectedComps.day, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return cal.date(from: combinedComps)
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let daysOfWeek = SMRecurrenceDayOfWeek.createDays(for: weekdays)
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: monthValues, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)

    //Frequency needs to be monthly when a set position is applied to single or multiple days of the week.
    //The initial interface shows yearly, but the underlying logic creates a rule with monthly frequency.
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    measure {
    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)

    print("End of last day of month occurrences starting \(rangeStart), count: \(occurrences.count)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDateComps.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, expectedDateComps.last!.day!)
    }
    }

    func test1stWeekdayOfEachQuarter() {
    let now = defaultStart()
    let year = 2018
    let weekdays: [EKWeekday] = [.monday, .tuesday, .wednesday, .thursday, .friday]
    let monthValues = [1, 4, 7, 10]
    let setPositions = [1]

    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps = [DateComponents(year: year, month: 1, day: 1),
    DateComponents(year: year, month: 4, day: 2),
    DateComponents(year: year, month: 7, day: 2),
    DateComponents(year: year, month: 10, day: 1),
    ]

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ expectedComps in
    let combinedComps = DateComponents(year: expectedComps.year, month: expectedComps.month, day: expectedComps.day, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return cal.date(from: combinedComps)
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let daysOfWeek = SMRecurrenceDayOfWeek.createDays(for: weekdays)
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: monthValues, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)

    //Frequency needs to be monthly when a set position is applied to single or multiple days of the week.
    //The initial interface shows yearly, but the underlying logic creates a rule with monthly frequency.
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    measure {
    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)

    print("End of last day of month occurrences starting \(rangeStart), count: \(occurrences.count)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDateComps.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, expectedDateComps.last!.day!)
    }
    }

    func test2ndWeekdayOfJune() {
    let now = defaultStart()
    let weekdays: [EKWeekday] = [.monday, .tuesday, .wednesday, .thursday, .friday]
    let monthValues = [6]
    let setPositions = [2]

    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps = [DateComponents(year: 2018, month: 6, day: 4), //June 2018 starts on Friday, so 2nd weekday is Monday the 4th
    DateComponents(year: 2019, month: 6, day: 4),
    DateComponents(year: 2020, month: 6, day: 2),
    ]

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ expectedComps in
    let combinedComps = DateComponents(year: expectedComps.year, month: expectedComps.month, day: expectedComps.day, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    return cal.date(from: combinedComps)
    })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.addYears(2).yearEnd

    //Create rule
    let daysOfWeek = SMRecurrenceDayOfWeek.createDays(for: weekdays)
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: monthValues, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)

    //Frequency needs to be monthly when a set position is applied to single or multiple days of the week.
    //The initial interface shows yearly, but the underlying logic creates a rule with monthly frequency.
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)

    print("End of last day of month occurrences starting \(rangeStart), count: \(occurrences.count)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDateComps.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, expectedDateComps.last!.day!)
    }

    /*
    The results should skip February in accordance with the RFC specifications.
    This is different from monthly repeating where the first instance is on the 30th.

    int/freq: Every 1 month
    daysOfTheMonth: [30]
    */
    func test30thOfEachMonth() {
    let now = defaultStart()
    let year = 2018
    let daysOfMonth = [30]

    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps: [DateComponents] = Array(1...12).flatMap({ month in
    guard month != 2 else { return nil}
    return DateComponents(year: year, month: month, day: daysOfMonth.first, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    })

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ cal.date(from: $0) })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: nil, daysOfTheMonth: daysOfMonth, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil)
    let rule = SMRecurrenceRule(frequency: .monthly, interval: 1, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of 30th of month occurrences starting \(rangeStart), count: \(occurrences.count)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDateComps.count, occurrences.count)

    let testPairs = zip(occurrences, expectedStartDates)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, expectedDateComps.last!.day!)
    }

    /*
    int/freq: Every 1 month
    daysOfTheMonth: [28, 29, 30]
    setPositions: [-1]

    This represents monthly repeating where the first instance is on the 30th.
    The results should include February 28/29 instead of skipping it, as would be the case if daysOfTheMonth = [30]
    This function tests with intervals of 1, 2, and 3.
    */
    func testMonthlyRepeatingStartingOn30th() {
    let now = defaultStart()
    let year = 2018
    let daysOfMonth = [28, 29, 30]
    let setPositions = [-1]
    let intervals = [1, 2, 3]

    let timeComps = cal.dateComponents([.hour, .minute, .second], from: now)

    let expectedDateComps: [DateComponents] = Array(1...12).map({ month in
    let expectedDay = (month == 2) ? 28 : 30
    return DateComponents(year: year, month: month, day: expectedDay, hour: timeComps.hour, minute: timeComps.minute, second: timeComps.second)
    })

    let expectedStartDates: [Date] = expectedDateComps.flatMap({ cal.date(from: $0) })

    let firstStart = expectedStartDates.first!
    let firstEnd = defaultEnd(for: firstStart)
    let rangeStart = firstStart.yearStart
    let rangeEnd = rangeStart.yearEnd

    //Create rule
    let unitArrays = SMRecurrenceRuleUnitArrays(daysOfTheWeek: nil, daysOfTheMonth: daysOfMonth, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: setPositions)

    for interval in intervals {
    let expectedDatesForInterval = expectedStartDates.filter({ date in
    return ((date.monthInt - 1 + interval) % interval) == 0
    })

    let rule = SMRecurrenceRule(frequency: .monthly, interval: interval, end: nil, unitArrays: unitArrays)

    let occurrences = rule.generateOccurrenceDates(firstStart: firstStart, firstEnd: firstEnd, exceptionDates: nil, from: rangeStart, upTo: rangeEnd, strictRange: true)
    print("End of 30th of month occurrences starting \(rangeStart), count: \(occurrences.count)")

    //Tests
    guard let firstOccurrenceStart = occurrences.first?.start, let lastOccurrenceStart = occurrences.last?.start else {
    XCTFail("Occurrence was nil")
    return
    }

    XCTAssertEqual(firstStart, firstOccurrenceStart)
    XCTAssertEqual(expectedDatesForInterval.count, occurrences.count)

    let testPairs = zip(occurrences, expectedDatesForInterval)

    for (i, (occurrence, expectedStart)) in testPairs.enumerated() {
    XCTAssertEqual(occurrence.start, expectedStart, "Pair \(i)")
    }

    let lastOccurrenceDay = cal.dateComponents([.day], from: lastOccurrenceStart)
    XCTAssertEqual(lastOccurrenceDay.day!, expectedDatesForInterval.last!.dayOfMonthInt)
    }
    }

    }