Weird DateFormatter behavior

I am seeing a weird behavior of the date formatter (Full code is below).

When run, this will give the following output:

 57: 1 month, 3 weeks, 5 days
 58: 1 month, 3 weeks, 6 days
 59: 2 months
 60: 2 months, 1 day
 61: 2 months
 62: 2 months, 1 day
 63: 2 months, 2 days

So both 59 days and 61 days are 2 months, and both 60 and 62 days are 2 months and 1 day.

This of course is especially weird because this means, 2 months also comes after 2 months and a day.

Can someone explain to me what is going on here?

import Foundation

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
let calendar = Calendar(identifier: .gregorian)
let today = calendar.date(from: DateComponents(year: 2025, month: 7, day: 26))!
for day in 57...63 {
    let startDate = calendar.date(byAdding: .day, value: -day, to: today)!
    let components = calendar.dateComponents([.day, .weekOfMonth, .month,. year], from: startDate, to: today)
    let result = formatter.string(from: components)!
    print ("\(String(format: "%3d", day)): \(result)")
}
Answered by below in 850867022

Thank you for your replies, I think I found the answer myself. The issue seems to be that I am formatting from the components. If I use string(from: to:), then the output is as I would expect it!

    let formatter = DateComponentsFormatter()
    formatter.unitsStyle = .full
    formatter.allowedUnits = [.year, .month, .weekOfMonth, .day]
    let result = formatter.string(from: startDate, to: endDate)

I think the problem there is that ".weekOfMonth". When you combine that with ".day", there is some internal uncertainty about when a week or month can be considered to have elapsed. You're just hitting an edge case.

The documentation suggests using Date.RelativeFormatStyle instead. When I try that with your code, all days return just "2 months".

There are two issues:

  1. The use of .weekOfMonth. This is not a "number of weeks". If the result had 21 days, this doesn't give 3, for example. Drop your use of .weekOfMonth since it is not a useful component for what you are trying to do.

  2. The "strange" output is the result of your use of the DateComponentsFormatter. The formatter assumes a 28-day month. Just print the raw components.

If you change the line:

let result = formatter.string(from: components)!

to:

let result = "\(components)"

then you will get correct output (once you remove the use of .weekOfMonth.

 57: year: 0 month: 1 day: 26 
 58: year: 0 month: 1 day: 27 
 59: year: 0 month: 1 day: 28 
 60: year: 0 month: 1 day: 29 
 61: year: 0 month: 2 day: 0 
 62: year: 0 month: 2 day: 1 
 63: year: 0 month: 2 day: 2 

I take back what I said about .weekOfMonth. Once you ditch DateComponentsFormatter, you get the correct result, even with .weekOfMonth.

 57: year: 0 month: 1 day: 5 weekOfMonth: 3 
 58: year: 0 month: 1 day: 6 weekOfMonth: 3 
 59: year: 0 month: 1 day: 0 weekOfMonth: 4 
 60: year: 0 month: 1 day: 1 weekOfMonth: 4 
 61: year: 0 month: 2 day: 0 weekOfMonth: 0 
 62: year: 0 month: 2 day: 1 weekOfMonth: 0 
 63: year: 0 month: 2 day: 2 weekOfMonth: 0 

Thank you for your replies, I think I found the answer myself. The issue seems to be that I am formatting from the components. If I use string(from: to:), then the output is as I would expect it!

    let formatter = DateComponentsFormatter()
    formatter.unitsStyle = .full
    formatter.allowedUnits = [.year, .month, .weekOfMonth, .day]
    let result = formatter.string(from: startDate, to: endDate)
Weird DateFormatter behavior
 
 
Q