Skip to content

Commit 3d8294a

Browse files
authored
Calendar.date(from: <DateComponents>) returns incorrect result when year, weekOfYear, yearForWeekOfYear are set. (#765)
Gregorian calendar's implementation for `date(from: <DateComponents>)` is incorrect for a `DateComponents` when `year`, `weekOfYear`, `yearForWeekOfYear` are set. Restore the behavior of `CalendarICU`, where `yearForWeekOfYear` is preferred to `year` when both are set. This would allow the calculation to use the `weekOfYear` field, which is set in the date components, instead an unset `day` field. Drive-by fix: give `weekdayOrdinal` a higher priority over other week fields when multiple of them are set. This isn't really an issue as this configuration is ambiguous by nature, but doing so makes it consistent with `CalendarICU`'s behavior. Fixes 130203724
1 parent aec5170 commit 3d8294a

File tree

2 files changed

+144
-5
lines changed

2 files changed

+144
-5
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,19 +134,19 @@ enum ResolvedDateComponents {
134134
adjustedYear = year
135135
}
136136
self = .day(year: adjustedYear, month: month, day: d, weekOfYear: components.weekOfYear)
137+
} else if let weekdayOrdinal = components.weekdayOrdinal, let weekday = components.weekday {
138+
self = .weekdayOrdinal(year: year, month: month, weekdayOrdinal: weekdayOrdinal, weekday: weekday)
137139
} else if let woy = components.weekOfYear, let weekday = components.weekday {
138140
self = .weekOfYear(year: year, weekOfYear: woy, weekday: weekday)
139141
} else if let wom = components.weekOfMonth, let weekday = components.weekday {
140142
self = .weekOfMonth(year: year, month: month, weekOfMonth: wom, weekday: weekday)
141-
} else if let weekdayOrdinal = components.weekdayOrdinal, let weekday = components.weekday {
142-
self = .weekdayOrdinal(year: year, month: month, weekdayOrdinal: weekdayOrdinal, weekday: weekday)
143143
} else if let dayOfYear = components.dayOfYear {
144144
self = .dayOfYear(year: year, dayOfYear: dayOfYear)
145-
} else if components.year != nil {
146-
self = .day(year: year, month: month, day: components.day, weekOfYear: components.weekOfYear)
147145
} else if components.yearForWeekOfYear != nil {
148146
self = .weekOfYear(year: year, weekOfYear: components.weekOfYear, weekday: components.weekday)
149-
} else if let weekOfYear = components.weekOfYear {
147+
} else if components.year != nil {
148+
self = .day(year: year, month: month, day: components.day, weekOfYear: components.weekOfYear)
149+
} else if let weekOfYear = components.weekOfYear {
150150
self = .weekOfYear(year: year, weekOfYear: weekOfYear, weekday: components.weekday)
151151
} else if let weekOfMonth = components.weekOfMonth {
152152
self = .weekOfMonth(year: year, month: month, weekOfMonth: weekOfMonth, weekday: components.weekday)

Tests/FoundationInternationalizationTests/CalendarTests.swift

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,74 @@ final class CalendarTests : XCTestCase {
10221022
// Month is ignored
10231023
test(.init(month: 3, weekOfYear: 1, yearForWeekOfYear: 2000), Date(timeIntervalSinceReferenceDate: -32140800.0)) // 1999-12-26
10241024
test(.init(month: 1, weekOfYear: 53, yearForWeekOfYear: 1998), Date(timeIntervalSinceReferenceDate: -63590400.0)) // 1998-12-27
1025+
1026+
// year and yearForWeekOfYear
1027+
test(.init(year: 2024, weekOfYear: 30, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 743212800.0)) // 2024-07-21 00:00:00 UTC
1028+
test(.init(year: 2023, weekOfYear: 1 ,yearForWeekOfYear: 2023), Date(timeIntervalSinceReferenceDate: 694224000.0)) // 2023-01-01T00:00:00Z
1029+
test(.init(year: 2023, weekOfYear: 52, yearForWeekOfYear: 2023), Date(timeIntervalSinceReferenceDate: 725068800.0)) // 2023-12-24T00:00:00Z
1030+
test(.init(year: 2023, weekOfYear: 53, yearForWeekOfYear: 2023), Date(timeIntervalSinceReferenceDate: 725673600.0)) // 2023-12-31T00:00:00Z
1031+
test(.init(year: 2024, weekOfYear: 30, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 743212800.0)) // 2024-07-21T00:00:00Z
1032+
test(.init(year: 2024, weekOfYear: 53, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 757123200.0)) // 2024-12-29T00:00:00Z
1033+
test(.init(year: 2025, weekOfYear: 1 ,yearForWeekOfYear: 2025), Date(timeIntervalSinceReferenceDate: 757123200.0)) // 2024-12-29T00:00:00Z
1034+
test(.init(year: 2024, weekOfYear: 1 ,yearForWeekOfYear: 2025), Date(timeIntervalSinceReferenceDate: 757123200.0)) // 2024-12-29T00:00:00Z
1035+
test(.init(year: 2025, weekOfYear: 1 ,yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 725673600.0)) // 2023-12-31T00:00:00Z
1036+
test(.init(year: 2024, weekOfYear: 52, yearForWeekOfYear: 2025), Date(timeIntervalSinceReferenceDate: 787968000.0)) // 2025-12-21T00:00:00Z
1037+
test(.init(year: 2025, weekOfYear: 52, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 756518400.0)) // 2024-12-22T00:00:00Z
1038+
test(.init(year: 2024, weekOfYear: 53, yearForWeekOfYear: 2025), Date(timeIntervalSinceReferenceDate: 788572800.0)) // 2025-12-28T00:00:00Z
1039+
test(.init(year: 2025, weekOfYear: 53, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 757123200.0)) // 2024-12-29T00:00:00Z
1040+
1041+
// year and weekOfYear, not valid setting so the result is just the first day of the year
1042+
test(.init(year: 2023, weekOfYear: 1), Date(timeIntervalSinceReferenceDate: 694224000.0)) // 2023-01-01T00:00:00Z
1043+
test(.init(year: 2023, weekOfYear: 30), Date(timeIntervalSinceReferenceDate: 694224000.0)) // 2023-01-01T00:00:00Z
1044+
test(.init(year: 2023, weekOfYear: 52), Date(timeIntervalSinceReferenceDate: 694224000.0)) // 2023-01-01T00:00:00Z
1045+
test(.init(year: 2023, weekOfYear: 53), Date(timeIntervalSinceReferenceDate: 694224000.0)) // 2023-01-01T00:00:00Z
1046+
1047+
// weekOfYear and yearForWeekOfYear
1048+
test(.init(weekOfYear: 1, yearForWeekOfYear: 2025), Date(timeIntervalSinceReferenceDate: 757123200.0)) // 2024-12-29T00:00:00Z
1049+
test(.init(weekOfYear: 52, yearForWeekOfYear: 2025), Date(timeIntervalSinceReferenceDate: 787968000.0)) // 2025-12-21T00:00:00Z
1050+
test(.init(weekOfYear: 53, yearForWeekOfYear: 2025), Date(timeIntervalSinceReferenceDate: 788572800.0)) // 2025-12-28T00:00:00Z
1051+
1052+
test(.init(weekOfYear: 1, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 725673600.0)) // 2023-12-31T00:00:00Z
1053+
test(.init(weekOfYear: 52, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 756518400.0)) // 2024-12-22T00:00:00Z
1054+
test(.init(weekOfYear: 53, yearForWeekOfYear: 2024), Date(timeIntervalSinceReferenceDate: 757123200.0)) // 2024-12-29T00:00:00Z
1055+
1056+
// weekday ordinal
1057+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3), Date(timeIntervalSinceReferenceDate: -156124800.0)) // 1996-01-21T00:00:00Z
1058+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2), Date(timeIntervalSinceReferenceDate: -156124800.0)) // 1996-01-21T00:00:00Z
1059+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2), Date(timeIntervalSinceReferenceDate: -156124800.0)) // 1996-01-21T00:00:00Z
1060+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4), Date(timeIntervalSinceReferenceDate: -156124800.0)) // 1996-01-21T00:00:00Z
1061+
1062+
// yearForWeekOfYear takes precedence over year
1063+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1064+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1065+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1066+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1067+
1068+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1069+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1070+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, weekOfYear: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1071+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1072+
1073+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1074+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1075+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, weekOfYear: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1076+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1077+
1078+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1079+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1080+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1081+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1082+
1083+
// weekdayOrdinal takes precedence over other week fields when weekday is set
1084+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1085+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1086+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, weekOfYear: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1087+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -189388800.0)) // 1995-01-01T00:00:00Z
1088+
1089+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1090+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1091+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
1092+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995), Date(timeIntervalSinceReferenceDate: -188179200.0)) // 1995-01-15T00:00:00Z
10251093
}
10261094

