Skip to content

Commit 3485432

Browse files
authored
Add epoch_micros date format (#19245)
Fixes #14669 Signed-off-by: Mikhail Stepura <[email protected]>
1 parent 1ca590a commit 3485432

File tree

6 files changed

+214
-32
lines changed

6 files changed

+214
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1818
- Add a dynamic cluster setting to control the enablement of the merged segment warmer ([#18929](https://github.com/opensearch-project/OpenSearch/pull/18929))
1919
- Publish transport-grpc-spi exposing QueryBuilderProtoConverter and QueryBuilderProtoConverterRegistry ([#18949](https://github.com/opensearch-project/OpenSearch/pull/18949))
2020
- Support system generated search pipeline. ([#19128](https://github.com/opensearch-project/OpenSearch/pull/19128))
21+
- Add `epoch_micros` date format ([#14669](https://github.com/opensearch-project/OpenSearch/issues/14669))
2122

2223
### Changed
2324
- Add CompletionStage variants to methods in the Client Interface and default to ActionListener impl ([#18998](https://github.com/opensearch-project/OpenSearch/pull/18998))

server/src/main/java/org/opensearch/common/time/DateFormatters.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,6 +2119,8 @@ static DateFormatter forPattern(String input) {
21192119
} else if (FormatNames.EPOCH_MILLIS.matches(input)) {
21202120
return EpochTime.MILLIS_FORMATTER;
21212121
// strict date formats here, must be at least 4 digits for year and two for months and two for day
2122+
} else if (FormatNames.EPOCH_MICROS.matches(input)) {
2123+
return EpochTime.MICROS_FORMATTER;
21222124
} else if (FormatNames.STRICT_BASIC_WEEK_DATE.matches(input)) {
21232125
return STRICT_BASIC_WEEK_DATE;
21242126
} else if (FormatNames.STRICT_BASIC_WEEK_DATE_TIME.matches(input)) {

server/src/main/java/org/opensearch/common/time/EpochTime.java

Lines changed: 108 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@
4949
import 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
*

server/src/main/java/org/opensearch/common/time/FormatNames.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public enum FormatNames {
9191
YEAR_MONTH_DAY("yearMonthDay", "year_month_day"),
9292
EPOCH_SECOND(null, "epoch_second"),
9393
EPOCH_MILLIS(null, "epoch_millis"),
94+
EPOCH_MICROS(null, "epoch_micros"),
9495
// strict date formats here, must be at least 4 digits for year and two for months and two for day"
9596
STRICT_BASIC_WEEK_DATE("strictBasicWeekDate", "strict_basic_week_date"),
9697
STRICT_BASIC_WEEK_DATE_TIME("strictBasicWeekDateTime", "strict_basic_week_date_time"),

server/src/test/java/org/opensearch/common/joda/JavaJodaTimeDuellingTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,8 @@ public void testSamePrinterOutput() {
782782
for (FormatNames format : FormatNames.values()) {
783783
if (format == FormatNames.ISO8601
784784
|| format == FormatNames.STRICT_DATE_OPTIONAL_TIME_NANOS
785-
|| format == FormatNames.RFC3339_LENIENT) {
785+
|| format == FormatNames.RFC3339_LENIENT
786+
|| format == FormatNames.EPOCH_MICROS) {
786787
// Nanos aren't supported by joda
787788
continue;
788789
}

0 commit comments

Comments
 (0)