49
49
import java .util .Optional ;
50
50
51
51
/**
52
- * This class provides {@link DateTimeFormatter}s capable of parsing epoch seconds and milliseconds .
52
+ * This class provides {@link DateTimeFormatter}s capable of parsing epoch seconds, milliseconds, and microseconds .
53
53
* <p>
54
54
* The seconds formatter is provided by {@link #SECONDS_FORMATTER}.
55
55
* The milliseconds formatter is provided by {@link #MILLIS_FORMATTER}.
56
+ * The microseconds formatter is provided by {@link #MICROS_FORMATTER}.
56
57
* <p>
57
- * Both formatters support fractional time, up to nanosecond precision.
58
+ * All formatters support fractional time, up to nanosecond precision.
58
59
*
59
60
* @opensearch.internal
60
61
*/
@@ -116,44 +117,62 @@ public long getFrom(TemporalAccessor temporal) {
116
117
}
117
118
};
118
119
119
- // Millis as absolute values. Negative millis are encoded by having a NEGATIVE SIGN.
120
- private static final EpochField MILLIS_ABS = new EpochField (ChronoUnit .MILLIS , ChronoUnit .FOREVER , LONG_POSITIVE_RANGE ) {
120
+ private static class AbsoluteEpochField extends EpochField {
121
+ private final long unitsPerSecond ;
122
+ private final long nanosPerUnit ;
123
+ private final ChronoField unitField ;
124
+ private final EpochField nanosOfUnitField ;
125
+
126
+ private AbsoluteEpochField (
127
+ TemporalUnit baseUnit ,
128
+ long unitsPerSecond ,
129
+ long nanosPerUnit ,
130
+ ChronoField unitField ,
131
+ EpochField nanosOfUnitField
132
+ ) {
133
+ super (baseUnit , ChronoUnit .FOREVER , LONG_POSITIVE_RANGE );
134
+ this .unitsPerSecond = unitsPerSecond ;
135
+ this .nanosPerUnit = nanosPerUnit ;
136
+ this .unitField = unitField ;
137
+ this .nanosOfUnitField = nanosOfUnitField ;
138
+ }
139
+
121
140
@ Override
122
141
public boolean isSupportedBy (TemporalAccessor temporal ) {
123
142
return temporal .isSupported (ChronoField .INSTANT_SECONDS )
124
- && (temporal .isSupported (ChronoField .NANO_OF_SECOND ) || temporal .isSupported (ChronoField . MILLI_OF_SECOND ));
143
+ && (temporal .isSupported (ChronoField .NANO_OF_SECOND ) || temporal .isSupported (unitField ));
125
144
}
126
145
127
146
@ Override
128
147
public long getFrom (TemporalAccessor temporal ) {
129
148
long instantSeconds = temporal .getLong (ChronoField .INSTANT_SECONDS );
130
- if (instantSeconds < Long .MIN_VALUE / 1000L || instantSeconds > Long .MAX_VALUE / 1000L ) {
149
+ if (instantSeconds < Long .MIN_VALUE / unitsPerSecond || instantSeconds > Long .MAX_VALUE / unitsPerSecond ) {
131
150
// Multiplying would yield integer overflow
132
151
return Long .MAX_VALUE ;
133
152
}
134
- long instantSecondsInMillis = instantSeconds * 1_000 ;
135
- if (instantSecondsInMillis >= 0 ) {
153
+ long instantSecondsInUnits = instantSeconds * unitsPerSecond ;
154
+ if (instantSecondsInUnits >= 0 ) {
136
155
if (temporal .isSupported (ChronoField .NANO_OF_SECOND )) {
137
- return instantSecondsInMillis + (temporal .getLong (ChronoField .NANO_OF_SECOND ) / 1_000_000 );
156
+ return instantSecondsInUnits + (temporal .getLong (ChronoField .NANO_OF_SECOND ) / nanosPerUnit );
138
157
} else {
139
- return instantSecondsInMillis + temporal .getLong (ChronoField . MILLI_OF_SECOND );
158
+ return instantSecondsInUnits + temporal .getLong (unitField );
140
159
}
141
160
} else { // negative timestamp
142
161
if (temporal .isSupported (ChronoField .NANO_OF_SECOND )) {
143
- long millis = instantSecondsInMillis ;
162
+ long units = instantSecondsInUnits ;
144
163
long nanos = temporal .getLong (ChronoField .NANO_OF_SECOND );
145
- if (nanos % 1_000_000 != 0 ) {
164
+ if (nanos % nanosPerUnit != 0 ) {
146
165
// Fractional negative timestamp.
147
- // Add 1 ms towards positive infinity because the fraction leads
166
+ // Add 1 unit towards positive infinity because the fraction leads
148
167
// the output's integral part to be an off-by-one when the
149
- // `(nanos / 1_000_000 )` is added below.
150
- millis += 1 ;
168
+ // `(nanos / nanosPerUnit )` is added below.
169
+ units += 1 ;
151
170
}
152
- millis += (nanos / 1_000_000 );
153
- return -millis ;
171
+ units += (nanos / nanosPerUnit );
172
+ return -units ;
154
173
} else {
155
- long millisOfSecond = temporal .getLong (ChronoField . MILLI_OF_SECOND );
156
- return -(instantSecondsInMillis + millisOfSecond );
174
+ long unitsOfSecond = temporal .getLong (unitField );
175
+ return -(instantSecondsInUnits + unitsOfSecond );
157
176
}
158
177
}
159
178
}
@@ -166,19 +185,19 @@ public TemporalAccessor resolve(
166
185
) {
167
186
Long sign = Optional .ofNullable (fieldValues .remove (SIGN )).orElse (POSITIVE );
168
187
169
- Long nanosOfMilli = fieldValues .remove (NANOS_OF_MILLI );
170
- long secondsAndMillis = fieldValues .remove (this );
188
+ Long nanosOfUnit = fieldValues .remove (nanosOfUnitField );
189
+ long secondsAndUnits = fieldValues .remove (this );
171
190
172
191
long seconds ;
173
192
long nanos ;
174
193
if (sign == NEGATIVE ) {
175
- secondsAndMillis = -secondsAndMillis ;
176
- seconds = secondsAndMillis / 1_000 ;
177
- nanos = secondsAndMillis % 1000 * 1_000_000 ;
178
- // `secondsAndMillis < 0` implies negative timestamp; so `nanos < 0`
179
- if (nanosOfMilli != null ) {
194
+ secondsAndUnits = -secondsAndUnits ;
195
+ seconds = secondsAndUnits / unitsPerSecond ;
196
+ nanos = secondsAndUnits % unitsPerSecond * nanosPerUnit ;
197
+ // `secondsAndUnits < 0` implies negative timestamp; so `nanos < 0`
198
+ if (nanosOfUnit != null ) {
180
199
// aggregate fractional part of the input; subtract b/c `nanos < 0`
181
- nanos -= nanosOfMilli ;
200
+ nanos -= nanosOfUnit ;
182
201
}
183
202
if (nanos != 0 ) {
184
203
// nanos must be positive. B/c the timestamp is represented by the
@@ -188,12 +207,12 @@ public TemporalAccessor resolve(
188
207
nanos = 1_000_000_000 + nanos ;
189
208
}
190
209
} else {
191
- seconds = secondsAndMillis / 1_000 ;
192
- nanos = secondsAndMillis % 1000 * 1_000_000 ;
210
+ seconds = secondsAndUnits / unitsPerSecond ;
211
+ nanos = secondsAndUnits % unitsPerSecond * nanosPerUnit ;
193
212
194
- if (nanosOfMilli != null ) {
213
+ if (nanosOfUnit != null ) {
195
214
// aggregate fractional part of the input
196
- nanos += nanosOfMilli ;
215
+ nanos += nanosOfUnit ;
197
216
}
198
217
}
199
218
fieldValues .put (ChronoField .INSTANT_SECONDS , seconds );
@@ -207,6 +226,24 @@ public TemporalAccessor resolve(
207
226
}
208
227
return null ;
209
228
}
229
+ }
230
+
231
+ private static final EpochField NANOS_OF_MICRO = new EpochField (ChronoUnit .NANOS , ChronoUnit .MICROS , ValueRange .of (0 , 999 )) {
232
+ @ Override
233
+ public boolean isSupportedBy (TemporalAccessor temporal ) {
234
+ return temporal .isSupported (ChronoField .INSTANT_SECONDS )
235
+ && temporal .isSupported (ChronoField .NANO_OF_SECOND )
236
+ && temporal .getLong (ChronoField .NANO_OF_SECOND ) % 1_000 != 0 ;
237
+ }
238
+
239
+ @ Override
240
+ public long getFrom (TemporalAccessor temporal ) {
241
+ if (temporal .getLong (ChronoField .INSTANT_SECONDS ) < 0 ) {
242
+ return (1_000_000_000 - temporal .getLong (ChronoField .NANO_OF_SECOND )) % 1_000 ;
243
+ } else {
244
+ return temporal .getLong (ChronoField .NANO_OF_SECOND ) % 1_000 ;
245
+ }
246
+ }
210
247
};
211
248
212
249
private static final EpochField NANOS_OF_MILLI = new EpochField (ChronoUnit .NANOS , ChronoUnit .MILLIS , ValueRange .of (0 , 999_999 )) {
@@ -227,6 +264,23 @@ public long getFrom(TemporalAccessor temporal) {
227
264
}
228
265
};
229
266
267
+ // Millis as absolute values. Negative millis are encoded by having a NEGATIVE SIGN.
268
+ private static final EpochField MILLIS_ABS = new AbsoluteEpochField (
269
+ ChronoUnit .MILLIS ,
270
+ 1_000L ,
271
+ 1_000_000L ,
272
+ ChronoField .MILLI_OF_SECOND ,
273
+ NANOS_OF_MILLI
274
+ );
275
+
276
+ private static final EpochField MICROS = new AbsoluteEpochField (
277
+ ChronoUnit .MICROS ,
278
+ 1_000_000L ,
279
+ 1_000L ,
280
+ ChronoField .MICRO_OF_SECOND ,
281
+ NANOS_OF_MICRO
282
+ );
283
+
230
284
// this supports seconds without any fraction
231
285
private static final DateTimeFormatter SECONDS_FORMATTER1 = new DateTimeFormatterBuilder ().appendValue (SECONDS , 1 , 19 , SignStyle .NORMAL )
232
286
.optionalStart () // optional is used so isSupported will be called when printing
@@ -261,6 +315,21 @@ public long getFrom(TemporalAccessor temporal) {
261
315
.appendLiteral ('.' )
262
316
.toFormatter (Locale .ROOT );
263
317
318
+ // this supports microseconds
319
+ private static final DateTimeFormatter MICROSECONDS_FORMATTER1 = new DateTimeFormatterBuilder ().optionalStart ()
320
+ .appendText (SIGN , SIGN_FORMATTER_LOOKUP ) // field is only created in the presence of a '-' char.
321
+ .optionalEnd ()
322
+ .appendValue (MICROS , 1 , 19 , SignStyle .NOT_NEGATIVE )
323
+ .optionalStart ()
324
+ .appendFraction (NANOS_OF_MICRO , 0 , 3 , true )
325
+ .optionalEnd ()
326
+ .toFormatter (Locale .ROOT );
327
+
328
+ // this supports microseconds ending in dot
329
+ private static final DateTimeFormatter MICROSECONDS_FORMATTER2 = new DateTimeFormatterBuilder ().append (MICROSECONDS_FORMATTER1 )
330
+ .appendLiteral ('.' )
331
+ .toFormatter (Locale .ROOT );
332
+
264
333
static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter (
265
334
"epoch_second" ,
266
335
SECONDS_FORMATTER1 ,
@@ -277,6 +346,14 @@ public long getFrom(TemporalAccessor temporal) {
277
346
MILLISECONDS_FORMATTER2
278
347
);
279
348
349
+ static final DateFormatter MICROS_FORMATTER = new JavaDateFormatter (
350
+ "epoch_micros" ,
351
+ MICROSECONDS_FORMATTER1 ,
352
+ (builder , parser ) -> builder .parseDefaulting (EpochTime .NANOS_OF_MICRO , 999L ),
353
+ MICROSECONDS_FORMATTER1 ,
354
+ MICROSECONDS_FORMATTER2
355
+ );
356
+
280
357
/**
281
358
* Base class for an epoch field
282
359
*
0 commit comments