From 2f79461bc9143b8eb30bc801334889d79da3fa36 Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Tue, 16 Jun 2026 10:52:23 +0200 Subject: [PATCH 1/2] Use _strptime module for datetime.strptime instead of our own impl --- .../modules/datetime/DateTimeBuiltins.java | 777 +----------------- 1 file changed, 8 insertions(+), 769 deletions(-) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/datetime/DateTimeBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/datetime/DateTimeBuiltins.java index 4fc7a42e6b..c617260541 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/datetime/DateTimeBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/datetime/DateTimeBuiltins.java @@ -59,34 +59,21 @@ import static com.oracle.graal.python.util.PythonUtils.TS_ENCODING; import static com.oracle.graal.python.util.PythonUtils.tsLiteral; -import java.text.ParsePosition; import java.time.DateTimeException; -import java.time.DayOfWeek; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.Month; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.chrono.IsoChronology; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.time.format.DateTimeParseException; -import java.time.format.FormatStyle; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.time.temporal.IsoFields; -import java.time.temporal.TemporalAccessor; -import java.time.temporal.WeekFields; import java.util.Arrays; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.annotations.ArgumentClinic; @@ -120,6 +107,7 @@ import com.oracle.graal.python.builtins.objects.type.slots.TpSlotRichCompare.RichCmpBuiltinNode; import com.oracle.graal.python.lib.PyFloatAsDoubleNode; import com.oracle.graal.python.lib.PyFloatCheckNode; +import com.oracle.graal.python.lib.PyImportImport; import com.oracle.graal.python.lib.PyDateCheckNode; import com.oracle.graal.python.lib.PyDateTimeCheckNode; import com.oracle.graal.python.lib.PyDeltaCheckNode; @@ -1812,765 +1800,16 @@ protected ArgumentClinicProvider getArgumentClinic() { return DateTimeBuiltinsClinicProviders.StrPTimeNodeClinicProviderGen.INSTANCE; } + static final TruffleString T_MOD_STRPTIME = tsLiteral("_strptime"); + static final TruffleString T_FUNC_STRPTIME_DATETIME = tsLiteral("_strptime_datetime"); + @Specialization static Object strptime(VirtualFrame frame, Object cls, TruffleString stringTs, TruffleString formatTs, @Bind Node inliningTarget, - @Cached TruffleString.ToJavaStringNode toJavaStringNode, - @Cached("createFor($node)") IndirectCallData.BoundaryCallData boundaryCallData) { - String string = toJavaStringNode.execute(stringTs); - String format = toJavaStringNode.execute(formatTs); - - Object saved = ExecutionContext.BoundaryCallContext.enter(frame, boundaryCallData); - try { - return parse(string, format, getContext(inliningTarget), cls, inliningTarget); - } finally { - // A Python method call (using DateTimeNodes.SubclassNewNode) should be - // connected to a current node. - ExecutionContext.BoundaryCallContext.exit(frame, boundaryCallData, saved); - } - } - - static class DateTimeBuilder { - private final Node inliningTarget; - - private Integer year; - private int month = 1; - private int day = 1; - private Integer hours; - private int minutes = 0; - private int seconds = 0; - private int microseconds = 0; - - private boolean is12HourClock; - private boolean isAm = true; - - private Integer dayOfYear; - private Integer week; - private Integer weekStartsOn; - private Integer dayOfWeek; - - private Integer yearIso8601; - private Integer weekIso8601; - - private String timeZoneName; - private Integer timeZoneUtcOffsetAsSeconds; - private Integer timeZoneUtcOffsetMicroseconds; - - DateTimeBuilder(Node inliningTarget) { - this.inliningTarget = inliningTarget; - } - - public void setYear(int year) { - this.year = year; - } - - public void setYearWithoutCentury(int year) { - if (year <= 68) { - this.year = 2000 + year; - } else { - this.year = 1900 + year; - } - } - - public void setMonth(int month) { - this.month = month; - } - - public void setDay(int day) { - this.day = day; - } - - public void setHours(int hours) { - this.hours = hours; - this.is12HourClock = false; - } - - public void set12HourClockHours(int hours) { - this.hours = hours; - this.is12HourClock = true; - } - - public void setMinutes(int minutes) { - this.minutes = minutes; - } - - public void setSeconds(int seconds) { - this.seconds = seconds; - } - - public void setMicroseconds(int microseconds) { - this.microseconds = microseconds; - } - - public void setIsAm(boolean isAm) { - this.isAm = isAm; - } - - public void setDayOfYear(int dayOfYear) { - this.dayOfYear = dayOfYear; - } - - public void setWeekStartingOnSunday(int week) { - this.week = week; - this.weekStartsOn = 6; - } - - public void setWeekStartingOnMonday(int week) { - this.week = week; - this.weekStartsOn = 0; - } - - public void setDayOfWeek(int dayOfWeek) { - // given parameter is in range 0-6 starting from Sunday - if (dayOfWeek == 0) { - this.dayOfWeek = 6; - } else { - this.dayOfWeek = dayOfWeek - 1; - } - } - - public void setDayOfWeekShortName(int dayOfWeek) { - this.dayOfWeek = dayOfWeek; - } - - public void setDayOfWeekFullName(int dayOfWeek) { - this.dayOfWeek = dayOfWeek; - } - - public void setYearIso8601(int yearIso8601) { - this.yearIso8601 = yearIso8601; - } - - public void setWeekIso8601(int weekIso8601) { - this.weekIso8601 = weekIso8601; - } - - public void setDayOfWeekIso8601(int dayOfWeekIso8601) { - // given parameter is in range 1-7 starting from Monday - this.dayOfWeek = dayOfWeekIso8601 - 1; - } - - public void setDateTime(LocalDateTime dateTime) { - year = dateTime.getYear(); - month = dateTime.getMonthValue(); - day = dateTime.getDayOfMonth(); - hours = dateTime.getHour(); - minutes = dateTime.getMinute(); - seconds = dateTime.getSecond(); - } - - public void setDate(LocalDate date) { - year = date.getYear(); - month = date.getMonthValue(); - day = date.getDayOfMonth(); - } - - public void setTime(LocalTime time) { - hours = time.getHour(); - minutes = time.getMinute(); - seconds = time.getSecond(); - } - - public void setTimeZoneName(String name) { - this.timeZoneName = name; - } - - public void setTimezoneUtcOffset(int seconds) { - this.timeZoneUtcOffsetAsSeconds = seconds; - } - - public void setTimezoneUtcOffset(int seconds, int microseconds) { - this.timeZoneUtcOffsetAsSeconds = seconds; - this.timeZoneUtcOffsetMicroseconds = microseconds; - } - - public LocalDateTime getLocalDateTime() { - // check whether there are some ambiguities - if (yearIso8601 != null) { - if (dayOfYear != null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.DAY_OF_THE_YEAR_DIRECTIVE_IS_NOT_COMPATIBLE_WITH); - } else if (weekIso8601 == null || dayOfWeek == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.ISO_YEAR_DIRECTIVE_MUST_BE_USED_WITH); - } - } else if (weekIso8601 != null) { - if (year == null || dayOfWeek == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.ISO_WEEK_DIRECTIVE_MUST_BE_USED_WITH); - } else { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.ISO_WEEK_DIRECTIVE_IS_INCOMPATIBLE_WITH); - } - } - - // handle 'Feb 29' when year isn't given so default year 1900 becomes incorrect - int year; - boolean leadYearFix = false; - if (this.year == null) { - if (this.month == 2 && this.day == 29) { - year = 1904; // 1904 is first leap year in the 20th century - leadYearFix = true; - } else { - year = 1900; - } - } else { - year = this.year; - } - - // Calculate month and day from day of year. - // If day of year is given or can be calculated - it takes precedence over - // month/day. - int month = this.month; - int day = this.day; - if (dayOfYear != null || (dayOfWeek != null && week != null) || (dayOfWeek != null && weekIso8601 != null)) { - final LocalDate date; - - if (this.dayOfYear != null) { - date = LocalDate.ofYearDay(year, dayOfYear); - } else if (week != null) { - final WeekFields weekFields; - int dayOfWeek; - - assert weekStartsOn == 0 || weekStartsOn == 6; // either Monday or Sunday - - if (weekStartsOn == 6) { - weekFields = WeekFields.of(DayOfWeek.SUNDAY, 7); - - // convert Monday-based day of week to Sunday-based one - if (this.dayOfWeek == 6) { - dayOfWeek = 0; - } else { - dayOfWeek = this.dayOfWeek + 1; - } - dayOfWeek = dayOfWeek + 1; // convert from range 0..6 to 1..7 - } else { - weekFields = WeekFields.of(DayOfWeek.MONDAY, 7); - dayOfWeek = this.dayOfWeek + 1; // convert from range 0..6 to 1..7 - } - - date = LocalDate.of(year, 1, 1).with(weekFields.weekOfYear(), week).with(weekFields.dayOfWeek(), dayOfWeek); - } else { - int dayOfWeek = this.dayOfWeek + 1; // convert from range 0..6 to 1..7 - date = LocalDate.now().with(IsoFields.WEEK_BASED_YEAR, this.yearIso8601).with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, this.weekIso8601).with(ChronoField.DAY_OF_WEEK, dayOfWeek); - } - - year = date.getYear(); - month = date.getMonthValue(); - day = date.getDayOfMonth(); - } - - if (leadYearFix) { - // year wasn't given but the date is Feb 29th. We couldn't use the default of - // 1900 for computations so changed it and set it back now. - year = 1900; - } - - // calculate hours - final int hours; - if (this.hours == null) { - hours = 0; - } else if (!this.is12HourClock) { - hours = this.hours; - } else { - if (isAm) { - if (this.hours == 12) { - hours = 0; // 12 AM == midnight == hour 0 - } else { - hours = this.hours; - } - } else { - if (this.hours == 12) { - hours = 12; // 12 PM == midday == hour 12 - } else { - hours = this.hours + 12; - } - } - } - - return LocalDateTime.of(year, month, day, hours, this.minutes, this.seconds, this.microseconds * 1_000); - } - - String getTimeZoneName() { - return timeZoneName; - } - - Integer getTimeZoneUtcOffsetAsSeconds() { - return timeZoneUtcOffsetAsSeconds; - } - - Integer getTimeZoneUtcOffsetMicroseconds() { - return timeZoneUtcOffsetMicroseconds; - } - } - - @TruffleBoundary - private static Object parse(String string, String format, PythonContext context, Object cls, Node inliningTarget) { - try { - var builder = new DateTimeBuilder(inliningTarget); - int i = 0, j = 0; - - while (i < string.length() && j < format.length()) { - if (format.charAt(j) != '%') { - if (string.charAt(i) != format.charAt(j)) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - i++; - j++; - } else { - j++; // move from '%' to the format code character - - switch (format.charAt(j)) { - case 'a' -> { - String pattern = "EEE"; // short form - var position = new ParsePosition(i); - TemporalAccessor accessor = parseLocalizedComponent(string, pattern, position); - DayOfWeek dayOfWeek = DayOfWeek.from(accessor); - - int a = dayOfWeek.getValue() - 1; // DayOfWeek numeric values is - // 1-based, so 1 is Monday etc - builder.setDayOfWeekShortName(a); - - i = position.getIndex(); - } - case 'A' -> { - String pattern = "EEEE"; // full form - var position = new ParsePosition(i); - TemporalAccessor accessor = parseLocalizedComponent(string, pattern, position); - DayOfWeek dayOfWeek = DayOfWeek.from(accessor); - - int a = dayOfWeek.getValue() - 1; // DayOfWeek numeric values is - // 1-based, so 1 is Monday etc - builder.setDayOfWeekFullName(a); - - i = position.getIndex(); - } - case 'w' -> { - Integer w = parseDigits(string, i, 1); - - if (w == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setDayOfWeek(w); - i += 1; - } - case 'd' -> { - var pos = new ParsePosition(i); - Integer d = parseDigitsUpTo(string, pos, 2); - - if (d == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setDay(d); - i = pos.getIndex(); - } - case 'b' -> { - String pattern = "LLL"; // short form - var position = new ParsePosition(i); - TemporalAccessor accessor = parseLocalizedComponent(string, pattern, position); - Month month = Month.from(accessor); - int b = month.getValue(); - - builder.setMonth(b); - i = position.getIndex(); - } - case 'B' -> { - String pattern = "LLLL"; // full form - var position = new ParsePosition(i); - TemporalAccessor accessor = parseLocalizedComponent(string, pattern, position); - Month month = Month.from(accessor); - int b = month.getValue(); - - builder.setMonth(b); - i = position.getIndex(); - } - case 'm' -> { - var pos = new ParsePosition(i); - Integer m = parseDigitsUpTo(string, pos, 2); - - if (m == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setMonth(m); - i = pos.getIndex(); - } - case 'y' -> { - Integer y = parseDigits(string, i, 2); - - if (y == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setYearWithoutCentury(y); - i += 2; - } - case 'Y' -> { - Integer y = parseDigits(string, i, 4); - - if (y == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setYear(y); - i += 4; - } - case 'H' -> { - var pos = new ParsePosition(i); - Integer h = parseDigitsUpTo(string, pos, 2); - - if (h == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setHours(h); - i = pos.getIndex(); - } - case 'I' -> { - var pos = new ParsePosition(i); - Integer h = parseDigitsUpTo(string, pos, 2); - - if (h == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.set12HourClockHours(h); - i = pos.getIndex(); - } - case 'p' -> { - // TODO: localize it - String p = string.substring(i, i + 2); - - if (!p.equalsIgnoreCase("am") && !p.equalsIgnoreCase("pm")) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - boolean isAm = p.equalsIgnoreCase("am"); - builder.setIsAm(isAm); - i += 2; - } - case 'M' -> { - var pos = new ParsePosition(i); - Integer m = parseDigitsUpTo(string, pos, 2); - - if (m == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setMinutes(m); - i = pos.getIndex(); - } - case 'S' -> { - var pos = new ParsePosition(i); - Integer s = parseDigitsUpTo(string, pos, 2); - - if (s == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setSeconds(s); - i = pos.getIndex(); - } - case 'f' -> { - var pos = new ParsePosition(i); - Integer f = parseDigitsUpTo(string, pos, 6); - - if (f == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - // complete microseconds up to 6 digits - int length = pos.getIndex() - i; - if (length < 6) { - for (int k = 1; k <= 6 - length; k++) { - f *= 10; - } - } - - builder.setMicroseconds(f); - i = pos.getIndex(); - } - case 'z' -> { - if (string.charAt(i) == 'Z') { - builder.setTimezoneUtcOffset(0); - i += 1; - } else { - String regex = "\\A[+-]\\d\\d:?[0-5]\\d(:?[0-5]\\d(\\.\\d{1,6})?)?"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(string); - matcher.region(i, string.length()); - - if (matcher.lookingAt()) { - int pos = i; - boolean hasSeparator = false; - - int sign = string.charAt(pos) == '+' ? 1 : -1; - pos += 1; - - Integer hours = parseDigits(string, pos, 2); - pos += 2; - - if (string.charAt(pos) == ':') { - hasSeparator = true; - pos += 1; - } - - Integer minutes = parseDigits(string, pos, 2); - pos += 2; - - if (pos == matcher.end()) { - // [+-]HH:MM - int secondsTotal = sign * (hours * 3600 + minutes * 60); - builder.setTimezoneUtcOffset(secondsTotal); - } else { - // [+-]HH:SS:MM and optional microseconds - if (hasSeparator != (string.charAt(pos) == ':')) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.INCONSISTENT_USE_OF_COLON_IN_S, string.substring(i, matcher.end())); - } - - if (string.charAt(pos) == ':') { - pos += 1; - } - - Integer seconds = parseDigits(string, pos, 2); - pos += 2; - - if (pos == matcher.end()) { - int secondsTotal = sign * (hours * 3600 + minutes * 60 + seconds); - builder.setTimezoneUtcOffset(secondsTotal); - } else { - pos += 1; // skip '.' - - int length = matcher.end() - pos; - Integer microseconds = parseDigits(string, pos, length); - - // complete microseconds up to 6 digits - if (length < 6) { - for (int k = 1; k <= 6 - length; k++) { - microseconds *= 10; - } - } - - int secondsTotal = sign * (hours * 3600 + minutes * 60 + seconds); - builder.setTimezoneUtcOffset(secondsTotal, sign * microseconds); - } - } - - i = matcher.end(); - } else { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - } - } - case 'Z' -> { - TimeZone timeZone = TimeModuleBuiltins.getGlobalTimeZone(context); - String zoneName = timeZone.getDisplayName(false, TimeZone.SHORT); - String zoneNameDaylightSaving = timeZone.getDisplayName(true, TimeZone.SHORT); - String matchedZoneName = matchTimeZoneName(string, i, zoneName, zoneNameDaylightSaving, "UTC", "GMT"); - - if (matchedZoneName == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setTimeZoneName(matchedZoneName); - i += matchedZoneName.length(); - } - case 'j' -> { - var pos = new ParsePosition(i); - Integer jj = parseDigitsUpTo(string, pos, 3); - - if (jj == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setDayOfYear(jj); - i = pos.getIndex(); - } - case 'U' -> { - Integer u = parseDigits(string, i, 2); - - if (u == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setWeekStartingOnSunday(u); - i += 2; - } - case 'W' -> { - var pos = new ParsePosition(i); - Integer w = parseDigitsUpTo(string, pos, 2); - - if (w == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setWeekStartingOnMonday(w); - i = pos.getIndex(); - } - case 'c' -> { - // TODO: don't hardcore format and use a localized one - String pattern = "E M d HH:mm:ss y"; - var position = new ParsePosition(i); - TemporalAccessor accessor = parseLocalizedComponent(string, pattern, position); - LocalDateTime localDateTime = LocalDateTime.from(accessor); - - builder.setDateTime(localDateTime); - i = position.getIndex(); - } - case 'x' -> { - var locale = Locale.getDefault(); - String pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT, null, IsoChronology.INSTANCE, locale); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, locale); - var position = new ParsePosition(i); - TemporalAccessor accessor = formatter.parse(string, position); - LocalDate localDate = LocalDate.from(accessor); - - builder.setDate(localDate); - i = position.getIndex(); - } - case 'X' -> { - // TODO: don't hardcore format and use a localized one - String pattern = "HH:mm:ss"; - var position = new ParsePosition(i); - TemporalAccessor accessor = parseLocalizedComponent(string, pattern, position); - LocalTime localTime = LocalTime.from(accessor); - - builder.setTime(localTime); - i = position.getIndex(); - } - case '%' -> { - // just do nothing, it's escaped '%' - } - case 'G' -> { - Integer g = parseDigits(string, i, 4); - - if (g == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setYearIso8601(g); - i += 4; - } - case 'u' -> { - Integer u = parseDigits(string, i, 1); - - if (u == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setDayOfWeekIso8601(u); - i += 1; - } - case 'V' -> { - var pos = new ParsePosition(i); - Integer v = parseDigitsUpTo(string, pos, 2); - - if (v == null) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - builder.setWeekIso8601(v); - i = pos.getIndex(); - } - default -> - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.S_IS_A_BAD_DIRECTIVE_IN_FORMAT_S, String.valueOf(format.charAt(j)), format); - } - - j++; // move to the next character after the format code - } - - } - - // extra characters in the source string - if (i < string.length()) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.UNCONVERTED_DATA_REMAINS_S, string.substring(i)); - } - - // extra characters in the format string - if (j < format.length()) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - - LocalDateTime localDateTime = builder.getLocalDateTime(); - - final Object tzInfo; - if (builder.getTimeZoneUtcOffsetAsSeconds() != null) { - final PTimeDelta utcOffset; - if (builder.getTimeZoneUtcOffsetMicroseconds() == null) { - utcOffset = TimeDeltaNodes.NewNode.getUncached().executeBuiltin(inliningTarget, - 0, builder.getTimeZoneUtcOffsetAsSeconds(), 0, 0, 0, 0, 0); - } else { - utcOffset = TimeDeltaNodes.NewNode.getUncached().executeBuiltin(inliningTarget, - 0, builder.getTimeZoneUtcOffsetAsSeconds(), builder.getTimeZoneUtcOffsetMicroseconds(), 0, 0, 0, 0); - } - - if (builder.getTimeZoneName() == null) { - tzInfo = TimeZoneNodes.NewNode.getUncached().execute(inliningTarget, getContext(inliningTarget), PythonBuiltinClassType.PTimezone, utcOffset, PNone.NO_VALUE); - } else { - TruffleString name = TruffleString.FromJavaStringNode.getUncached().execute(builder.getTimeZoneName(), TS_ENCODING); - tzInfo = TimeZoneNodes.NewNode.getUncached().execute(inliningTarget, getContext(inliningTarget), PythonBuiltinClassType.PTimezone, utcOffset, name); - } - } else { - tzInfo = PNone.NONE; - } - - return toPDateTime(localDateTime, tzInfo, 0, inliningTarget, cls); - } catch (IndexOutOfBoundsException | DateTimeParseException e) { - throw PRaiseNode.raiseStatic(inliningTarget, ValueError, ErrorMessages.TIME_DATA_S_DOES_NOT_MATCH_FORMAT_S, string, format); - } - } - - @TruffleBoundary - private static Integer parseDigits(String source, int from, int digitsCount) { - int result = 0; - - for (int i = 0; i < digitsCount; i++) { - int n = source.charAt(from + i) - '0'; - if (n < 0 || n > 9) { - return null; - } - result = result * 10 + n; - } - - return result; - } - - private static String matchTimeZoneName(String string, int from, String... candidates) { - String matched = null; - for (String candidate : candidates) { - if (candidate != null && string.startsWith(candidate, from) && (matched == null || candidate.length() > matched.length())) { - matched = candidate; - } - } - return matched; - } - - @TruffleBoundary - private static Integer parseDigitsUpTo(String source, ParsePosition from, int maxDigitsCount) { - int result = 0; - int limit = Math.min(maxDigitsCount, source.length() - from.getIndex()); - - for (int i = 0; i < limit; i++) { - int n = source.charAt(from.getIndex() + i) - '0'; - if (n < 0 || n > 9) { - if (i == 0) { - return null; - } else { - from.setIndex(from.getIndex() + i); - return result; - } - } - result = result * 10 + n; - } - - from.setIndex(from.getIndex() + limit); - return result; - } - - @TruffleBoundary - private static TemporalAccessor parseLocalizedComponent(String source, String pattern, ParsePosition pos) { - var locale = Locale.getDefault(); - var formatter = DateTimeFormatter.ofPattern(pattern, locale); - return formatter.parse(source, pos); + @Cached PyImportImport importNode, + @Cached PyObjectCallMethodObjArgs callNode) { + Object module = importNode.execute(frame, inliningTarget, T_MOD_STRPTIME); + return callNode.execute(frame, inliningTarget, module, T_FUNC_STRPTIME_DATETIME, cls, stringTs, formatTs); } } From 5100c20edd2f4d8fc24a4002b6bfa89392c5130c Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Tue, 16 Jun 2026 10:54:12 +0200 Subject: [PATCH 2/2] Add tests for upper case datetime parsing --- .../src/tests/test_datetime.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_datetime.py b/graalpython/com.oracle.graal.python.test/src/tests/test_datetime.py index 03d18c7258..943e52b46a 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_datetime.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_datetime.py @@ -603,6 +603,13 @@ def test_strptime(self): with self.assertRaisesRegex(ValueError, "Inconsistent use of : in \\+00:0000"): datetime.datetime.strptime("+00:0000", "%z") + with self.assertRaises(ValueError): + datetime.datetime.strptime("APRIL 31", "%B %d") + + actual = datetime.datetime.strptime("APRIL 30", "%B %d") + expected = datetime.datetime(1900, 4, 30, 0, 0, 0) + self.assertEqual(actual, expected) + # ambiguity handling