diff --git a/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift b/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift index 225f3292..d548312b 100644 --- a/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift +++ b/Sources/SwiftDate/TimePeriod/Groups/TimePeriodChain.swift @@ -18,50 +18,81 @@ import Foundation /// Time period chains do not allow overlaps within their set of time periods. /// This type of group is ideal for modeling schedules like sequential meetings or appointments. open class TimePeriodChain: TimePeriodGroup { - + + // MARK: - Initializers + + public override init(_ periods: [TimePeriodProtocol]? = nil) { + super.init(periods) + + updateExtremes() + } + // 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) } } @@ -86,9 +117,9 @@ open class TimePeriodChain: TimePeriodGroup { //Shift all periods after inserted period for i in 0.. index && i > 0 { - let currentPeriod = TimePeriod(start: period.start, end: period.end) + let duration = periods[i].duration periods[i].start = periods[i - 1].end - periods[i].end = periods[i].start!.addingTimeInterval(currentPeriod.duration) + periods[i].end = periods[i].start!.addingTimeInterval(duration) } } @@ -130,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) @@ -155,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/SwiftDate.xcodeproj/project.pbxproj b/SwiftDate.xcodeproj/project.pbxproj index 7212ad18..a54d8ce2 100644 --- a/SwiftDate.xcodeproj/project.pbxproj +++ b/SwiftDate.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 3A825B6325FEC54700F21DB2 /* TestTimePeriodChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A825B4925FEC54100F21DB2 /* TestTimePeriodChain.swift */; }; + 3A825B6425FEC54800F21DB2 /* TestTimePeriodChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A825B4925FEC54100F21DB2 /* TestTimePeriodChain.swift */; }; + 3A825B6525FEC54800F21DB2 /* TestTimePeriodChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A825B4925FEC54100F21DB2 /* TestTimePeriodChain.swift */; }; 52D6D9871BEFF229002C0205 /* SwiftDate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* SwiftDate.framework */; }; 6434DD6120C7FAF6007626EF /* DateInRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6434DD6020C7FAF6007626EF /* DateInRegion.swift */; }; 6434DD6220C7FAF6007626EF /* DateInRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6434DD6020C7FAF6007626EF /* DateInRegion.swift */; }; @@ -258,6 +261,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3A825B4925FEC54100F21DB2 /* TestTimePeriodChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTimePeriodChain.swift; sourceTree = ""; }; 52D6D97C1BEFF229002C0205 /* SwiftDate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftDate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52D6D9861BEFF229002C0205 /* SwiftDate-iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftDate-iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 52D6D9E21BEFFF6E002C0205 /* SwiftDate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftDate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -569,6 +573,7 @@ 64BAB12720E6411100FEED79 /* TestSwiftDate.swift */, 647AD65B21F4851F00CF787E /* TestDataStructures.swift */, A89F3FAE22A00019002D1BD0 /* TestDate.swift */, + 3A825B4925FEC54100F21DB2 /* TestTimePeriodChain.swift */, ); name = Tests; path = Tests/SwiftDateTests; @@ -974,6 +979,7 @@ 64BAB12420E63A3A00FEED79 /* TestDateInRegion+Langs.swift in Sources */, 647AD65C21F4851F00CF787E /* TestDataStructures.swift in Sources */, 6439232220D912670098EC03 /* TestDateInRegion+Math.swift in Sources */, + 3A825B6525FEC54800F21DB2 /* TestTimePeriodChain.swift in Sources */, 64EF3E0F20D65478002793C6 /* TestDateInRegion+Compare.swift in Sources */, 6439232620D91D170098EC03 /* TestFormatters.swift in Sources */, 64EF3E0B20D65329002793C6 /* TestDateInRegion+Create.swift in Sources */, @@ -1163,6 +1169,7 @@ buildActionMask = 2147483647; files = ( 64BAB12520E63A3A00FEED79 /* TestDateInRegion+Langs.swift in Sources */, + 3A825B6425FEC54800F21DB2 /* TestTimePeriodChain.swift in Sources */, 647AD65D21F4851F00CF787E /* TestDataStructures.swift in Sources */, A89F3FAF22A00019002D1BD0 /* TestDate.swift in Sources */, 6439232320D912670098EC03 /* TestDateInRegion+Math.swift in Sources */, @@ -1183,6 +1190,7 @@ 64BAB12620E63A3A00FEED79 /* TestDateInRegion+Langs.swift in Sources */, 647AD65E21F4851F00CF787E /* TestDataStructures.swift in Sources */, 6439232420D912670098EC03 /* TestDateInRegion+Math.swift in Sources */, + 3A825B6325FEC54700F21DB2 /* TestTimePeriodChain.swift in Sources */, 64EF3E1120D65478002793C6 /* TestDateInRegion+Compare.swift in Sources */, 6439232820D91D170098EC03 /* TestFormatters.swift in Sources */, 64EF3E0D20D65329002793C6 /* TestDateInRegion+Create.swift in Sources */, diff --git a/Tests/SwiftDateTests/TestTimePeriodChain.swift b/Tests/SwiftDateTests/TestTimePeriodChain.swift new file mode 100644 index 00000000..a53f899b --- /dev/null +++ b/Tests/SwiftDateTests/TestTimePeriodChain.swift @@ -0,0 +1,239 @@ +// +// SwiftDate +// Parse, validate, manipulate, and display dates, time and timezones in Swift +// +// Created by AsioOtus +// - Mail: asio.otus.verus@icloud.com +// +// Copyright © 2019 Daniele Margutti. Licensed under MIT License. +// + +@testable import SwiftDate +import XCTest + +class TestTimePeriodChain: XCTestCase { + let periods = [ + TimePeriod(start: .init(year: 2020, month: 2, day: 1), end: .init(year: 2020, month: 2, day: 28)), + TimePeriod(start: .init(year: 2020, month: 3, day: 1), end: .init(year: 2020, month: 3, day: 31)), + TimePeriod(start: .init(year: 2020, month: 4, day: 1), end: .init(year: 2020, month: 4, day: 30)), + TimePeriod(start: .init(year: 2020, month: 5, day: 1), end: .init(year: 2020, month: 5, day: 31)) + ] + + func testInitialExtremes () { + let chain = TimePeriodChain(periods) + + XCTAssert(chain.start == periods.first!.start, "TimePeriodChain initial start not equals to source period start") + XCTAssert(chain.end == periods.last!.end, "TimePeriodChain initial end not equals to source period end") + } + + func testInsert () { + let chain = TimePeriodChain(periods) + let period = TimePeriod(start: .init(year: 2020, month: 1, day: 1), end: .init(year: 2020, month: 1, day: 20)) + + let originalDuration_0 = chain.periods[0].duration + let originalDuration_1 = chain.periods[1].duration + let originalDuration_2 = chain.periods[2].duration + let originalDuration_3 = chain.periods[3].duration + + chain.insert(period, at: 2) + + XCTAssert(chain.periods[0].duration == originalDuration_0, "Unexpected duration of 0 period– Actual: \(chain.periods[0].duration) – Expected: \(originalDuration_0)") + XCTAssert(chain.periods[1].duration == originalDuration_1, "Unexpected duration of 1 period – Actual: \(chain.periods[1].duration) – Expected: \(originalDuration_1)") + XCTAssert(chain.periods[2].duration == period.duration, "Unexpected duration of 2 period – Actual: \(chain.periods[2].duration) – Expected: \(period.duration)") + 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))" + ) + } + +}