Skip to content

Commit

Permalink
Convert datetime in Lua within OffsetDateTime in Java
Browse files Browse the repository at this point in the history
Since `datetime` in Lua supports working with offsets, it is logical to allow it to be converted to `OffsetDateTime` in Java to expand the possibilities of working with dates
  • Loading branch information
valery1707 committed Oct 27, 2023
1 parent a86d4e8 commit cb4d1b9
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 3 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@
<version>1.35</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import java.nio.ByteOrder;
import java.time.Instant;

import static io.tarantool.driver.mappers.converters.value.defaults.DefaultExtensionValueToInstantConverter.DATETIME_TYPE;

/**
* Default {@link java.time.Instant} to {@link ExtensionValue} converter
*
Expand All @@ -21,8 +23,6 @@ public class DefaultInstantToExtensionValueConverter implements ObjectConverter<

private static final long serialVersionUID = 20221025L;

private static final byte DATETIME_TYPE = 0x04;

private byte[] toBytes(Instant value) {
long seconds = value.getEpochSecond();
Integer nano = value.getNano();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.tarantool.driver.mappers.converters.object;

import io.tarantool.driver.mappers.converters.ObjectConverter;
import org.msgpack.value.ExtensionValue;
import org.msgpack.value.ValueFactory;

import java.nio.ByteBuffer;
import java.time.OffsetDateTime;

import static io.tarantool.driver.mappers.converters.value.defaults.DefaultExtensionValueToInstantConverter.DATETIME_TYPE;
import static io.tarantool.driver.mappers.converters.value.defaults.DefaultExtensionValueToOffsetDateTimeConverter.SECONDS_PER_MINUTE;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.time.ZoneOffset.UTC;

/**
* Default {@link ExtensionValue} to {@link java.time.OffsetDateTime} converter.
*
* @author Valeriy Vyrva
*/
public class DefaultOffsetDateTimeToExtensionValueConverter implements ObjectConverter<OffsetDateTime, ExtensionValue> {

private static final long serialVersionUID = 20231027114017L;

/**
* Will contain only requited part:
* <ol>
* <li>{@code 8 bytes}: Seconds since Epoch.</li>
* </ol>
*
* @see <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/datetime.h#L85">struct datetime</a>
*/
private static final int BUFFER_SIZE_COMPACT = Long.BYTES;
/**
* Will contain and required and optional parts:
* <ol>
* <li>{@code 8 bytes}: Seconds since Epoch.</li>
* <li>{@code 4 bytes}: Nanoseconds.</li>
* <li>{@code 2 bytes}: Offset in minutes from UTC.</li>
* <li>{@code 2 bytes}: Olson timezone id.</li>
* </ol>
* The "timezone id" is not used on Java.
*
* @see <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/datetime.h#L85">struct datetime</a>
*/
private static final int BUFFER_SIZE_COMPLETE = Long.BYTES + Integer.BYTES + Short.BYTES + Short.BYTES;

@Override
public ExtensionValue toValue(OffsetDateTime object) {
return ValueFactory.newExtension(DATETIME_TYPE, toBytes(object));
}

/**
* Encode java object into protocol level representation.
*
* @param object Object to encode
* @return Protocol level representation
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/mp_datetime.c#L18">
* serialization schema</a>
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/datetime.h#L85">struct datetime</a>
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/mp_datetime.c#L107">datetime_pack</a>
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/mp_datetime.c#L56">datetime_unpack</a>
*/
private byte[] toBytes(OffsetDateTime object) {
boolean isCompact = object.getNano() == 0 && object.getOffset().equals(UTC);
ByteBuffer buffer = ByteBuffer.wrap(new byte[isCompact ? BUFFER_SIZE_COMPACT : BUFFER_SIZE_COMPLETE]);
buffer.order(LITTLE_ENDIAN);
//Required part
buffer.putLong(object.toEpochSecond());
//Optional part
if (!isCompact) {
buffer.putInt(object.getNano());
buffer.putShort((short) (object.getOffset().getTotalSeconds() / SECONDS_PER_MINUTE));
}
return buffer.array();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
public class DefaultExtensionValueToInstantConverter implements ValueConverter<ExtensionValue, Instant> {

private static final long serialVersionUID = 20221025L;
private static final byte DATETIME_TYPE = 0x04;
/**
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/mp_extension_types.h#L47">
* mp_extension_type#MP_DATETIME</a>
*/
public static final byte DATETIME_TYPE = 0x04;

private Instant fromBytes(byte[] bytes) {
int size = bytes.length;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.tarantool.driver.mappers.converters.value.defaults;

import io.tarantool.driver.mappers.converters.ValueConverter;
import org.msgpack.value.ExtensionValue;

import java.nio.ByteBuffer;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

import static io.tarantool.driver.mappers.converters.value.defaults.DefaultExtensionValueToInstantConverter.DATETIME_TYPE;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.time.ZoneOffset.UTC;

/**
* Default {@link ExtensionValue} to {@link java.time.OffsetDateTime} converter.
*
* @author Valeriy Vyrva
*/
public class DefaultExtensionValueToOffsetDateTimeConverter implements ValueConverter<ExtensionValue, OffsetDateTime> {

private static final long serialVersionUID = 20231027114017L;

public static final int SECONDS_PER_MINUTE = 60;

@Override
public boolean canConvertValue(ExtensionValue value) {
return value.getType() == DATETIME_TYPE;
}

@Override
public OffsetDateTime fromValue(ExtensionValue value) {
return fromBytes(value.getData());
}

/**
* Decode protocol level representation into java object.
*
* @param value Bytes from protocol level
* @return Decoded value
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/mp_datetime.c#L18">
* serialization schema</a>
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/datetime.h#L85">struct datetime</a>
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/mp_datetime.c#L107">datetime_pack</a>
* @see
* <a href="https://github.com/tarantool/tarantool/blob/master/src/lib/core/mp_datetime.c#L56">datetime_unpack</a>
*/
private OffsetDateTime fromBytes(byte[] value) {
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(LITTLE_ENDIAN);
return Instant
//Required part
.ofEpochSecond(buffer.getLong())
//Optional part
.plusNanos(buffer.hasRemaining() ? buffer.getInt() : 0)
.atOffset(buffer.hasRemaining() ? ZoneOffset.ofTotalSeconds(buffer.getShort() * SECONDS_PER_MINUTE) : UTC);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.tarantool.driver.mappers.converters.object.DefaultLongArrayToArrayValueConverter;
import io.tarantool.driver.mappers.converters.object.DefaultLongToIntegerValueConverter;
import io.tarantool.driver.mappers.converters.object.DefaultNilValueToNullConverter;
import io.tarantool.driver.mappers.converters.object.DefaultOffsetDateTimeToExtensionValueConverter;
import io.tarantool.driver.mappers.converters.object.DefaultPackableObjectConverter;
import io.tarantool.driver.mappers.converters.object.DefaultShortToIntegerValueConverter;
import io.tarantool.driver.mappers.converters.object.DefaultStringToStringValueConverter;
Expand All @@ -21,6 +22,7 @@
import io.tarantool.driver.mappers.converters.value.defaults.DefaultBinaryValueToByteArrayConverter;
import io.tarantool.driver.mappers.converters.value.defaults.DefaultBooleanValueToBooleanConverter;
import io.tarantool.driver.mappers.converters.value.defaults.DefaultExtensionValueToBigDecimalConverter;
import io.tarantool.driver.mappers.converters.value.defaults.DefaultExtensionValueToOffsetDateTimeConverter;
import io.tarantool.driver.mappers.converters.value.defaults.DefaultExtensionValueToUUIDConverter;
import io.tarantool.driver.mappers.converters.value.defaults.DefaultFloatValueToDoubleConverter;
import io.tarantool.driver.mappers.converters.value.defaults.DefaultFloatValueToFloatConverter;
Expand All @@ -46,6 +48,7 @@

import java.math.BigDecimal;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.UUID;

/**
Expand Down Expand Up @@ -86,6 +89,8 @@ private DefaultMessagePackMapperFactory() {
.withValueConverter(ValueType.EXTENSION, BigDecimal.class,
new DefaultExtensionValueToBigDecimalConverter())
.withValueConverter(ValueType.EXTENSION, Instant.class, new DefaultExtensionValueToInstantConverter())
.withValueConverter(ValueType.EXTENSION, OffsetDateTime.class,
new DefaultExtensionValueToOffsetDateTimeConverter())
.withValueConverter(ValueType.NIL, Object.class, new DefaultNilValueToNullConverter())
//TODO: Potential issue https://github.com/tarantool/cartridge-java/issues/118
.withObjectConverter(Character.class, StringValue.class, new DefaultCharacterToStringValueConverter())
Expand All @@ -102,6 +107,8 @@ private DefaultMessagePackMapperFactory() {
.withObjectConverter(BigDecimal.class, ExtensionValue.class,
new DefaultBigDecimalToExtensionValueConverter())
.withObjectConverter(Instant.class, ExtensionValue.class, new DefaultInstantToExtensionValueConverter())
.withObjectConverter(OffsetDateTime.class, ExtensionValue.class,
new DefaultOffsetDateTimeToExtensionValueConverter())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.UUID;

import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -110,4 +114,94 @@ public void test_boxOperations_shouldWorkWithVarbinary() throws Exception {
List<Byte> byteListFromTarantool = Utils.convertBytesToByteList(bytesFromTarantool);
Assertions.assertEquals(byteList, byteListFromTarantool);
}

@ParameterizedTest(name = "[{index}] {0}")
@CsvSource(delimiter = '|', value = {
"Construct 'compact' value (zero nanoseconds, offset and timezone)" +
" | new({year = 2023, month = 10, day = 25, hour = 13, min = 55, sec = 17})" +
" | 2023-10-25T13:55:17Z",
"Construct 'complete' (has nanos) value" +
" | new({year = 2023, month = 10, day = 25, hour = 13, min = 55, sec = 17, usec = 71983})" +
" | 2023-10-25T13:55:17.071983Z",
"Construct 'complete' (has nanos and positive offset) value" +
" | new({year = 2023, month = 10, day = 25, hour = 13, min = 55, sec = 17, usec = 71983, tzoffset = 0+180})" +
" | 2023-10-25T13:55:17.071983+03:00",
"Construct 'complete' (has nanos and negative offset) value" +
" | new({year = 2023, month = 10, day = 25, hour = 13, min = 55, sec = 17, usec = 71983, tzoffset = 0-180})" +
" | 2023-10-25T13:55:17.071983-03:00",
"Construct 'complete' (has nanos and timezone) value" +
" | new({year = 2023, month = 10, day = 25, hour = 13, min = 55, sec = 17, usec = 71983, tz = " +
"'Europe/Isle_of_Man'})" +
" | 2023-10-25T13:55:17.071983+01:00",
"Parse with default format" +
" | parse('1970-01-01T00:00:00Z')" +
" | 1970-01-01T00:00:00Z",
"Parse with ISO8601 format and offset" +
" | parse('1970-01-01T00:00:00', {format = 'iso8601', tzoffset = 180})" +
" | 1970-01-01T00:00:00+03:00",
"Parse with RFC3339 format" +
" | parse('2017-12-27T18:45:32.999999-05:00', {format = 'rfc3339'})" +
" | 2017-12-27T18:45:32.999999-05:00",
})
@EnabledIf("io.tarantool.driver.TarantoolUtils#versionWithInstant")
public void test_eval_shouldReturnOffsetDateTime(
String description, String expression, OffsetDateTime expected
) throws Exception {
List<?> result = client
.eval("return require('datetime')." + expression)
.get();

Assertions.assertEquals(expected, result.get(0), description);
}

@ParameterizedTest(name = "[{index}] {0}")
@CsvSource(delimiter = '|', value = {
"Same 'compact' value" +
" | ''" +
" | 2023-10-25T13:55:17Z" +
" | 2023-10-25T13:55:17Z",
"Same 'complete' value" +
" | ''" +
" | 2023-10-25T13:55:17.071983+03:00" +
" | 2023-10-25T13:55:17.071983+03:00",
"Subtract day from 'complete' value" +
" | :sub({day = 1})" +
" | 2023-10-25T13:55:17.071983+03:00" +
" | 2023-10-24T13:55:17.071983+03:00",
"Clear timezone from 'complete' value" +
" | :set({tz = 'UTC'})" +
" | 2023-10-25T13:55:17.071983+03:00" +
" | 2023-10-25T13:55:17.071983Z",
"Clear nanoseconds from 'complete' value" +
" | :set({nsec = 0})" +
" | 2023-10-25T13:55:17.071983+03:00" +
" | 2023-10-25T13:55:17+03:00",
"Clear nanoseconds and timezone from 'complete' value" +
" | :set({nsec = 0, tz = 'UTC'})" +
" | 2023-10-25T13:55:17.071983+03:00" +
" | 2023-10-25T13:55:17Z",
"Add nanoseconds into 'compact' value" +
" | :add({usec = 100500})" +
" | 2023-10-25T13:55:17Z" +
" | 2023-10-25T13:55:17.100500Z",
"Add nanoseconds into 'complete' (has offset) value" +
" | :add({usec = 100500})" +
" | 2023-10-25T13:55:17+03:00" +
" | 2023-10-25T13:55:17.100500+03:00",
"Add nanoseconds into 'complete' (has nanos) value" +
" | :add({usec = 100500})" +
" | 2023-10-25T13:55:17.023067+03:00" +
" | 2023-10-25T13:55:17.123567+03:00",
})
@EnabledIf("io.tarantool.driver.TarantoolUtils#versionWithInstant")
public void test_eval_shouldHandleOffsetDateTime(
String description, String expression, OffsetDateTime original, OffsetDateTime expected
) throws Exception {
List<?> result = client
.eval("args = {...}; return args[1]" + expression, Collections.singleton(original))
.get();

Assertions.assertEquals(expected, result.get(0), description);
}

}
Loading

0 comments on commit cb4d1b9

Please sign in to comment.