The issue here is that Calendar.dateComponents(in:from:)
is returning to you a DateComponents
value with all fields filled. startDateComponents
on my machine:
calendar: gregorian (fixed) timeZone: GMT (fixed) era: 1 year: 1960 month: 12 day: 18 hour: 0 minute: 0 second: 0 nanosecond: 0 weekday: 1 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 4 weekOfYear: 52 yearForWeekOfYear: 1960 isLeapMonth: false
Note that this is specifying things like .weekday
, .quarter
, etc., which are specific to the returned date.
Changing just .month
on those components means that all of the other fields may be now be out of sync: 1960-01-18 was not, for instance, yearForWeekOfYear: 1960, weekOfYear: 52
, but yearForWeekOfYear: 1960, weekOfYear: 3
.
When you request a date by calling transformedDateComponents.date!
, the components you're using are inconsistent, and the results of the calendrical calculations depend on the order in which the components are attempted to be matched.
In the end, based on the order of calculations attempted, the next date matching some of the components you tried ends up being 1961-01-18.
If you want to perform an operation like this, you should either:
-
Use a more explicit calendrical calculation method like Calendar.nextDate(after:matching:matchingPolicy:repeatedTimePolicy:direction)
and pass in the minimal changed components + a direction you want to search in, or
-
Specify only the components you want to grab from the calendar so that changing the .month
no longer produces inconsistent components:
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = .init(abbreviation: "UTC")!
var incorrectDate = calendar.date(from: .init(calendar: calendar, timeZone: calendar.timeZone, year: 1960, month: 12, day: 18))!
print(incorrectDate)
// => 1960-12-18 00:00:00 +0000
var correctDate = calendar.date(from: .init(calendar: calendar, timeZone: calendar.timeZone, year: 1960, month: 12, day: 17))!
print(correctDate)
// => 1960-12-17 00:00:00 +0000
var startDateComponents = calendar.dateComponents([.calendar, .timeZone, .year, .month, .day], from: incorrectDate)
print(startDateComponents)
// => calendar: gregorian (fixed) timeZone: GMT (fixed) year: 1960 month: 12 day: 18 isLeapMonth: false
var dateFromComponents = startDateComponents.date!
print(dateFromComponents)
// => 1960-12-18 00:00:00 +0000
var transformedComponents = startDateComponents
transformedComponents.month = 1
var transformedDateFromTransformedComponents = transformedComponents.date!
print(transformedDateFromTransformedComponents)
// => 1960-01-18 00:00:00 +0000
var changedComponents = calendar.dateComponents(in: calendar.timeZone, from: transformedDateFromTransformedComponents)
print(changedComponents)
// => calendar: gregorian (fixed) timeZone: GMT (fixed) era: 1 year: 1960 month: 1 day: 18 hour: 0 minute: 0 second: 0 nanosecond: 0 weekday: 2 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 4 weekOfYear: 4 yearForWeekOfYear: 1960 isLeapMonth: false
The downside to approach (2) is that you need to be very careful about the components you pull out: like @eskimo mentions, unless you hard-code your calculations to be in the Gregorian calendar, results may not be what you expect.
(In general, I’d recommend calling the appropriate methods on a calendar directly — it’s very easy to come up with nonsense date components, and calling .date
on the components directly gives you no real way to affect results or recover reasonably.)