swift-foundation icon indicating copy to clipboard operation
swift-foundation copied to clipboard

Calendar.RecurrenceRule incorrectly calculates ordinal weekdays when firstWeekday != 1

Open theoks opened this issue 5 months ago • 0 comments

Description

Calendar.RecurrenceRule produces incorrect results or returns no results for ordinal weekday calculations (e.g., "1st Sunday", "last Friday") when the calendar's firstWeekday property is not set to 1 (Sunday). This affects users in some European or other locales where Monday (or another weekday) is the first day of the week.

Steps to Reproduce 1

import Foundation

// Create calendar with Monday as first day (standard in some countries in Europe)
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
calendar.firstWeekday = 2  // Monday

// Find the last Sunday of January 2024
let rule = Calendar.RecurrenceRule(
    calendar: calendar,
    frequency: .monthly,
    weekdays: [.nth(-1, .sunday)]  // -1 means "last"
)

let jan2024 = calendar.date(from: DateComponents(year: 2024, month: 1, day: 1))!
let feb2024 = calendar.date(from: DateComponents(year: 2024, month: 2, day: 1))!

let results = Array(rule.recurrences(of: jan2024, in: jan2024..<feb2024))

// Expected: January 28 (the last Sunday)
// Actual: No results returned!
print("Results count: \(results.count)")  // Prints "Results count: 0"

Expected Results

Ordinal weekday calculations should return the same dates regardless of firstWeekday:

  • Last Sunday of January 2024: January 28
  • 1st Sunday of January 2024: January 7

Actual Results

With firstWeekday = 2 (Monday):

  • Last Sunday of January 2024: No result
  • 1st Sunday of January 2024: January 14 ❌ (returns 2nd Sunday instead)

Full test results for "1st Sunday of January 2024":

  • firstWeekday=1 (Sun): January 7 ✅
  • firstWeekday=2 (Mon): January 14 ❌ (wrong week)
  • firstWeekday=3 (Tue): January 7 ✅
  • firstWeekday=4 (Wed): January 7 ✅
  • firstWeekday=5 (Thu): January 7 ✅
  • firstWeekday=6 (Fri): January 7 ✅
  • firstWeekday=7 (Sat): January 7 ✅

Steps to Reproduce 2

import Foundation

// Create a calendar with Wednesday as first day
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
calendar.firstWeekday = 4  // Wednesday

// Find the 4th Thursday of January 2024
let rule = Calendar.RecurrenceRule(
    calendar: calendar,
    frequency: .monthly,
    weekdays: [.nth(4, .thursday)]
)

let startDate = calendar.date(from: DateComponents(year: 2024, month: 1, day: 1))!
let endDate = calendar.date(from: DateComponents(year: 2024, month: 2, day: 1))!

let results = Array(rule.recurrences(of: startDate, in: startDate..<endDate))
if let result = results.first {
    let day = calendar.component(.day, from: result)
    print("Got January \(day)")  // Prints "Got January 18" (incorrect)
}

Expected Results

The 4th Thursday of January 2024 should always be January 25, regardless of the calendar's firstWeekday setting.

Actual Results

The calculation returns different (incorrect) dates depending on firstWeekday:

  • firstWeekday=1 (Sun): January 25 ✅
  • firstWeekday=2 (Mon): January 25 ✅
  • firstWeekday=3 (Tue): January 18 ❌
  • firstWeekday=4 (Wed): January 18 ❌
  • firstWeekday=5 (Thu): January 18 ❌
  • firstWeekday=6 (Fri): January 25 ✅
  • firstWeekday=7 (Sat): January 25 ✅

Environment

  • iOS 18+, macOS 15.5
  • Xcode 16.4

Workaround

Temporarily set calendar.firstWeekday = 1 before creating the RecurrenceRule:

var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "en_GB")  // UK locale (Monday first)
// ... configure calendar ...

// Workaround: Force Sunday as first day for calculation only
var calcCalendar = calendar
calcCalendar.firstWeekday = 1
let rule = Calendar.RecurrenceRule(
    calendar: calcCalendar,
    frequency: .monthly,
    weekdays: [.nth(-1, .sunday)]
)
// Now returns correct results

theoks avatar Jun 28 '25 00:06 theoks