Skip to content
This repository has been archived by the owner on Jul 17, 2024. It is now read-only.

feat: Add strptime and strftime to datetime classes #101

Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ private static void registerMethods() throws NoSuchMethodException {
DATE_TYPE.addMethod("isoformat",
PythonDate.class.getMethod("iso_format"));

DATE_TYPE.addMethod("strftime",
ArgumentSpec.forFunctionReturning("strftime", PythonString.class.getName())
.addArgument("format", PythonString.class.getName())
.asPythonFunctionSignature(PythonDate.class.getMethod("strftime", PythonString.class)));

DATE_TYPE.addMethod("ctime",
PythonDate.class.getMethod("ctime"));

Expand Down Expand Up @@ -363,8 +368,8 @@ public PythonString ctime() {
}

public PythonString strftime(PythonString format) {
// TODO
throw new UnsupportedOperationException();
var formatter = PythonDateTimeFormatter.getDateTimeFormatter(format.value);
return PythonString.valueOf(formatter.format(localDate));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ai.timefold.jpyinterpreter.types.datetime;

import java.time.Clock;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
Expand All @@ -10,8 +11,10 @@
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalQuery;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -120,6 +123,16 @@ private static void registerMethods() throws NoSuchMethodException {
PythonNumber.class,
PythonLikeObject.class)));

DATE_TIME_TYPE.addMethod("strptime",
ArgumentSpec.forFunctionReturning("strptime", PythonDateTime.class.getName())
.addArgument("datetime_type", PythonLikeType.class.getName())
.addArgument("date_string", PythonString.class.getName())
.addArgument("format", PythonString.class.getName())
.asClassPythonFunctionSignature(PythonDateTime.class.getMethod("strptime",
PythonLikeType.class,
PythonString.class,
PythonString.class)));

DATE_TIME_TYPE.addMethod("utcfromtimestamp",
ArgumentSpec.forFunctionReturning("utcfromtimestamp", PythonDate.class.getName())
.addArgument("date_type", PythonLikeType.class.getName())
Expand Down Expand Up @@ -203,6 +216,11 @@ private static void registerMethods() throws NoSuchMethodException {
.asPythonFunctionSignature(
PythonDateTime.class.getMethod("iso_format", PythonString.class, PythonString.class)));

DATE_TIME_TYPE.addMethod("strftime",
ArgumentSpec.forFunctionReturning("strftime", PythonString.class.getName())
.addArgument("format", PythonString.class.getName())
.asPythonFunctionSignature(PythonDateTime.class.getMethod("strftime", PythonString.class)));

DATE_TIME_TYPE.addMethod("ctime",
PythonDateTime.class.getMethod("ctime"));

Expand Down Expand Up @@ -506,6 +524,38 @@ public static PythonDate from_iso_calendar(PythonInteger year, PythonInteger wee
}
}

private static <T> T tryParseOrNull(DateTimeFormatter formatter, String text, TemporalQuery<T> query) {
try {
return formatter.parse(text, query);
} catch (DateTimeException e) {
return null;
}
}

public static PythonDateTime strptime(PythonLikeType type, PythonString date_string, PythonString format) {
if (type != DATE_TIME_TYPE) {
throw new TypeError("Unknown datetime type (" + type + ").");
}
var formatter = PythonDateTimeFormatter.getDateTimeFormatter(format.value);
var asZonedDateTime = tryParseOrNull(formatter, date_string.value, ZonedDateTime::from);
if (asZonedDateTime != null) {
return new PythonDateTime(asZonedDateTime);
}
var asLocalDateTime = tryParseOrNull(formatter, date_string.value, LocalDateTime::from);
if (asLocalDateTime != null) {
return new PythonDateTime(asLocalDateTime);
}
var asLocalDate = tryParseOrNull(formatter, date_string.value, LocalDate::from);
if (asLocalDate != null) {
return new PythonDateTime(asLocalDate.atTime(LocalTime.MIDNIGHT));
}
var asLocalTime = tryParseOrNull(formatter, date_string.value, LocalTime::from);
if (asLocalTime != null) {
return new PythonDateTime(asLocalTime.atDate(LocalDate.of(1900, 1, 1)));
}
throw new ValueError("data " + date_string.repr() + " does not match the format " + format.repr());
}

