Skip to content

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

Open
@theoks

Description

@theoks

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions