In Swift, how can I get the "last Sunday of a month before the current date"?

113 views Asked by At

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?

3

There are 3 answers

2
Umar Farooq Nadeem On

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.

0
Tom Harrington On

A simpler approach would be to get the first day of the current month, and then look for the most recent Sunday before that.

You can do the first part by getting the current year and month and constructing a date with the same values but zeroes for everything else. That's the start of the current month.

Then use nextDate to look backwards from that date to a date where the components have a weekday of 1, for Sunday.

Put it together and it looks like this:

func lastSundayOfLastMonth(before referenceDate: Date = Date()) throws -> Date {
    enum DateError: Error {
        case lastSundayOfLastMonthNotFound
    }
    
    var calendar: Calendar = Calendar(identifier: .gregorian)
    calendar.timeZone = .gmt
    let sundayComponents = DateComponents(calendar: calendar, weekday: 1)

    guard let startOfCurrentMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: referenceDate)) else {
        throw DateError.lastSundayOfLastMonthNotFound
    }

    let matchedSunday = calendar.nextDate(after: startOfCurrentMonth, matching: sundayComponents, matchingPolicy: .nextTime, repeatedTimePolicy: .first, direction: .backward)

    guard let lastSunday = matchedSunday else {
        throw DateError.lastSundayOfLastMonthNotFound
    }
    return lastSunday
}

Call this as lastSundayOfLastMonth() to use the current date as the reference, or as lastSundayOfLastMonth(before: someOtherDate) to use a different date.

0
Sweeper On

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")