Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make fewer boundary crossings in time.py #4440

Merged
Merged
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
devinrsmith marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.deephaven.integrations.python;

import io.deephaven.util.annotations.ScriptApi;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZonedDateTime;

/**
* Utility functions for {@code time.py} to extract {@link java.time} components in a single call.
*/
@ScriptApi
public class PythonTimeComponents {

/**
* Extracts the components from a {@link LocalTime}. Equivalent to {@code new int[] {dt.getHour(), dt.getMinute(),
* dt.getSecond(), dt.getNano()}}.
*
* @param dt the local time
* @return the components
*/
@ScriptApi
public static int[] getLocalTimeComponents(LocalTime dt) {
return new int[] {dt.getHour(), dt.getMinute(), dt.getSecond(), dt.getNano()};
}

/**
* Extracts the local time components from a {@link ZonedDateTime}. Equivalent to
* {@code getComponents(dt.toLocalTime())}.
*
* @param dt the zoned date time
* @return the components
* @see ZonedDateTime#toLocalTime()
* @see #getLocalTimeComponents(LocalTime)
*/
@ScriptApi
public static int[] getLocalTimeComponents(ZonedDateTime dt) {
return getLocalTimeComponents(dt.toLocalTime());
}

/**
* Extracts the components from an {@link Instant}. Equivalent to {@code new long[] {dt.getEpochSecond(),
* dt.getNano()}}.
*
* @param dt the instant
* @return the components
*/
@ScriptApi
public static long[] getInstantComponents(Instant dt) {
return new long[] {dt.getEpochSecond(), dt.getNano()};
}

/**
* Extracts the {@link Instant} components from a {@link ZonedDateTime}. Equivalent to
* {@code getComponents(dt.toInstant())}.
*
* @param dt the zoned date time
* @return the components
* @see #getInstantComponents(Instant)
* @see ZonedDateTime#toInstant()
*/
@ScriptApi
public static long[] getInstantComponents(ZonedDateTime dt) {
// This would be a little bit less efficient, since dt.toInstant() allocates a new object
// return getComponents(dt.toInstant());
return new long[] {dt.toEpochSecond(), dt.getNano()};
}

/**
* Extracts the components from a {@link Duration}. Equivalent to {@code new long[] {dt.getSeconds(),
* dt.getNano()}}.
*
* @param dt the duration
* @return the components
*/
@ScriptApi
public static long[] getDurationComponents(Duration dt) {
return new long[] {dt.getSeconds(), dt.getNano()};
}

/**
* Extracts the components from a {@link Period}. Equivalent to {@code new int[] {dt.getYears(), dt.getMonths(),
* dt.getDays()}}.
*
* @param dt the period
* @return the components
*/
@ScriptApi
public static int[] getPeriodComponents(Period dt) {
return new int[] {dt.getYears(), dt.getMonths(), dt.getDays()};
}

/**
* Extracts the components from a {@link LocalDate}. Equivalent to {@code new int[] {dt.getYear(),
* dt.getMonthValue(), dt.getDayOfMonth()}}.
*
* @param dt the local date
* @return the components
*/
@ScriptApi
public static int[] getLocalDateComponents(LocalDate dt) {
return new int[] {dt.getYear(), dt.getMonthValue(), dt.getDayOfMonth()};
}

/**
* Extracts the {@link LocalDate} components from a {@link ZonedDateTime}. Equivalent to
* {@code getComponents(dt.toLocalDate())}.
*
* @param dt the zoned date time
* @return the components
* @see #getLocalDateComponents(LocalDate)
* @see ZonedDateTime#toLocalDate()
*/
@ScriptApi
public static int[] getLocalDateComponents(ZonedDateTime dt) {
return getLocalDateComponents(dt.toLocalDate());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.deephaven.integrations.python;

import org.junit.Assert;
import org.junit.Test;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import static io.deephaven.integrations.python.PythonTimeComponents.getDurationComponents;
import static io.deephaven.integrations.python.PythonTimeComponents.getInstantComponents;
import static io.deephaven.integrations.python.PythonTimeComponents.getLocalDateComponents;
import static io.deephaven.integrations.python.PythonTimeComponents.getLocalTimeComponents;
import static io.deephaven.integrations.python.PythonTimeComponents.getPeriodComponents;

public class PythonTimeComponentsTest {

private static final LocalTime TIME_1234 = LocalTime.of(1, 2, 3, 4);
private static final Instant INSTANT_42_43 = Instant.ofEpochSecond(42, 43);
private static final LocalDate DATE_2023_FEB_7 = LocalDate.of(2023, Month.FEBRUARY, 7);
private static final Duration DURATION_101_102 = Duration.ofSeconds(101, 102);
private static final Period PERIOD_3_4_5 = Period.of(3, 4, 5);

@Test
public void getLocalTimeComponentsFromLocalTime() {
Assert.assertArrayEquals(new int[] {1, 2, 3, 4}, getLocalTimeComponents(TIME_1234));
}

@Test
public void getLocalTimeComponentsFromZonedDateTime() {
Assert.assertArrayEquals(new int[] {1, 2, 3, 4},
getLocalTimeComponents(ZonedDateTime.of(DATE_2023_FEB_7, TIME_1234, ZoneId.systemDefault())));
}

@Test
public void getInstantComponentsFromInstant() {
Assert.assertArrayEquals(new long[] {42, 43}, getInstantComponents(INSTANT_42_43));
}

@Test
public void getInstantComponentsFromZonedDateTime() {
Assert.assertArrayEquals(new long[] {42, 43},
getInstantComponents(INSTANT_42_43.atZone(ZoneId.systemDefault())));
}

@Test
public void getDurationComponentsFromDuration() {
Assert.assertArrayEquals(new long[] {101, 102}, getDurationComponents(DURATION_101_102));
}

@Test
public void getPeriodComponentsFromPeriod() {
Assert.assertArrayEquals(new int[] {3, 4, 5}, getPeriodComponents(PERIOD_3_4_5));
}

@Test
public void getLocalDateComponentsFromLocalDate() {
Assert.assertArrayEquals(new int[] {2023, 2, 7}, getLocalDateComponents(DATE_2023_FEB_7));
}

@Test
public void getLocalDateComponentsFromLocalDateFromZonedDateTime() {
Assert.assertArrayEquals(new int[] {2023, 2, 7},
getLocalDateComponents(ZonedDateTime.of(DATE_2023_FEB_7, TIME_1234, ZoneId.systemDefault())));
}
}
61 changes: 33 additions & 28 deletions py/server/deephaven/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
from deephaven.dtypes import Instant, LocalDate, LocalTime, ZonedDateTime, Duration, Period, TimeZone

_JDateTimeUtils = jpy.get_type("io.deephaven.time.DateTimeUtils")
_JPythonTimeComponents = jpy.get_type("io.deephaven.integrations.python.PythonTimeComponents")
_JLocalDate = jpy.get_type("java.time.LocalDate")
_JLocalTime = jpy.get_type("java.time.LocalTime")
_JInstant = jpy.get_type("java.time.Instant")
_JZonedDateTime = jpy.get_type("java.time.ZonedDateTime")
_JDuration = jpy.get_type("java.time.Duration")
_JPeriod = jpy.get_type("java.time.Period")

_nanos_per_second = 1000000000
_nanos_per_micro = 1000
_NANOS_PER_SECOND = 1000000000
_NANOS_PER_MICRO = 1000


# region Clock
Expand Down Expand Up @@ -277,9 +278,9 @@ def to_j_local_time(dt: Union[None, LocalTime, int, str, datetime.time, datetime
elif isinstance(dt, str):
return _JDateTimeUtils.parseLocalTime(dt)
elif isinstance(dt, pandas.Timestamp):
return _JLocalTime.of(dt.hour, dt.minute, dt.second, dt.microsecond * _nanos_per_micro + dt.nanosecond)
return _JLocalTime.of(dt.hour, dt.minute, dt.second, dt.microsecond * _NANOS_PER_MICRO + dt.nanosecond)
elif isinstance(dt, datetime.time) or isinstance(dt, datetime.datetime):
return _JLocalTime.of(dt.hour, dt.minute, dt.second, dt.microsecond * _nanos_per_micro)
return _JLocalTime.of(dt.hour, dt.minute, dt.second, dt.microsecond * _NANOS_PER_MICRO)
elif isinstance(dt, numpy.datetime64):
# Conversion only supports micros resolution
return to_j_local_time(dt.astype(datetime.time))
Expand Down Expand Up @@ -327,11 +328,11 @@ def to_j_instant(dt: Union[None, Instant, int, str, datetime.datetime, numpy.dat
elif isinstance(dt, datetime.datetime):
epoch_time = dt.timestamp()
epoch_sec = int(epoch_time)
nanos = int((epoch_time - epoch_sec) * _nanos_per_second)
nanos = int((epoch_time - epoch_sec) * _NANOS_PER_SECOND)
return _JInstant.ofEpochSecond(epoch_sec, nanos)
elif isinstance(dt, numpy.datetime64):
epoch_nanos = dt.astype('datetime64[ns]').astype(numpy.int64)
epoch_sec, nanos = divmod(epoch_nanos, _nanos_per_second)
epoch_sec, nanos = divmod(epoch_nanos, _NANOS_PER_SECOND)
return _JInstant.ofEpochSecond(int(epoch_sec), int(nanos))
elif isinstance(dt, pandas.Timestamp):
return _JDateTimeUtils.epochNanosToInstant(dt.value)
Expand Down Expand Up @@ -439,10 +440,10 @@ def to_j_duration(dt: Union[None, Duration, int, str, datetime.timedelta, numpy.
elif isinstance(dt, str):
return _JDateTimeUtils.parseDuration(dt)
elif isinstance(dt, pandas.Timedelta):
nanos = int((dt / datetime.timedelta(microseconds=1)) * _nanos_per_micro) + dt.nanoseconds
nanos = int((dt / datetime.timedelta(microseconds=1)) * _NANOS_PER_MICRO) + dt.nanoseconds
return _JDuration.ofNanos(nanos)
elif isinstance(dt, datetime.timedelta):
nanos = int((dt / datetime.timedelta(microseconds=1)) * _nanos_per_micro)
nanos = int((dt / datetime.timedelta(microseconds=1)) * _NANOS_PER_MICRO)
return _JDuration.ofNanos(nanos)
elif isinstance(dt, numpy.timedelta64):
nanos = int(dt.astype('timedelta64[ns]').astype(numpy.int64))
Expand Down Expand Up @@ -559,9 +560,11 @@ def to_date(dt: Union[None, LocalDate, ZonedDateTime]) -> Optional[datetime.date
if dt is None:
return None
if isinstance(dt, LocalDate.j_type):
return datetime.date(dt.getYear(), dt.getMonthValue(), dt.getDayOfMonth())
year, month_value, day_of_month = _JPythonTimeComponents.getLocalDateComponents(dt)
return datetime.date(year, month_value, day_of_month)
if isinstance(dt, ZonedDateTime.j_type):
return datetime.date(dt.getYear(), dt.getMonthValue(), dt.getDayOfMonth())
year, month_value, day_of_month = _JPythonTimeComponents.getLocalDateComponents(dt)
return datetime.date(year, month_value, day_of_month)
else:
raise TypeError("Unsupported conversion: " + str(type(dt)) + " -> datetime.date")
except TypeError as e:
Expand All @@ -588,9 +591,11 @@ def to_time(dt: Union[None, LocalTime, ZonedDateTime]) -> Optional[datetime.time
if dt is None:
return None
elif isinstance(dt, LocalTime.j_type):
return datetime.time(dt.getHour(), dt.getMinute(), dt.getSecond(), dt.getNano() // _nanos_per_micro)
hour, minute, second, nano = _JPythonTimeComponents.getLocalTimeComponents(dt)
return datetime.time(hour, minute, second, nano // _NANOS_PER_MICRO)
elif isinstance(dt, ZonedDateTime.j_type):
return datetime.time(dt.getHour(), dt.getMinute(), dt.getSecond(), dt.getNano() // _nanos_per_micro)
hour, minute, second, nano = _JPythonTimeComponents.getLocalTimeComponents(dt)
return datetime.time(hour, minute, second, nano // _NANOS_PER_MICRO)
else:
raise TypeError("Unsupported conversion: " + str(type(dt)) + " -> datetime.time")
except TypeError as e:
Expand All @@ -617,10 +622,12 @@ def to_datetime(dt: Union[None, Instant, ZonedDateTime]) -> Optional[datetime.da
if dt is None:
return None
elif isinstance(dt, Instant.j_type):
ts = dt.getEpochSecond() + (dt.getNano() / _nanos_per_second)
epoch_second, nano = _JPythonTimeComponents.getInstantComponents(dt)
ts = epoch_second + (nano / _NANOS_PER_SECOND)
return datetime.datetime.fromtimestamp(ts)
elif isinstance(dt, ZonedDateTime.j_type):
ts = dt.toEpochSecond() + (dt.getNano() / _nanos_per_second)
epoch_second, nano = _JPythonTimeComponents.getInstantComponents(dt)
ts = epoch_second + (nano / _NANOS_PER_SECOND)
return datetime.datetime.fromtimestamp(ts)
else:
raise TypeError("Unsupported conversion: " + str(type(dt)) + " -> datetime.datetime")
Expand Down Expand Up @@ -676,10 +683,12 @@ def to_np_datetime64(dt: Union[None, Instant, ZonedDateTime]) -> Optional[numpy.
if dt is None:
return None
elif isinstance(dt, Instant.j_type):
ts = dt.getEpochSecond() * _nanos_per_second + dt.getNano()
epoch_second, nano = _JPythonTimeComponents.getInstantComponents(dt)
ts = epoch_second * _NANOS_PER_SECOND + nano
return numpy.datetime64(ts, 'ns')
elif isinstance(dt, ZonedDateTime.j_type):
ts = dt.toEpochSecond() * _nanos_per_second + dt.getNano()
epoch_second, nano = _JPythonTimeComponents.getInstantComponents(dt)
ts = epoch_second * _NANOS_PER_SECOND + nano
return numpy.datetime64(ts, 'ns')
else:
raise TypeError("Unsupported conversion: " + str(type(dt)) + " -> datetime.datetime")
Expand All @@ -706,11 +715,10 @@ def to_timedelta(dt: Union[None, Duration]) -> Optional[datetime.timedelta]:
if dt is None:
return None
elif isinstance(dt, Duration.j_type):
return datetime.timedelta(seconds=dt.getSeconds(), microseconds=dt.getNano() // _nanos_per_micro)
seconds, nano = _JPythonTimeComponents.getDurationComponents(dt)
return datetime.timedelta(seconds=seconds, microseconds=nano // _NANOS_PER_MICRO)
elif isinstance(dt, Period.j_type):
y = dt.getYears()
m = dt.getMonths()
d = dt.getDays()
y, m, d = _JPythonTimeComponents.getPeriodComponents(dt)

if y or m:
raise ValueError("Unsupported conversion: " + str(type(dt)) +
Expand Down Expand Up @@ -744,12 +752,11 @@ def to_pd_timedelta(dt: Union[None, Duration]) -> Optional[pandas.Timedelta]:
if dt is None:
return None
elif isinstance(dt, Duration.j_type):
micros, nanos = divmod(dt.getNano(), _nanos_per_micro)
return pandas.Timedelta(seconds=dt.getSeconds(), microseconds=micros, nanoseconds=nanos)
seconds, nano = _JPythonTimeComponents.getDurationComponents(dt)
micros, nanos = divmod(nano, _NANOS_PER_MICRO)
return pandas.Timedelta(seconds=seconds, microseconds=micros, nanoseconds=nanos)
elif isinstance(dt, Period.j_type):
y = dt.getYears()
m = dt.getMonths()
d = dt.getDays()
y, m, d = _JPythonTimeComponents.getPeriodComponents(dt)

if y or m:
raise ValueError("Unsupported conversion: " + str(type(dt)) +
Expand Down Expand Up @@ -785,9 +792,7 @@ def to_np_timedelta64(dt: Union[None, Duration, Period]) -> Optional[numpy.timed
elif isinstance(dt, Duration.j_type):
return numpy.timedelta64(dt.toNanos(), 'ns')
elif isinstance(dt, Period.j_type):
d = dt.getDays()
m = dt.getMonths()
y = dt.getYears()
y, m, d = _JPythonTimeComponents.getPeriodComponents(dt)

count = (1 if d else 0) + (1 if m else 0) + (1 if y else 0)

Expand Down
Loading