diff --git a/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift b/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift index bfd6f0de..d548312b 100644 --- a/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift +++ b/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift @@ -30,46 +30,69 @@ open class TimePeriodChain: TimePeriodGroup { // MARK: - Chain Existence Manipulation /** - * Append a TimePeriodProtocol to the periods array and update the Chain's - * beginning and end. + * Adds a period of equivalent length to the end of the chain, regardless of + * whether the period intersects with the chain or not. * * - parameter period: TimePeriodProtocol to add to the collection */ public func append(_ period: TimePeriodProtocol) { - let beginning = (periods.count > 0) ? periods.last!.end! : period.start - - let newPeriod = TimePeriod(start: beginning!, duration: period.duration) - periods.append(newPeriod) - - //Update updateExtremes - if periods.count == 1 { - start = period.start - end = period.end - } else { - end = end?.addingTimeInterval(period.duration) + guard isPeriodHasExtremes(period) else { + print("All TimePeriods in a TimePeriodChain must contain a defined start and end date") + return; + } + + if let startDate = periods.last?.end! ?? period.start { + let newPeriod = TimePeriod(start: startDate, duration: period.duration) + periods.append(newPeriod) + + updateExtremes() } } - + /** - * Append a TimePeriodProtocol array to the periods array and update the Chain's - * beginning and end. + * Adds a periods of equivalent length of group to the end of the chain, regardless of + * whether the period intersects with the chain or not. * * - parameter periodArray: TimePeriodProtocol list to add to the collection */ public func append(contentsOf group: G) { for period in group.periods { - let beginning = (periods.count > 0) ? periods.last!.end! : period.start - - let newPeriod = TimePeriod(start: beginning!, duration: period.duration) - periods.append(newPeriod) - - //Update updateExtremes - if periods.count == 1 { - start = period.start - end = period.end - } else { - end = end?.addingTimeInterval(period.duration) - } + append(period) + } + } + + /** + * Adds a period of equivalent length to the start of the chain, regardless of + * whether the period intersects with the chain or not. + * + * - parameter period: TimePeriodProtocol to add to the collection + */ + public func prepend(_ period: TimePeriodProtocol) { + guard isPeriodHasExtremes(period) else { + print("All TimePeriods in a TimePeriodChain must contain a defined start and end date") + return; + } + + if let endDate = periods.first?.start! ?? period.end { + let startDate = endDate.addingTimeInterval(-period.duration) + + let newPeriod = TimePeriod(start: startDate, duration: period.duration) + periods.insert(newPeriod, at: periods.startIndex) + + updateExtremes() + } + } + + /** + * Adds a periods of equivalent length of group to the start of the chain, regardless of + * whether the period intersects with the chain or not. + * + * - parameter periodArray: TimePeriodProtocol list to add to the collection + */ + + public func prepend(contentsOf group: G) { + for period in group.periods { + prepend(period) } } @@ -138,6 +161,26 @@ open class TimePeriodChain: TimePeriodGroup { start = start?.addingTimeInterval(duration) end = end?.addingTimeInterval(duration) } + + /// Shifts chain's start date and all chain's periods to the given date + /// + /// - Parameter date: The date to which the period's start is shifted + public func shiftStart(to date: DateInRegion) { + if let firstPeriodStart = periods.first?.start! { + let difference = date - firstPeriodStart + shift(by: difference) + } + } + + /// Shifts chain's end date and all chain's periods to the given date + /// + /// - Parameter date: The date to which the period's end is shifted + public func shiftEnd(to date: DateInRegion) { + if let firstPeriodEnd = periods.last?.end! { + let difference = date - firstPeriodEnd + shift(by: difference) + } + } public override func map(_ transform: (TimePeriodProtocol) throws -> T) rethrows -> [T] { return try periods.map(transform) @@ -163,5 +206,9 @@ open class TimePeriodChain: TimePeriodGroup { start = periods.first?.start end = periods.last?.end } + + internal func isPeriodHasExtremes (_ period: TimePeriodProtocol) -> Bool { + period.start != nil && period.end != nil + } } diff --git a/Tests/SwiftDateTests/TestTimePeriodChain.swift b/Tests/SwiftDateTests/TestTimePeriodChain.swift index 68e120ff..a53f899b 100644 --- a/Tests/SwiftDateTests/TestTimePeriodChain.swift +++ b/Tests/SwiftDateTests/TestTimePeriodChain.swift @@ -43,4 +43,197 @@ class TestTimePeriodChain: XCTestCase { XCTAssert(chain.periods[3].duration == originalDuration_2, "Unexpected duration of 3 period – Actual: \(chain.periods[3].duration) – Expected: \(originalDuration_2)") XCTAssert(chain.periods[4].duration == originalDuration_3, "Unexpected duration of 4 period – Actual: \(chain.periods[4].duration) – Expected: \(originalDuration_3)") } + + func testPrependPeriod () { + let period = TimePeriod(start: .init(year: 2020, month: 1, day: 1), end: .init(year: 2020, month: 1, day: 20)) + let chain = TimePeriodChain(periods) + + chain.prepend(period) + + XCTAssert( + chain[0].start == DateInRegion(year: 2020, month: 1, day: 13), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[0].start!) – Expected: \(DateInRegion(year: 2020, month: 1, day: 13))" + ) + + XCTAssert( + chain[1].start == DateInRegion(year: 2020, month: 2, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[1].start!) – Expected: \(DateInRegion(year: 2020, month: 2, day: 1))" + ) + + XCTAssert( + chain[2].start == DateInRegion(year: 2020, month: 3, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[2].start!) – Expected: \(DateInRegion(year: 2020, month: 3, day: 1))" + ) + + XCTAssert( + chain[3].start == DateInRegion(year: 2020, month: 4, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[3].start!) – Expected: \(DateInRegion(year: 2020, month: 4, day: 1))" + ) + + XCTAssert( + chain[4].start == DateInRegion(year: 2020, month: 5, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[4].start!) – Expected: \(DateInRegion(year: 2020, month: 5, day: 1))" + ) + } + + func testPrependGroup () { + let testPeriods = [ + TimePeriod(start: .init(year: 2019, month: 10, day: 1), end: .init(year: 2019, month: 10, day: 31)), + TimePeriod(start: .init(year: 2019, month: 11, day: 1), end: .init(year: 2019, month: 11, day: 30)), + TimePeriod(start: .init(year: 2019, month: 12, day: 1), end: .init(year: 2019, month: 12, day: 31)), + ] + let group = TimePeriodGroup(testPeriods) + let chain = TimePeriodChain(periods) + + chain.prepend(contentsOf: group) + + XCTAssert( + chain[0].start == DateInRegion(year: 2019, month: 11, day: 4), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[0].start!) – Expected: \(DateInRegion(year: 2019, month: 11, day: 1))" + ) + + XCTAssert( + chain[1].start == DateInRegion(year: 2019, month: 12, day: 4), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[1].start!) – Expected: \(DateInRegion(year: 2019, month: 12, day: 4))" + ) + + XCTAssert( + chain[2].start == DateInRegion(year: 2020, month: 1, day: 2), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[2].start!) – Expected: \(DateInRegion(year: 2020, month: 1, day: 2))" + ) + + XCTAssert( + chain[3].start == DateInRegion(year: 2020, month: 2, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[3].start!) – Expected: \(DateInRegion(year: 2020, month: 2, day: 1))" + ) + + XCTAssert( + chain[4].start == DateInRegion(year: 2020, month: 3, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[4].start!) – Expected: \(DateInRegion(year: 2020, month: 3, day: 1))" + ) + + XCTAssert( + chain[5].start == DateInRegion(year: 2020, month: 4, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[5].start!) – Expected: \(DateInRegion(year: 2020, month: 4, day: 1))" + ) + + XCTAssert( + chain[6].start == DateInRegion(year: 2020, month: 5, day: 1), + "Prepended TimePeriodChain contains unexpected period start date – Actual: \(chain[6].start!) – Expected: \(DateInRegion(year: 2020, month: 5, day: 1))" + ) + } + + func testAppendPeriod () { + let period = TimePeriod(start: .init(year: 2020, month: 1, day: 1), end: .init(year: 2020, month: 1, day: 20)) + let chain = TimePeriodChain(periods) + + chain.append(period) + + XCTAssert( + chain[0].end == DateInRegion(year: 2020, month: 2, day: 28), + "Appended TimePeriodChain contains unexpected period end date – Actual: \(chain[0].end!) – Expected: \(DateInRegion(year: 2020, month: 2, day: 28))" + ) + + XCTAssert( + chain[1].end == DateInRegion(year: 2020, month: 3, day: 31), + "Appended TimePeriodChain contains unexpected period end date – Actual: \(chain[1].end!) – Expected: \(DateInRegion(year: 2020, month: 3, day: 31))" + ) + + XCTAssert( + chain[2].end == DateInRegion(year: 2020, month: 4, day: 30), + "Appended TimePeriodChain contains unexpected period end date – Actual: \(chain[2].end!) – Expected: \(DateInRegion(year: 2020, month: 4, day: 30))" + ) + + XCTAssert( + chain[3].end == DateInRegion(year: 2020, month: 5, day: 31), + "Appended TimePeriodChain contains unexpected period end date – Actual: \(chain[3].end!) – Expected: \(DateInRegion(year: 2020, month: 5, day: 31))" + ) + + XCTAssert( + chain[4].end == DateInRegion(year: 2020, month: 6, day: 19), + "Appended TimePeriodChain contains unexpected period end date – Actual: \(chain[4].end!) – Expected: \(DateInRegion(year: 2020, month: 6, day: 19))" + ) + } + + func testAppendGroup () { + let testPeriods = [ + TimePeriod(start: .init(year: 2019, month: 10, day: 1), end: .init(year: 2019, month: 10, day: 31)), + TimePeriod(start: .init(year: 2019, month: 11, day: 1), end: .init(year: 2019, month: 11, day: 30)), + TimePeriod(start: .init(year: 2019, month: 12, day: 1), end: .init(year: 2019, month: 12, day: 31)), + ] + let group = TimePeriodGroup(testPeriods) + let chain = TimePeriodChain(periods) + + chain.append(contentsOf: group) + + XCTAssert( + chain[0].end == DateInRegion(year: 2020, month: 2, day: 28), + "Prepended TimePeriodChain contains unexpected period end date – Actual: \(chain[0].end!) – Expected: \(DateInRegion(year: 2020, month: 2, day: 28))" + ) + + XCTAssert( + chain[1].end == DateInRegion(year: 2020, month: 3, day: 31), + "Prepended TimePeriodChain contains unexpected period end date – Actual: \(chain[1].end!) – Expected: \(DateInRegion(year: 2020, month: 3, day: 31))" + ) + + XCTAssert( + chain[2].end == DateInRegion(year: 2020, month: 4, day: 30), + "Prepended TimePeriodChain contains unexpected period end date – Actual: \(chain[2].end!) – Expected: \(DateInRegion(year: 2020, month: 4, day: 30))" + ) + + XCTAssert( + chain[3].end == DateInRegion(year: 2020, month: 5, day: 31), + "Prepended TimePeriodChain contains unexpected period end date – Actual: \(chain[3].end!) – Expected: \(DateInRegion(year: 2020, month: 5, day: 31))" + ) + + XCTAssert( + chain[4].end == DateInRegion(year: 2020, month: 6, day: 30), + "Prepended TimePeriodChain contains unexpected period end date – Actual: \(chain[4].end!) – Expected: \(DateInRegion(year: 2020, month: 6, day: 30))" + ) + + XCTAssert( + chain[5].end == DateInRegion(year: 2020, month: 7, day: 29), + "Prepended TimePeriodChain contains unexpected period end date – Actual: \(chain[5].end!) – Expected: \(DateInRegion(year: 2020, month: 7, day: 29))" + ) + + XCTAssert( + chain[6].end == DateInRegion(year: 2020, month: 8, day: 28), + "Prepended TimePeriodChain contains unexpected period end date – Actual: \(chain[6].end!) – Expected: \(DateInRegion(year: 2020, month: 8, day: 28))" + ) + } + + func testShiftStartToDate () { + let date = DateInRegion(year: 2020, month: 1, day: 1) + let chain = TimePeriodChain(periods) + + chain.shiftStart(to: date) + + XCTAssert( + chain.start == DateInRegion(year: 2020, month: 1, day: 1), + "Shifted TimePeriodChain has unexpected start date – Actual: \(chain.start!) – Expected: \(DateInRegion(year: 2020, month: 1, day: 1))" + ) + + XCTAssert( + chain.end == DateInRegion(year: 2020, month: 4, day: 30), + "Shifted TimePeriodChain has unexpected end date – Actual: \(chain.end!) – Expected: \(DateInRegion(year: 2020, month: 4, day: 30))" + ) + } + + func testShiftEndToDate () { + let date = DateInRegion(year: 2020, month: 1, day: 1) + let chain = TimePeriodChain(periods) + + chain.shiftEnd(to: date) + + XCTAssert( + chain.start == DateInRegion(year: 2019, month: 9, day: 3), + "Shifted TimePeriodChain has unexpected start date – Actual: \(chain.start!) – Expected: \(DateInRegion(year: 2019, month: 9, day: 3))" + ) + + XCTAssert( + chain.end == DateInRegion(year: 2020, month: 1, day: 1), + "Shifted TimePeriodChain has unexpected end date – Actual: \(chain.end!) – Expected: \(DateInRegion(year: 2020, month: 1, day: 1))" + ) + } + }