skip to Main Content

I want to find the "last Sunday of a month before the current date" in Swift, but using the Calendar nextDate function doesn’t work (always returns nil).

var calendar: Calendar = Calendar(identifier: .gregorian)
calendar.timeZone = .gmt

let lastSundayDateComponents: DateComponents = DateComponents(
    weekday: 1,
    weekdayOrdinal: -1
)

let previousLastSundayDate: Date? = calendar.nextDate(
    after: Date.now,
    matching: lastSundayDateComponents,
    matchingPolicy: .nextTime,
    repeatedTimePolicy: .first,
    direction: .backward
)

print(previousLastSundayDate ?? "Not found") // "Not found"

If I use a positive weekdayOrdinal, it’s working normally and the same nextDate method provides the correct date.

let firstSundayDateComponents: DateComponents = DateComponents(
    weekday: 1,
    weekdayOrdinal: 1
)

When I check if the date components can provide a valid date for the given calendar, it returns false…

let lastSundayInNovember2023DateComponents: DateComponents = DateComponents(
    year: 2023,
    month: 11,
    weekday: 1,
    weekdayOrdinal: -1
)
            
// THIS RETURNS FALSE
let isValid: Bool = lastSundayInNovember2023DateComponents.isValidDate(in: calendar)
print(isValid) // false

… even if the correct date can be created.

let lastSundayInNovember2023: Date = calendar.date(from: lastSundayInNovember2023DateComponents)!
print(lastSundayInNovember2023) // 2023-11-26 00:00:00 +0000

Is that a bug in Foundation?

2

Answers


  1. It seems like there might be an issue with using negative weekdayOrdinal values in combination with the nextDate function in Swift’s Calendar class. This behavior is not well-documented, and there might be an inconsistency or limitation in the implementation.

    As a workaround, you can use the following approach to find the last Sunday of the month before the current date:

    func lastSunday(from date: Date) -> Date? {
    var calendar = Calendar(identifier: .gregorian)
    calendar.timeZone = .current
    
    // Get the weekday component of the given date
    let weekday = calendar.component(.weekday, from: date)
    
    // Calculate the number of days to subtract to get to the last Sunday (considering Sunday as 1st day of the week)
    let daysToSubtract = (weekday - calendar.firstWeekday + 7) % 7
    
    // Calculate the date for the last Sunday from the provided date
    if let lastSunday = calendar.date(byAdding: .day, value: -daysToSubtract, to: date) {
            return lastSunday
        } else {
            return nil
        }
    }
    
    // Example usage:
    var dateComponents = DateComponents()
    dateComponents.year = 2023
    dateComponents.month = 11
    dateComponents.day = 2
    dateComponents.timeZone = .gmt
    // Create date from components
    let userCalendar = Calendar(identifier: .gregorian) // since the components above (like year 1980) are for Gregorian
    let date = userCalendar.date(from: dateComponents)!
    
    if let lastSundayDate = lastSunday(from: date) {
        print(date)
        print(lastSundayDate)
    } else {
        print("Not found")
    }
    

    This approach first calculates the first day of the current month and then finds the last day of the previous month. Finally, it uses the dateComponents method to get the last Sunday of the previous month based on the year, week of the year, and weekday components.

    This workaround should provide you with the correct result without relying on negative weekdayOrdinal values.

    Login or Signup to reply.
  2. From the documentation of weekdayOrdinal, it doesn’t sound like it should ever be negative:

    Weekday ordinal units represent the position of the weekday within the next larger calendar unit, such as the month. For example, 2 is the weekday ordinal unit for the second Friday of the month.

    Its name also suggests that it is an ordinal number, which cannot be negative.

    I would find the last Sunday of a month by finding what the weekdayOrdinal of the next Sunday.

    • If the next Sunday is the first Sunday of the month, we know that one week before the next Sunday is the last Sunday of a month
    • If the next Sunday is the second Sunday of the month, that means two weeks before the next Sunday is the last Sunday of a month
    • If the next Sunday is the third Sunday of the month, that means three weeks before the next Sunday is the last Sunday of a month

    and so on.

    var calendar: Calendar = Calendar(identifier: .gregorian)
    calendar.timeZone = .gmt
    let now = Date.now
    let currentMonth = calendar.component(.month, from: now)
    let nextSunday = calendar.nextDate(
        after: now,
        matching: DateComponents(weekday: 1),
        matchingPolicy: .nextTime
    )!
    
    let nextSundayOrdinal = calendar.component(.weekdayOrdinal, from: nextSunday)
    let previousLastSundayDate = calendar.date(
        byAdding: .day,
        value: -nextSundayOrdinal * 7,
        to: nextSunday
    )
    
    print(previousLastSundayDate ?? "Not found")
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search