|
6 | 6 | import io.github.dfa1.vortex.core.VortexException; |
7 | 7 | import io.github.dfa1.vortex.core.array.Array; |
8 | 8 | import io.github.dfa1.vortex.core.array.GenericArray; |
| 9 | +import io.github.dfa1.vortex.proto.DTypeProtos; |
9 | 10 | import io.github.dfa1.vortex.proto.EncodingProtos; |
10 | 11 |
|
11 | 12 | import java.lang.foreign.MemorySegment; |
12 | 13 | import java.nio.ByteBuffer; |
| 14 | +import java.util.ArrayList; |
| 15 | +import java.util.List; |
13 | 16 |
|
14 | | -/// Decoder for {@code vortex.datetimeparts} — timestamp split into days/seconds/subseconds. |
| 17 | +/// Encoder/decoder for {@code vortex.datetimeparts} — timestamp split into days/seconds/subseconds. |
15 | 18 | /// |
16 | 19 | /// <p>Wire format (per Rust vtable): |
17 | 20 | /// <ul> |
|
25 | 28 | /// <li>Slot 2 — {@code subseconds}: {@code Primitive(subseconds_ptype, false)} |
26 | 29 | /// </ul> |
27 | 30 | /// </ul> |
| 31 | +/// |
| 32 | +/// <p>Extension metadata for {@code vortex.timestamp} dtype (hand-rolled, not protobuf): |
| 33 | +/// {@code byte[0]=TimeUnit tag, bytes[1-2]=tz_len (u16 LE), bytes[3+]=tz UTF-8} |
28 | 34 | public final class DateTimePartsEncoding implements Encoding { |
29 | 35 |
|
30 | | - @Override |
31 | | - public EncodingId encodingId() { |
32 | | - return EncodingId.VORTEX_DATETIMEPARTS; |
33 | | - } |
34 | | - |
35 | | - @Override |
36 | | - public boolean accepts(DType dtype) { |
37 | | - return dtype instanceof DType.Extension; |
38 | | - } |
39 | | - |
40 | | - @Override |
41 | | - public EncodeResult encode(DType dtype, Object data) { |
42 | | - throw new UnsupportedOperationException("encode not yet implemented for " + encodingId()); |
43 | | - } |
44 | | - |
45 | | - @Override |
46 | | - public Array decode(DecodeContext ctx) { |
47 | | - return Decoder.decode(ctx); |
48 | | - } |
49 | | - |
50 | | - private static final class Decoder { |
51 | | - |
52 | | - private static Array decode(DecodeContext ctx) { |
53 | | - ByteBuffer meta = ctx.metadata(); |
54 | | - if (meta == null || meta.remaining() == 0) { |
55 | | - throw new VortexException(EncodingId.VORTEX_DATETIMEPARTS, "missing metadata"); |
56 | | - } |
57 | | - EncodingProtos.DateTimePartsMetadata decoded; |
58 | | - try { |
59 | | - byte[] bytes = new byte[meta.remaining()]; |
60 | | - meta.duplicate().get(bytes); |
61 | | - decoded = EncodingProtos.DateTimePartsMetadata.parseFrom(bytes); |
62 | | - } catch (InvalidProtocolBufferException e) { |
63 | | - throw new VortexException(EncodingId.VORTEX_DATETIMEPARTS, "invalid metadata: " + e.getMessage()); |
64 | | - } |
65 | | - |
66 | | - PType daysPtype = PType.values()[decoded.getDaysPtypeValue()]; |
67 | | - PType secondsPtype = PType.values()[decoded.getSecondsPtypeValue()]; |
68 | | - PType subsecondsPtype = PType.values()[decoded.getSubsecondsPtypeValue()]; |
69 | | - boolean nullable = ctx.dtype().nullable(); |
70 | | - |
71 | | - Array days = decodeChild(ctx, 0, new DType.Primitive(daysPtype, nullable)); |
72 | | - Array seconds = decodeChild(ctx, 1, new DType.Primitive(secondsPtype, false)); |
73 | | - Array subseconds = decodeChild(ctx, 2, new DType.Primitive(subsecondsPtype, false)); |
74 | | - |
75 | | - return new GenericArray(ctx.dtype(), ctx.rowCount(), new MemorySegment[0], |
76 | | - new Array[]{days, seconds, subseconds}); |
77 | | - } |
78 | | - |
79 | | - private static Array decodeChild(DecodeContext ctx, int idx, DType childDtype) { |
80 | | - ArrayNode childNode = ctx.node().children()[idx]; |
81 | | - DecodeContext childCtx = new DecodeContext( |
82 | | - childNode, childDtype, ctx.rowCount(), |
83 | | - ctx.segmentBuffers(), ctx.registry(), ctx.arena()); |
84 | | - return ctx.registry().decode(childCtx); |
85 | | - } |
86 | | - } |
| 36 | + @Override |
| 37 | + public EncodingId encodingId() { |
| 38 | + return EncodingId.VORTEX_DATETIMEPARTS; |
| 39 | + } |
| 40 | + |
| 41 | + @Override |
| 42 | + public boolean accepts(DType dtype) { |
| 43 | + return dtype instanceof DType.Extension; |
| 44 | + } |
| 45 | + |
| 46 | + @Override |
| 47 | + public EncodeResult encode(DType dtype, Object data) { |
| 48 | + return Encoder.encode((DType.Extension) dtype, (DateTimePartsData) data); |
| 49 | + } |
| 50 | + |
| 51 | + @Override |
| 52 | + public Array decode(DecodeContext ctx) { |
| 53 | + return Decoder.decode(ctx); |
| 54 | + } |
| 55 | + |
| 56 | + private static final class Encoder { |
| 57 | + |
| 58 | + private static final long SECONDS_PER_DAY = 86_400L; |
| 59 | + private static final DType I64 = new DType.Primitive(PType.I64, false); |
| 60 | + private static final DType I64_NULLABLE = new DType.Primitive(PType.I64, true); |
| 61 | + private static final DTypeProtos.PType I64_PROTO = |
| 62 | + DTypeProtos.PType.forNumber(PType.I64.ordinal()); |
| 63 | + |
| 64 | + static EncodeResult encode(DType.Extension dtype, DateTimePartsData data) { |
| 65 | + ByteBuffer extMeta = dtype.metadata(); |
| 66 | + if (extMeta == null || extMeta.remaining() < 3) { |
| 67 | + throw new VortexException(EncodingId.VORTEX_DATETIMEPARTS, |
| 68 | + "extension metadata missing or too short"); |
| 69 | + } |
| 70 | + byte[] extBytes = new byte[extMeta.remaining()]; |
| 71 | + extMeta.duplicate().get(extBytes); |
| 72 | + TimeUnit unit = TimeUnit.fromTag(extBytes[0]); |
| 73 | + |
| 74 | + long divisor = unit.divisor(); |
| 75 | + long ticksPerDay = SECONDS_PER_DAY * divisor; |
| 76 | + int n = data.timestamps().length; |
| 77 | + |
| 78 | + long[] days = new long[n]; |
| 79 | + long[] seconds = new long[n]; |
| 80 | + long[] subseconds = new long[n]; |
| 81 | + |
| 82 | + for (int i = 0; i < n; i++) { |
| 83 | + long ts = data.timestamps()[i]; |
| 84 | + long d = ts / ticksPerDay; |
| 85 | + long rem = ts % ticksPerDay; |
| 86 | + if (rem < 0) { |
| 87 | + rem += ticksPerDay; |
| 88 | + d--; |
| 89 | + } |
| 90 | + days[i] = d; |
| 91 | + seconds[i] = rem / divisor; |
| 92 | + subseconds[i] = rem % divisor; |
| 93 | + } |
| 94 | + |
| 95 | + PrimitiveEncoding primEnc = new PrimitiveEncoding(); |
| 96 | + DType daysDtype = data.nullable() ? I64_NULLABLE : I64; |
| 97 | + |
| 98 | + EncodeResult daysResult = primEnc.encode(daysDtype, days); |
| 99 | + EncodeResult secondsResult = primEnc.encode(I64, seconds); |
| 100 | + EncodeResult subsecondsResult = primEnc.encode(I64, subseconds); |
| 101 | + |
| 102 | + List<MemorySegment> allBuffers = new ArrayList<>(); |
| 103 | + allBuffers.addAll(daysResult.buffers()); |
| 104 | + allBuffers.addAll(secondsResult.buffers()); |
| 105 | + allBuffers.addAll(subsecondsResult.buffers()); |
| 106 | + |
| 107 | + int off1 = daysResult.buffers().size(); |
| 108 | + int off2 = off1 + secondsResult.buffers().size(); |
| 109 | + |
| 110 | + EncodeNode daysNode = EncodeNode.remapBufferIndices(daysResult.rootNode(), 0); |
| 111 | + EncodeNode secondsNode = EncodeNode.remapBufferIndices(secondsResult.rootNode(), off1); |
| 112 | + EncodeNode subsecondsNode = EncodeNode.remapBufferIndices(subsecondsResult.rootNode(), off2); |
| 113 | + |
| 114 | + byte[] metaBytes = EncodingProtos.DateTimePartsMetadata.newBuilder() |
| 115 | + .setDaysPtype(I64_PROTO) |
| 116 | + .setSecondsPtype(I64_PROTO) |
| 117 | + .setSubsecondsPtype(I64_PROTO) |
| 118 | + .build() |
| 119 | + .toByteArray(); |
| 120 | + |
| 121 | + EncodeNode root = new EncodeNode( |
| 122 | + EncodingId.VORTEX_DATETIMEPARTS, |
| 123 | + ByteBuffer.wrap(metaBytes), |
| 124 | + new EncodeNode[]{daysNode, secondsNode, subsecondsNode}, |
| 125 | + new int[]{}); |
| 126 | + return new EncodeResult(root, List.copyOf(allBuffers), null, null); |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + private static final class Decoder { |
| 131 | + |
| 132 | + private static Array decode(DecodeContext ctx) { |
| 133 | + ByteBuffer meta = ctx.metadata(); |
| 134 | + if (meta == null || meta.remaining() == 0) { |
| 135 | + throw new VortexException(EncodingId.VORTEX_DATETIMEPARTS, "missing metadata"); |
| 136 | + } |
| 137 | + EncodingProtos.DateTimePartsMetadata decoded; |
| 138 | + try { |
| 139 | + byte[] bytes = new byte[meta.remaining()]; |
| 140 | + meta.duplicate().get(bytes); |
| 141 | + decoded = EncodingProtos.DateTimePartsMetadata.parseFrom(bytes); |
| 142 | + } catch (InvalidProtocolBufferException e) { |
| 143 | + throw new VortexException(EncodingId.VORTEX_DATETIMEPARTS, "invalid metadata: " + e.getMessage()); |
| 144 | + } |
| 145 | + |
| 146 | + PType daysPtype = PType.values()[decoded.getDaysPtypeValue()]; |
| 147 | + PType secondsPtype = PType.values()[decoded.getSecondsPtypeValue()]; |
| 148 | + PType subsecondsPtype = PType.values()[decoded.getSubsecondsPtypeValue()]; |
| 149 | + boolean nullable = ctx.dtype().nullable(); |
| 150 | + |
| 151 | + Array days = decodeChild(ctx, 0, new DType.Primitive(daysPtype, nullable)); |
| 152 | + Array seconds = decodeChild(ctx, 1, new DType.Primitive(secondsPtype, false)); |
| 153 | + Array subseconds = decodeChild(ctx, 2, new DType.Primitive(subsecondsPtype, false)); |
| 154 | + |
| 155 | + return new GenericArray(ctx.dtype(), ctx.rowCount(), new MemorySegment[0], |
| 156 | + new Array[]{days, seconds, subseconds}); |
| 157 | + } |
| 158 | + |
| 159 | + private static Array decodeChild(DecodeContext ctx, int idx, DType childDtype) { |
| 160 | + ArrayNode childNode = ctx.node().children()[idx]; |
| 161 | + DecodeContext childCtx = new DecodeContext( |
| 162 | + childNode, childDtype, ctx.rowCount(), |
| 163 | + ctx.segmentBuffers(), ctx.registry(), ctx.arena()); |
| 164 | + return ctx.registry().decode(childCtx); |
| 165 | + } |
| 166 | + } |
87 | 167 | } |
0 commit comments