Other answers work fine if the desired behavior adds a month and allows daylight saving time to be used. This gives such results that:
01/03/2017 00:00 + 1 month -> 31/03/2017 23:00 01/10/2017 00:00 + 1 month -> 01/11/2017 01:00
However, I wanted to ignore the hour lost or received by DST, so:
01/03/2017 00:00 + 1 month -> 01/04/2017 00:00 01/10/2017 00:00 + 1 month -> 01/11/2017 00:00
So, I check if the DST border passes, and if so, then add or subtract the hour accordingly:
func offsetDaylightSavingsTime() -> Date { // daylightSavingTimeOffset is either + 1hr or + 0hr. To offset DST for a given date, we need to add an hour or subtract an hour // +1hr -> +1hr // +0hr -> -1hr // offset = (daylightSavingTimeOffset * 2) - 1 hour let daylightSavingsTimeOffset = TimeZone.current.daylightSavingTimeOffset(for: self) let oneHour = TimeInterval(3600) let offset = (daylightSavingsTimeOffset * 2) - oneHour return self.addingTimeInterval(offset) } func isBetweeen(date date1: Date, andDate date2: Date) -> Bool { return date1.compare(self).rawValue * self.compare(date2).rawValue >= 0 } func offsetDaylightSavingsTimeIfNecessary(nextDate: Date) -> Date { if let nextDST = TimeZone.current.nextDaylightSavingTimeTransition(after: self) { if nextDST.isBetweeen(date: self, andDate: nextDate){ let offsetDate = nextDate.offsetDaylightSavingsTime() let difference = offsetDate.timeIntervalSince(nextDate) return nextDate.addingTimeInterval(difference) } } return nextDate } func dateByAddingMonths(_ months: Int) -> Date? { if let dateWithMonthsAdded = Calendar.current.date(byAdding: .month, value: months, to: self) { return self.offsetDaylightSavingsTimeIfNecessary(nextDate: dateWithMonthsAdded) } return self }
Test:
func testDateByAddingMonths() { let date1 = "2017-01-01T00:00:00Z".asDate() let date2 = "2017-02-01T00:00:00Z".asDate() let date3 = "2017-03-01T00:00:00Z".asDate() let date4 = "2017-04-01T00:00:00Z".asDate() let date5 = "2017-05-01T00:00:00Z".asDate() let date6 = "2017-06-01T00:00:00Z".asDate() let date7 = "2017-07-01T00:00:00Z".asDate() let date8 = "2017-08-01T00:00:00Z".asDate() let date9 = "2017-09-01T00:00:00Z".asDate() let date10 = "2017-10-01T00:00:00Z".asDate() let date11 = "2017-11-01T00:00:00Z".asDate() let date12 = "2017-12-01T00:00:00Z".asDate() let date13 = "2018-01-01T00:00:00Z".asDate() let date14 = "2018-02-01T00:00:00Z".asDate() var testDate = "2017-01-01T00:00:00Z".asDate() XCTAssertEqual(testDate, date1) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date2) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date3) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date4) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date5) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date6) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date7) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date8) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date9) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date10) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date11) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date12) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date13) testDate = testDate.dateByAddingMonths(1)! XCTAssertEqual(testDate, date14) }
For completeness, the .asDate () method, I use
extension String { static let dateFormatter = DateFormatter() func checkIsValidDate() -> Bool { return self.tryParseToDate() != nil } func tryParseToDate() -> Date? { String.dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" return String.dateFormatter.date(from: self) } func asDate() -> Date { return tryParseToDate()! } }