GH-90750: Use datetime.fromisocalendar in _strptime (#103802)

Use datetime.fromisocalendar in _strptime

This unifies the ISO → Gregorian conversion logic and improves handling
of invalid ISO weeks.
diff --git a/Lib/_strptime.py b/Lib/_strptime.py
index b97dfcc..77ccdc9 100644
--- a/Lib/_strptime.py
+++ b/Lib/_strptime.py
@@ -290,22 +290,6 @@ def _calc_julian_from_U_or_W(year, week_of_year, day_of_week, week_starts_Mon):
         return 1 + days_to_week + day_of_week
 
 
-def _calc_julian_from_V(iso_year, iso_week, iso_weekday):
-    """Calculate the Julian day based on the ISO 8601 year, week, and weekday.
-    ISO weeks start on Mondays, with week 01 being the week containing 4 Jan.
-    ISO week days range from 1 (Monday) to 7 (Sunday).
-    """
-    correction = datetime_date(iso_year, 1, 4).isoweekday() + 3
-    ordinal = (iso_week * 7) + iso_weekday - correction
-    # ordinal may be negative or 0 now, which means the date is in the previous
-    # calendar year
-    if ordinal < 1:
-        ordinal += datetime_date(iso_year, 1, 1).toordinal()
-        iso_year -= 1
-        ordinal -= datetime_date(iso_year, 1, 1).toordinal()
-    return iso_year, ordinal
-
-
 def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
     """Return a 2-tuple consisting of a time struct and an int containing
     the number of microseconds based on the input string and the
@@ -483,7 +467,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
                     else:
                         tz = value
                         break
-    # Deal with the cases where ambiguities arize
+
+    # Deal with the cases where ambiguities arise
     # don't assume default values for ISO week/year
     if year is None and iso_year is not None:
         if iso_week is None or weekday is None:
@@ -511,7 +496,6 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
     elif year is None:
         year = 1900
 
-
     # If we know the week of the year and what day of that week, we can figure
     # out the Julian day of the year.
     if julian is None and weekday is not None:
@@ -520,7 +504,10 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
             julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
                                                 week_starts_Mon)
         elif iso_year is not None and iso_week is not None:
-            year, julian = _calc_julian_from_V(iso_year, iso_week, weekday + 1)
+            datetime_result = datetime_date.fromisocalendar(iso_year, iso_week, weekday + 1)
+            year = datetime_result.year
+            month = datetime_result.month
+            day = datetime_result.day
         if julian is not None and julian <= 0:
             year -= 1
             yday = 366 if calendar.isleap(year) else 365
diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py
index e3fcabe..810c5a3 100644
--- a/Lib/test/test_strptime.py
+++ b/Lib/test/test_strptime.py
@@ -242,6 +242,16 @@ def test_ValueError(self):
         # 5. Julian/ordinal day (%j) is specified with %G, but not %Y
         with self.assertRaises(ValueError):
             _strptime._strptime("1999 256", "%G %j")
+        # 6. Invalid ISO weeks
+        invalid_iso_weeks = [
+            "2019-00-1",
+            "2019-54-1",
+            "2021-53-1",
+        ]
+        for invalid_iso_dtstr in invalid_iso_weeks:
+            with self.subTest(invalid_iso_dtstr):
+                with self.assertRaises(ValueError):
+                    _strptime._strptime(invalid_iso_dtstr, "%G-%V-%u")
 
 
     def test_strptime_exception_context(self):
diff --git a/Misc/NEWS.d/next/Library/2023-04-24-16-00-28.gh-issue-90750.da0Xi8.rst b/Misc/NEWS.d/next/Library/2023-04-24-16-00-28.gh-issue-90750.da0Xi8.rst
new file mode 100644
index 0000000..99e10f1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-24-16-00-28.gh-issue-90750.da0Xi8.rst
@@ -0,0 +1,3 @@
+Use :meth:`datetime.datetime.fromisocalendar` in the implementation of
+:meth:`datetime.datetime.strptime`, which should now accept only valid ISO
+dates. (Patch by Paul Ganssle)