alpennec
alpennec

Reputation: 2114

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

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?

Upvotes: 1

Views: 305

Answers (2)

Tom Harrington
Tom Harrington

Reputation: 70946

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.

Upvotes: 0

Sweeper
Sweeper

Reputation: 271410

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

Upvotes: 1

Related Questions