4949import java .util .Optional ;
5050
5151/**
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 .
5353 * <p>
5454 * The seconds formatter is provided by {@link #SECONDS_FORMATTER}.
5555 * The milliseconds formatter is provided by {@link #MILLIS_FORMATTER}.
56+ * The microseconds formatter is provided by {@link #MICROS_FORMATTER}.
5657 * <p>
57- * Both formatters support fractional time, up to nanosecond precision.
58+ * All formatters support fractional time, up to nanosecond precision.
5859 *
5960 * @opensearch.internal
6061 */
@@ -116,44 +117,62 @@ public long getFrom(TemporalAccessor temporal) {
116117 }
117118 };
118119
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+
121140 @ Override
122141 public boolean isSupportedBy (TemporalAccessor temporal ) {
123142 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 ));
125144 }
126145
127146 @ Override
128147 public long getFrom (TemporalAccessor temporal ) {
129148 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 ) {
131150 // Multiplying would yield integer overflow
132151 return Long .MAX_VALUE ;
133152 }
134- long instantSecondsInMillis = instantSeconds * 1_000 ;
135- if (instantSecondsInMillis >= 0 ) {
153+ long instantSecondsInUnits = instantSeconds * unitsPerSecond ;
154+ if (instantSecondsInUnits >= 0 ) {
136155 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 );
138157 } else {
139- return instantSecondsInMillis + temporal .getLong (ChronoField . MILLI_OF_SECOND );
158+ return instantSecondsInUnits + temporal .getLong (unitField );
140159 }
141160 } else { // negative timestamp
142161 if (temporal .isSupported (ChronoField .NANO_OF_SECOND )) {
143- long millis = instantSecondsInMillis ;
162+ long units = instantSecondsInUnits ;
144163 long nanos = temporal .getLong (ChronoField .NANO_OF_SECOND );
145- if (nanos % 1_000_000 != 0 ) {
164+ if (nanos % nanosPerUnit != 0 ) {
146165 // 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
148167 // 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 ;
151170 }
152- millis += (nanos / 1_000_000 );
153- return -millis ;
171+ units += (nanos / nanosPerUnit );
172+ return -units ;
154173 } else {
155- long millisOfSecond = temporal .getLong (ChronoField . MILLI_OF_SECOND );
156- return -(instantSecondsInMillis + millisOfSecond );
174+ long unitsOfSecond = temporal .getLong (unitField );
175+ return -(instantSecondsInUnits + unitsOfSecond );
157176 }
158177 }
159178 }
@@ -166,19 +185,19 @@ public TemporalAccessor resolve(
166185 ) {
167186 Long sign = Optional .ofNullable (fieldValues .remove (SIGN )).orElse (POSITIVE );
168187
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 );
171190
172191 long seconds ;
173192 long nanos ;
174193 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 ) {
180199 // aggregate fractional part of the input; subtract b/c `nanos < 0`
181- nanos -= nanosOfMilli ;
200+ nanos -= nanosOfUnit ;
182201 }
183202 if (nanos != 0 ) {
184203 // nanos must be positive. B/c the timestamp is represented by the
@@ -188,12 +207,12 @@ public TemporalAccessor resolve(
188207 nanos = 1_000_000_000 + nanos ;
189208 }
190209 } else {
191- seconds = secondsAndMillis / 1_000 ;
192- nanos = secondsAndMillis % 1000 * 1_000_000 ;
210+ seconds = secondsAndUnits / unitsPerSecond ;
211+ nanos = secondsAndUnits % unitsPerSecond * nanosPerUnit ;
193212
194- if (nanosOfMilli != null ) {
213+ if (nanosOfUnit != null ) {
195214 // aggregate fractional part of the input
196- nanos += nanosOfMilli ;
215+ nanos += nanosOfUnit ;
197216 }
198217 }
199218 fieldValues .put (ChronoField .INSTANT_SECONDS , seconds );
@@ -207,6 +226,24 @@ public TemporalAccessor resolve(
207226 }
208227 return null ;
209228 }
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+ }
210247 };
211248
212249 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) {
227264 }
228265 };
229266
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+
230284 // this supports seconds without any fraction
231285 private static final DateTimeFormatter SECONDS_FORMATTER1 = new DateTimeFormatterBuilder ().appendValue (SECONDS , 1 , 19 , SignStyle .NORMAL )
232286 .optionalStart () // optional is used so isSupported will be called when printing
@@ -261,6 +315,21 @@ public long getFrom(TemporalAccessor temporal) {
261315 .appendLiteral ('.' )
262316 .toFormatter (Locale .ROOT );
263317
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+
264333 static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter (
265334 "epoch_second" ,
266335 SECONDS_FORMATTER1 ,
@@ -277,6 +346,14 @@ public long getFrom(TemporalAccessor temporal) {
277346 MILLISECONDS_FORMATTER2
278347 );
279348
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+
280357 /**
281358 * Base class for an epoch field
282359 *
0 commit comments