How does Calendar handle “add a month”, “next Friday” and “start of the month”?

2.2.5 · L2 · Learning

Predict first

What does this print?

var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "UTC")!
let jan31 = cal.date(from: DateComponents(year: 2024, month: 1, day: 31))!
let next = cal.date(byAdding: .month, value: 1, to: jan31)!
print(cal.component(.month, from: next), cal.component(.day, from: next))
  • 2 29
  • 3 2
  • 2 31

Predict first

What day of the month is the next Friday after Monday 30 December 2024?

var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "UTC")!
let monday = Date(timeIntervalSince1970: 1735516800)
let friday = cal.nextDate(after: monday, matching: DateComponents(weekday: 6), matchingPolicy: .nextTime)!
print(cal.component(.day, from: friday))
  • 3
  • 4
  • 31

Mental model. Adding a month is moving a bookmark one chapter on. If the next chapter is shorter, the bookmark stops at its last page.

Calendar maths is defined on fields, not seconds. date(byAdding: .month, value: 1, to:) moves the month and clamps the day if needed: 31 January plus one month is 29 February in 2024, not 2 March. nextDate(after:matching:matchingPolicy:) finds the next time that matches components, such as the next Friday at 9 am. dateInterval(of: .month, for:) and range(of:in:for:) give a month’s bounds and its number of days.

For anything “month-ish” (billing dates, birthdays) decide the rule on purpose. “The 31st of each month” is not the same as “last day of each month”, and the calendar will quietly choose clamping for you.

The day is clamped to the last valid day of February, which is the 29th in a leap year.

Weekday 6 is Friday in the Gregorian numbering, and the next one is 3 January 2025.

Common traps

  • Looping “add 30 days” for monthly billing. The date drifts. Add months to the original date each time.
  • Assuming a month is 30 days. Ask range(of: .day, in: .month, for:).

They might follow up with

  • How do you get the last day of a month? Take dateInterval(of: .month, for:) and step back one day from its end.
  • What does matchingPolicy do? It decides what happens for times that do not exist or repeat, around DST changes.

Related questions

  • 2.2.4 Why is date + 86,400 seconds not tomorrow?
  • 2.2.3 How do Calendar and DateComponents work, and why can YYYY give the wrong year?

← Why is date + 86,400 seconds not tomorrow?   What is a Locale, and how is it different from the user’s language? →