Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Sources/SwiftDate/DateInRegion/Region.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ public struct Region: Decodable, Encodable, Equatable, Hashable, CustomStringCon
public init(calendar: CalendarConvertible = SwiftDate.defaultRegion.calendar,
zone: ZoneConvertible = SwiftDate.defaultRegion.timeZone,
locale: LocaleConvertible = SwiftDate.defaultRegion.locale) {
self.calendar = Calendar.newCalendar(calendar, configure: {
let sourceCalendar = calendar.toCalendar()
self.calendar = Calendar.newCalendar(sourceCalendar, configure: {
$0.timeZone = zone.toTimezone()
$0.locale = locale.toLocale()
// Setting the locale re-derives firstWeekday/minimumDaysInFirstWeek, so
// restore a fully configured Calendar's own values (issue #830).
if calendar is Calendar {
$0.firstWeekday = sourceCalendar.firstWeekday
$0.minimumDaysInFirstWeek = sourceCalendar.minimumDaysInFirstWeek
}
})
}

Expand Down
91 changes: 91 additions & 0 deletions Tests/SwiftDateTests/TestRegion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,97 @@ class TestRegion: XCTestCase {

}

/// Regression coverage for issue #830: a fully configured `Calendar` must keep
/// its `firstWeekday`/`minimumDaysInFirstWeek`, while a bare `Calendar.Identifier`
/// still follows the locale.
func testRegionPreservesCustomCalendarWeekConfiguration() {

SwiftDate.defaultRegion = Region(calendar: Calendars.gregorian, zone: Zones.gmt, locale: Locales.english)

func naturalWeek(_ identifier: Calendar.Identifier, _ locale: Locale) -> (firstWeekday: Int, minDays: Int) {
var reference = Calendar(identifier: identifier)
reference.locale = locale
return (reference.firstWeekday, reference.minimumDaysInFirstWeek)
}

let locales = [Locales.englishUnitedStates, Locales.italian, Locales.french,
Locales.arabic, Locales.englishUnitedStatesComputer].map { $0.toLocale() }
let identifiers: [Calendar.Identifier] = [.gregorian, .buddhist, .japanese, .persian]

// Exact issue #830 scenario: autoupdatingCurrent stays linked to the system
// when firstWeekday equals the reported value, so a differing locale re-derives it.
let systemFirstWeekday = Calendar.autoupdatingCurrent.firstWeekday
let contrastingLocale = locales.first { naturalWeek(.gregorian, $0).firstWeekday != systemFirstWeekday }
?? Locales.englishUnitedStatesComputer.toLocale()

var autoCalendar = Calendar.autoupdatingCurrent
autoCalendar.firstWeekday = systemFirstWeekday
let issueRegion = Region(calendar: autoCalendar, zone: Zones.gmt, locale: contrastingLocale)
XCTAssertEqual(issueRegion.calendar.firstWeekday, systemFirstWeekday,
"issue #830: Region re-derived firstWeekday from the locale")

// The literally reported form: firstWeekday = 2, assigned via the default region.
var reportedCalendar = Calendar.autoupdatingCurrent
reportedCalendar.firstWeekday = 2
let reportedRegion = Region(calendar: reportedCalendar)
XCTAssertEqual(reportedRegion.calendar.firstWeekday, 2,
"issue #830: firstWeekday reverted to the default")

// A configured Calendar keeps its week configuration for every calendar/locale pair.
for identifier in identifiers {
for locale in locales {
let natural = naturalWeek(identifier, locale)
let customFirstWeekday = (natural.firstWeekday % 7) + 1 // guaranteed != natural
let customMinDays = (natural.minDays >= 4 ? 1 : 4) // guaranteed != natural

var customCalendar = Calendar(identifier: identifier)
customCalendar.firstWeekday = customFirstWeekday
customCalendar.minimumDaysInFirstWeek = customMinDays

let region = Region(calendar: customCalendar, zone: Zones.europeRome, locale: locale)
XCTAssertEqual(region.calendar.firstWeekday, customFirstWeekday,
"Region reset firstWeekday for \(identifier)/\(locale.identifier)")
XCTAssertEqual(region.calendar.minimumDaysInFirstWeek, customMinDays,
"Region reset minimumDaysInFirstWeek for \(identifier)/\(locale.identifier)")
XCTAssertEqual(region.calendar.identifier, identifier,
"Region changed the calendar identifier for \(identifier)/\(locale.identifier)")
XCTAssertEqual(region.timeZone.identifier, "Europe/Rome",
"Region changed the timezone for \(identifier)/\(locale.identifier)")
XCTAssertEqual(region.locale.identifier, locale.identifier,
"Region changed the locale for \(identifier)/\(locale.identifier)")
}
}

// Every firstWeekday value survives round-tripping on an autoupdating calendar.
for weekday in 1...7 {
var customCalendar = Calendar.autoupdatingCurrent
customCalendar.firstWeekday = weekday
let region = Region(calendar: customCalendar, zone: Zones.gmt, locale: Locales.italian)
XCTAssertEqual(region.calendar.firstWeekday, weekday,
"firstWeekday \(weekday) was not preserved")
}

// Passing only a Calendar (locale falls back to the default region) still preserves it.
var onlyCalendar = Calendar(identifier: .gregorian)
onlyCalendar.firstWeekday = (naturalWeek(.gregorian, SwiftDate.defaultRegion.locale).firstWeekday % 7) + 1
onlyCalendar.minimumDaysInFirstWeek = (naturalWeek(.gregorian, SwiftDate.defaultRegion.locale).minDays >= 4 ? 1 : 4)
let onlyCalendarRegion = Region(calendar: onlyCalendar)
XCTAssertEqual(onlyCalendarRegion.calendar.firstWeekday, onlyCalendar.firstWeekday,
"firstWeekday not preserved when only a Calendar is provided")
XCTAssertEqual(onlyCalendarRegion.calendar.minimumDaysInFirstWeek, onlyCalendar.minimumDaysInFirstWeek,
"minimumDaysInFirstWeek not preserved when only a Calendar is provided")

// A bare Calendar.Identifier must keep following the locale (backward compatibility).
for locale in locales {
let natural = naturalWeek(.gregorian, locale)
let region = Region(calendar: Calendars.gregorian, zone: Zones.gmt, locale: locale)
XCTAssertEqual(region.calendar.firstWeekday, natural.firstWeekday,
"A bare calendar identifier should follow the locale's firstWeekday for \(locale.identifier)")
XCTAssertEqual(region.calendar.minimumDaysInFirstWeek, natural.minDays,
"A bare calendar identifier should follow the locale's minimumDaysInFirstWeek for \(locale.identifier)")
}
}

}

func XCTAssertInTimeIntervalRange(value: Double, range: TimeInterval, _ error: String) {
Expand Down