10271095
func test_firstWeekday() {
@@ -1217,6 +1285,77 @@ final class GregorianCalendarCompatibilityTests: XCTestCase {
12171285
test(.init())
12181286
test(.init(month: 1, day: 1))
12191287
test(.init(month: 1, day: 1, hour: 1))
1288+
1289+
// both year and yearForWeekOfYear are set
1290+
test(.init(year: 2023, weekOfYear: 1, yearForWeekOfYear: 2023))
1291+
test(.init(year: 2023, weekOfYear: 52, yearForWeekOfYear: 2023))
1292+
test(.init(year: 2023, weekOfYear: 53, yearForWeekOfYear: 2023))
1293+
1294+
test(.init(year: 2024, weekOfYear: 30, yearForWeekOfYear: 2024))
1295+
test(.init(year: 2024, weekOfYear: 53, yearForWeekOfYear: 2024))
1296+
1297+
test(.init(year: 2025, weekOfYear: 1, yearForWeekOfYear: 2025))
1298+
1299+
// Conflicting setting of year and yearForWeekOfYear
1300+
test(.init(year: 2024, weekOfYear: 1, yearForWeekOfYear: 2025))
1301+
test(.init(year: 2025, weekOfYear: 1, yearForWeekOfYear: 2024))
1302+
test(.init(year: 2024, weekOfYear: 52, yearForWeekOfYear: 2025))
1303+
test(.init(year: 2025, weekOfYear: 52, yearForWeekOfYear: 2024))
1304+
test(.init(year: 2024, weekOfYear: 53, yearForWeekOfYear: 2025))
1305+
test(.init(year: 2025, weekOfYear: 53, yearForWeekOfYear: 2024))
1306+
1307+
test(.init(year: 2023, weekOfYear: 1))
1308+
test(.init(year: 2023, weekOfYear: 30))
1309+
test(.init(year: 2023, weekOfYear: 52))
1310+
test(.init(year: 2023, weekOfYear: 53))
1311+
1312+
test(.init(weekOfYear: 1, yearForWeekOfYear: 2025))
1313+
test(.init(weekOfYear: 1, yearForWeekOfYear: 2024))
1314+
test(.init(weekOfYear: 52, yearForWeekOfYear: 2025))
1315+
test(.init(weekOfYear: 52, yearForWeekOfYear: 2024))
1316+
test(.init(weekOfYear: 53, yearForWeekOfYear: 2025))
1317+
test(.init(weekOfYear: 53, yearForWeekOfYear: 2024))
1318+
1319+
// weekOfMonth and weekOfYear
1320+
test(.init(year: 1996, weekOfMonth: 2, weekOfYear: 10))
1321+
test(.init(year: 1996, weekday: 3, weekOfMonth: 2, weekOfYear: 10))
1322+
1323+
// weekday ordinal
1324+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3))
1325+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2))
1326+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2))
1327+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4))
1328+
1329+
// weekday ordinal, year, and WOY
1330+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, yearForWeekOfYear: 1995))
1331+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, yearForWeekOfYear: 1995))
1332+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, weekOfYear: 2, yearForWeekOfYear: 1995))
1333+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995))
1334+
1335+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, yearForWeekOfYear: 1995))
1336+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, yearForWeekOfYear: 1995))
1337+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, weekOfYear: 2, yearForWeekOfYear: 1995))
1338+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995))
1339+
1340+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, yearForWeekOfYear: 1995))
1341+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, yearForWeekOfYear: 1995))
1342+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, weekOfYear: 2, yearForWeekOfYear: 1995))
1343+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 1, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995))
1344+
1345+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, yearForWeekOfYear: 1995))
1346+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, yearForWeekOfYear: 1995))
1347+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2, yearForWeekOfYear: 1995))
1348+
test(.init(year: 1996, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995))
1349+
1350+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, yearForWeekOfYear: 1995))
1351+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, yearForWeekOfYear: 1995))
1352+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2, yearForWeekOfYear: 1995))
1353+
test(.init(year: 1995, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995))
1354+
1355+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, yearForWeekOfYear: 1995))
1356+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, yearForWeekOfYear: 1995))
1357+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, weekOfYear: 2, yearForWeekOfYear: 1995))
1358+
test(.init(year: 1994, weekday: 1, weekdayOrdinal: 3, weekOfMonth: 2, weekOfYear: 4, yearForWeekOfYear: 1995))
12201359
}
12211360

12221361

0 commit comments

Comments
 (0)