public PythonDateTime add_time_delta(PythonTimeDelta summand) {
if (dateTime instanceof LocalDateTime) {
return new PythonDateTime(((LocalDateTime) dateTime).plus(summand.duration));
Expand Down Expand Up @@ -699,8 +749,8 @@ public PythonString ctime() {

@Override
public PythonString strftime(PythonString format) {
// TODO
throw new UnsupportedOperationException();
var formatter = PythonDateTimeFormatter.getDateTimeFormatter(format.value);
return PythonString.valueOf(formatter.format(dateTime));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package ai.timefold.jpyinterpreter.types.datetime;

import java.time.DayOfWeek;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.FormatStyle;
import java.time.format.TextStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.WeekFields;
import java.util.regex.Pattern;

import ai.timefold.jpyinterpreter.types.errors.ValueError;

/**
* Based on the format specified
* <a href="https://docs.python.org/3.11/library/datetime.html#strftime-and-strptime-format-codes">in
* the datetime documentation</a>.
*/
public class PythonDateTimeFormatter {
private final static Pattern DIRECTIVE_PATTERN = Pattern.compile("([^%]*)%(.)");

static DateTimeFormatter getDateTimeFormatter(String pattern) {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
var matcher = DIRECTIVE_PATTERN.matcher(pattern);
int endIndex = 0;
while (matcher.find()) {
var literalPart = matcher.group(1);
builder.appendLiteral(literalPart);
endIndex = matcher.end();

char directive = matcher.group(2).charAt(0);
switch (directive) {
case 'a' -> {
builder.appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT);
}
case 'A' -> {
builder.appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL);
}
case 'w' -> {
builder.appendValue(ChronoField.DAY_OF_WEEK);
}
case 'd' -> {
builder.appendValue(ChronoField.DAY_OF_MONTH, 2);
}
case 'b' -> {
builder.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT);
}
case 'B' -> {
builder.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL);
}
case 'm' -> {
builder.appendValue(ChronoField.MONTH_OF_YEAR, 2);
}
case 'y' -> {
builder.appendPattern("uu");
}
case 'Y' -> {
builder.appendValue(ChronoField.YEAR);
}
case 'H' -> {
builder.appendValue(ChronoField.HOUR_OF_DAY, 2);
}
case 'I' -> {
builder.appendValue(ChronoField.HOUR_OF_AMPM, 2);
}
case 'p' -> {
builder.appendText(ChronoField.AMPM_OF_DAY);
}
case 'M' -> {
builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
}
case 'S' -> {
builder.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
}
case 'f' -> {
builder.appendValue(ChronoField.MICRO_OF_SECOND, 6);
}
case 'z' -> {
builder.appendOffset("+HHmmss", "");
}
case 'Z' -> {
builder.appendZoneOrOffsetId();
}
case 'j' -> {
builder.appendValue(ChronoField.DAY_OF_YEAR, 3);
}
case 'U' -> {
builder.appendValue(WeekFields.of(DayOfWeek.SUNDAY, 7).weekOfYear(), 2);
}
case 'W' -> {
builder.appendValue(WeekFields.of(DayOfWeek.MONDAY, 7).weekOfYear(), 2);
}
case 'c' -> {
builder.appendLocalized(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
}
case 'x' -> {
builder.appendLocalized(FormatStyle.MEDIUM, null);
}
case 'X' -> {
builder.appendLocalized(null, FormatStyle.MEDIUM);
}
case '%' -> {
builder.appendLiteral("%");
}
case 'G' -> {
builder.appendValue(WeekFields.of(DayOfWeek.MONDAY, 4).weekBasedYear());
}
case 'u' -> {
builder.appendValue(WeekFields.of(DayOfWeek.MONDAY, 4).dayOfWeek(), 1);
}
case 'V' -> {
builder.appendValue(WeekFields.of(DayOfWeek.MONDAY, 4).weekOfYear(), 2);
}
default -> {
throw new ValueError("Invalid directive (" + directive + ") in format string (" + pattern + ").");
}
}
}
builder.appendLiteral(pattern.substring(endIndex));
return builder.toFormatter();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ private static void registerMethods() throws NoSuchMethodException {
.addArgument("timespec", PythonString.class.getName(), PythonString.valueOf("auto"))
.asPythonFunctionSignature(PythonTime.class.getMethod("isoformat", PythonString.class)));

TIME_TYPE.addMethod("strftime",
ArgumentSpec.forFunctionReturning("strftime", PythonString.class.getName())
.addArgument("format", PythonString.class.getName())
.asPythonFunctionSignature(PythonTime.class.getMethod("strftime", PythonString.class)));

TIME_TYPE.addMethod("tzname",
PythonTime.class.getMethod("tzname"));

Expand Down Expand Up @@ -328,6 +333,11 @@ public PythonString isoformat(PythonString formatSpec) {
return PythonString.valueOf(result);
}

public PythonString strftime(PythonString formatSpec) {
var formatter = PythonDateTimeFormatter.getDateTimeFormatter(formatSpec.value);
return PythonString.valueOf(formatter.format(localTime));
}

@Override
public PythonString $method$__str__() {
return PythonString.valueOf(toString());
Expand Down
18 changes: 17 additions & 1 deletion jpyinterpreter/src/main/python/jvm_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jpype.imports
import importlib.resources
import os
import locale
from typing import List, ContextManager


Expand Down Expand Up @@ -52,7 +53,22 @@ def init(*args, path: List[str] = None, include_translator_jars: bool = True,
path = []
if include_translator_jars:
path = path + extract_python_translator_jars()
jpype.startJVM(*args, classpath=path, convertStrings=True) # noqa

user_locale = locale.getlocale()[0]
extra_jvm_args = []
if user_locale is not None:
user_locale = locale.normalize(user_locale)
if '.' in user_locale:
user_locale, _ = user_locale.split('.', 1)
if '_' in user_locale:
lang, country = user_locale.rsplit('_', maxsplit=1)
extra_jvm_args.append(f'-Duser.language={lang}')
extra_jvm_args.append(f'-Duser.country={country}')
else:
extra_jvm_args.append(f'-Duser.language={user_locale}')


jpype.startJVM(*args, *extra_jvm_args, classpath=path, convertStrings=True) # noqa

if class_output_path is not None:
from ai.timefold.jpyinterpreter import InterpreterStartupOptions # noqa
Expand Down
2 changes: 2 additions & 0 deletions jpyinterpreter/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from typing import Callable, Any
from copy import deepcopy
import locale


def get_argument_cloner(clone_arguments):
Expand Down Expand Up @@ -203,6 +204,7 @@ def pytest_sessionstart(session):
import pathlib
import sys

locale.setlocale(locale.LC_ALL, 'en_US')
class_output_path = None
if session.config.getoption('--output-generated-classes') != 'false':
class_output_path = pathlib.Path('target', 'tox-generated-classes', 'python',
Expand Down
46 changes: 46 additions & 0 deletions jpyinterpreter/tests/datetime/test_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,49 @@ def function(x: date) -> str:
verifier = verifier_for(function)

verifier.verify(date(2002, 12, 4), expected_result='Wed Dec 4 00:00:00 2002')


def test_strftime():
def function(x: date, fmt: str) -> str:
return x.strftime(fmt)

verifier = verifier_for(function)

verifier.verify(date(1, 2, 3), '%a',
expected_result='Sat')
verifier.verify(date(1, 2, 3), '%A',
expected_result='Saturday')
verifier.verify(date(1, 2, 3), '%W',
expected_result='05')
verifier.verify(date(1, 2, 3), '%d',
expected_result='03')
verifier.verify(date(1, 2, 3), '%b',
expected_result='Feb')
verifier.verify(date(1, 2, 3), '%B',
expected_result='February')
verifier.verify(date(1, 2, 3), '%m',
expected_result='02')
verifier.verify(date(1, 2, 3), '%y',
expected_result='01')
verifier.verify(date(1001, 2, 3), '%y',
expected_result='01')
# %Y have different results depending on the platform;
# Windows 0-pad it, Linux does not.
# verifier.verify(date(1, 2, 3), '%Y',
# expected_result='1')
verifier.verify(date(1, 2, 3), '%j',
expected_result='034')
verifier.verify(date(1, 2, 3), '%U',
expected_result='04')
verifier.verify(date(1, 2, 3), '%W',
expected_result='05')
# %Y have different results depending on the platform;
# Windows 0-pad it, Linux does not.
# verifier.verify(date(1, 2, 3), '%G',
# expected_result='1')
verifier.verify(date(1, 2, 3), '%u',
expected_result='6')
verifier.verify(date(1, 2, 3), '%%',
expected_result='%')
verifier.verify(date(1, 2, 3), '%V',
expected_result='05')
Loading
Loading