Skip to content

Instantly share code, notes, and snippets.

@blwinters
Created December 17, 2017 21:37
Show Gist options
  • Save blwinters/d4a49b99584e5a5e4eb7809adbba35ba to your computer and use it in GitHub Desktop.
Save blwinters/d4a49b99584e5a5e4eb7809adbba35ba to your computer and use it in GitHub Desktop.
Unit tests for generating dates from SMRecurrenceRule.
//
// 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)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment