Skip to content

Commit 1f4eefc

Browse files
committed
Create an ISO8601-specific parser for ISO datetimes
1 parent 12441f5 commit 1f4eefc

File tree

7 files changed

+1221
-94
lines changed

7 files changed

+1221
-94
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.common.time;
10+
11+
import java.util.stream.IntStream;
12+
13+
/**
14+
* A CharSequence that provides a subsequence of another CharSequence without allocating a new backing array (as String does)
15+
*/
16+
class CharSubSequence implements CharSequence {
17+
private final CharSequence wrapped;
18+
private final int startOffset; // inclusive
19+
private final int endOffset; // exclusive
20+
21+
CharSubSequence(CharSequence wrapped, int startOffset, int endOffset) {
22+
if (startOffset < 0) throw new IllegalArgumentException();
23+
if (endOffset > wrapped.length()) throw new IllegalArgumentException();
24+
if (endOffset < startOffset) throw new IllegalArgumentException();
25+
26+
this.wrapped = wrapped;
27+
this.startOffset = startOffset;
28+
this.endOffset = endOffset;
29+
}
30+
31+
@Override
32+
public int length() {
33+
return endOffset - startOffset;
34+
}
35+
36+
@Override
37+
public char charAt(int index) {
38+
int adjustedIndex = index + startOffset;
39+
if (adjustedIndex < startOffset || adjustedIndex >= endOffset) throw new IndexOutOfBoundsException(index);
40+
return wrapped.charAt(adjustedIndex);
41+
}
42+
43+
@Override
44+
public boolean isEmpty() {
45+
return startOffset == endOffset;
46+
}
47+
48+
@Override
49+
public CharSequence subSequence(int start, int end) {
50+
int adjustedStart = start + startOffset;
51+
int adjustedEnd = end + startOffset;
52+
if (adjustedStart < startOffset) throw new IndexOutOfBoundsException(start);
53+
if (adjustedEnd > endOffset) throw new IndexOutOfBoundsException(end);
54+
if (adjustedStart > adjustedEnd) throw new IndexOutOfBoundsException();
55+
56+
return wrapped.subSequence(adjustedStart, adjustedEnd);
57+
}
58+
59+
@Override
60+
public IntStream chars() {
61+
return wrapped.chars().skip(startOffset).limit(endOffset - startOffset);
62+
}
63+
64+
@Override
65+
public String toString() {
66+
return wrapped.subSequence(startOffset, endOffset).toString();
67+
}
68+
}

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

Lines changed: 10 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.time.temporal.TemporalQuery;
3131
import java.time.temporal.WeekFields;
3232
import java.util.Locale;
33+
import java.util.Set;
3334
import java.util.stream.Stream;
3435

3536
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
@@ -132,71 +133,15 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
132133
.toFormatter(Locale.ROOT)
133134
.withResolverStyle(ResolverStyle.STRICT);
134135

135-
private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER = new DateTimeFormatterBuilder().append(
136-
STRICT_YEAR_MONTH_DAY_FORMATTER
137-
)
138-
.optionalStart()
139-
.appendLiteral('T')
140-
.optionalStart()
141-
.appendValue(HOUR_OF_DAY, 2, 2, SignStyle.NOT_NEGATIVE)
142-
.optionalStart()
143-
.appendLiteral(':')
144-
.appendValue(MINUTE_OF_HOUR, 2, 2, SignStyle.NOT_NEGATIVE)
145-
.optionalStart()
146-
.appendLiteral(':')
147-
.appendValue(SECOND_OF_MINUTE, 2, 2, SignStyle.NOT_NEGATIVE)
148-
.optionalStart()
149-
.appendFraction(NANO_OF_SECOND, 1, 9, true)
150-
.optionalEnd()
151-
.optionalStart()
152-
.appendLiteral(',')
153-
.appendFraction(NANO_OF_SECOND, 1, 9, false)
154-
.optionalEnd()
155-
.optionalEnd()
156-
.optionalEnd()
157-
.optionalStart()
158-
.appendZoneOrOffsetId()
159-
.optionalEnd()
160-
.optionalStart()
161-
.append(TIME_ZONE_FORMATTER_NO_COLON)
162-
.optionalEnd()
163-
.optionalEnd()
164-
.optionalEnd()
165-
.toFormatter(Locale.ROOT)
166-
.withResolverStyle(ResolverStyle.STRICT);
167-
168136
/**
169137
* Returns a generic ISO datetime parser where the date is mandatory and the time is optional.
170138
*/
171-
private static final DateFormatter STRICT_DATE_OPTIONAL_TIME = newDateFormatter(
139+
private static final DateFormatter STRICT_DATE_OPTIONAL_TIME = new JavaDateFormatter(
172140
"strict_date_optional_time",
173-
STRICT_DATE_OPTIONAL_TIME_PRINTER,
174-
STRICT_DATE_OPTIONAL_TIME_FORMATTER
141+
new JavaTimeDateTimePrinter(STRICT_DATE_OPTIONAL_TIME_PRINTER),
142+
new Iso8601DateTimeParser(Set.of(), false).withLocale(Locale.ROOT)
175143
);
176144

177-
private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS = new DateTimeFormatterBuilder().append(
178-
STRICT_YEAR_MONTH_DAY_FORMATTER
179-
)
180-
.optionalStart()
181-
.appendLiteral('T')
182-
.append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
183-
.optionalStart()
184-
.appendFraction(NANO_OF_SECOND, 1, 9, true)
185-
.optionalEnd()
186-
.optionalStart()
187-
.appendLiteral(',')
188-
.appendFraction(NANO_OF_SECOND, 1, 9, false)
189-
.optionalEnd()
190-
.optionalStart()
191-
.appendZoneOrOffsetId()
192-
.optionalEnd()
193-
.optionalStart()
194-
.append(TIME_ZONE_FORMATTER_NO_COLON)
195-
.optionalEnd()
196-
.optionalEnd()
197-
.toFormatter(Locale.ROOT)
198-
.withResolverStyle(ResolverStyle.STRICT);
199-
200145
private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_PRINTER_NANOS = new DateTimeFormatterBuilder().append(
201146
STRICT_YEAR_MONTH_DAY_PRINTER
202147
)
@@ -224,50 +169,21 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
224169
/**
225170
* Returns a generic ISO datetime parser where the date is mandatory and the time is optional with nanosecond resolution.
226171
*/
227-
private static final DateFormatter STRICT_DATE_OPTIONAL_TIME_NANOS = newDateFormatter(
172+
private static final DateFormatter STRICT_DATE_OPTIONAL_TIME_NANOS = new JavaDateFormatter(
228173
"strict_date_optional_time_nanos",
229-
STRICT_DATE_OPTIONAL_TIME_PRINTER_NANOS,
230-
STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS
174+
new JavaTimeDateTimePrinter(STRICT_DATE_OPTIONAL_TIME_PRINTER_NANOS),
175+
new Iso8601DateTimeParser(Set.of(HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE), true).withLocale(Locale.ROOT)
231176
);
232177

233178
/**
234179
* Returns a ISO 8601 compatible date time formatter and parser.
235180
* This is not fully compatible to the existing spec, which would require far more edge cases, but merely compatible with the
236181
* existing legacy joda time ISO date formatter
237182
*/
238-
private static final DateFormatter ISO_8601 = newDateFormatter(
183+
private static final DateFormatter ISO_8601 = new JavaDateFormatter(
239184
"iso8601",
240-
STRICT_DATE_OPTIONAL_TIME_PRINTER,
241-
new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
242-
.optionalStart()
243-
.appendLiteral('T')
244-
.optionalStart()
245-
.appendValue(HOUR_OF_DAY, 2, 2, SignStyle.NOT_NEGATIVE)
246-
.optionalStart()
247-
.appendLiteral(':')
248-
.appendValue(MINUTE_OF_HOUR, 2, 2, SignStyle.NOT_NEGATIVE)
249-
.optionalStart()
250-
.appendLiteral(':')
251-
.appendValue(SECOND_OF_MINUTE, 2, 2, SignStyle.NOT_NEGATIVE)
252-
.optionalStart()
253-
.appendFraction(NANO_OF_SECOND, 1, 9, true)
254-
.optionalEnd()
255-
.optionalStart()
256-
.appendLiteral(",")
257-
.appendFraction(NANO_OF_SECOND, 1, 9, false)
258-
.optionalEnd()
259-
.optionalEnd()
260-
.optionalEnd()
261-
.optionalEnd()
262-
.optionalStart()
263-
.appendZoneOrOffsetId()
264-
.optionalEnd()
265-
.optionalStart()
266-
.append(TIME_ZONE_FORMATTER_NO_COLON)
267-
.optionalEnd()
268-
.optionalEnd()
269-
.toFormatter(Locale.ROOT)
270-
.withResolverStyle(ResolverStyle.STRICT)
185+
new JavaTimeDateTimePrinter(STRICT_DATE_OPTIONAL_TIME_PRINTER),
186+
new Iso8601DateTimeParser(Set.of(), false).withLocale(Locale.ROOT)
271187
);
272188

273189
/////////////////////////////////////////
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.common.time;
10+
11+
import java.time.LocalDate;
12+
import java.time.LocalDateTime;
13+
import java.time.LocalTime;
14+
import java.time.ZoneId;
15+
import java.time.ZoneOffset;
16+
import java.time.temporal.ChronoField;
17+
import java.time.temporal.TemporalAccessor;
18+
import java.time.temporal.TemporalField;
19+
import java.time.temporal.TemporalQueries;
20+
import java.time.temporal.TemporalQuery;
21+
import java.time.temporal.UnsupportedTemporalTypeException;
22+
23+
/**
24+
* Provides information on a parsed datetime
25+
*/
26+
record DateTime(
27+
int years,
28+
Integer months,
29+
Integer days,
30+
Integer hours,
31+
Integer minutes,
32+
Integer seconds,
33+
Integer nanos,
34+
ZoneId zoneId,
35+
ZoneOffset offset
36+
) implements TemporalAccessor {
37+
38+
@Override
39+
@SuppressWarnings("unchecked")
40+
public <R> R query(TemporalQuery<R> query) {
41+
// shortcut a few queries used by DateFormatters.from
42+
if (query == TemporalQueries.zoneId()) {
43+
return (R) zoneId;
44+
}
45+
if (query == TemporalQueries.offset()) {
46+
return (R) offset;
47+
}
48+
if (query == DateFormatters.LOCAL_DATE_QUERY || query == TemporalQueries.localDate()) {
49+
if (months != null && days != null) {
50+
return (R) LocalDate.of(years, months, days);
51+
}
52+
return null;
53+
}
54+
if (query == TemporalQueries.localTime()) {
55+
if (hours != null && minutes != null && seconds != null) {
56+
return (R) LocalTime.of(hours, minutes, seconds, nanos != null ? nanos : 0);
57+
}
58+
return null;
59+
}
60+
return TemporalAccessor.super.query(query);
61+
}
62+
63+
@Override
64+
public boolean isSupported(TemporalField field) {
65+
if (field instanceof ChronoField f) {
66+
return switch (f) {
67+
case YEAR -> true;
68+
case MONTH_OF_YEAR -> months != null;
69+
case DAY_OF_MONTH -> days != null;
70+
case HOUR_OF_DAY -> hours != null;
71+
case MINUTE_OF_HOUR -> minutes != null;
72+
case SECOND_OF_MINUTE -> seconds != null;
73+
case INSTANT_SECONDS -> months != null && days != null && hours != null && minutes != null && seconds != null;
74+
// if the time components are there, we just default nanos to 0 if it's not present
75+
case NANO_OF_SECOND, SECOND_OF_DAY, NANO_OF_DAY -> hours != null && minutes != null && seconds != null;
76+
case OFFSET_SECONDS -> offset != null;
77+
default -> false;
78+
};
79+
}
80+
81+
return field.isSupportedBy(this);
82+
}
83+
84+
@Override
85+
public long getLong(TemporalField field) {
86+
if (field instanceof ChronoField f) {
87+
switch (f) {
88+
case YEAR -> {
89+
return years;
90+
}
91+
case MONTH_OF_YEAR -> {
92+
return extractValue(f, months);
93+
}
94+
case DAY_OF_MONTH -> {
95+
return extractValue(f, days);
96+
}
97+
case HOUR_OF_DAY -> {
98+
return extractValue(f, hours);
99+
}
100+
case MINUTE_OF_HOUR -> {
101+
return extractValue(f, minutes);
102+
}
103+
case SECOND_OF_MINUTE -> {
104+
return extractValue(f, seconds);
105+
}
106+
case INSTANT_SECONDS -> {
107+
if (isSupported(ChronoField.INSTANT_SECONDS) == false) {
108+
throw new UnsupportedTemporalTypeException("No " + f + " value available");
109+
}
110+
return LocalDateTime.of(years, months, days, hours, minutes, seconds)
111+
.toEpochSecond(offset != null ? offset : ZoneOffset.UTC);
112+
}
113+
case NANO_OF_SECOND -> {
114+
if (isSupported(ChronoField.NANO_OF_SECOND) == false) {
115+
throw new UnsupportedTemporalTypeException("No " + f + " value available");
116+
}
117+
return nanos != null ? nanos.longValue() : 0L;
118+
}
119+
case SECOND_OF_DAY -> {
120+
if (isSupported(ChronoField.SECOND_OF_DAY) == false) {
121+
throw new UnsupportedTemporalTypeException("No " + f + " value available");
122+
}
123+
return LocalTime.of(hours, minutes, seconds).toSecondOfDay();
124+
}
125+
case NANO_OF_DAY -> {
126+
if (isSupported(ChronoField.NANO_OF_DAY) == false) {
127+
throw new UnsupportedTemporalTypeException("No " + f + " value available");
128+
}
129+
return LocalTime.of(hours, minutes, seconds, nanos != null ? nanos : 0).toNanoOfDay();
130+
}
131+
case OFFSET_SECONDS -> {
132+
if (offset == null) {
133+
throw new UnsupportedTemporalTypeException("No " + f + " value available");
134+
}
135+
return offset.getTotalSeconds();
136+
}
137+
default -> throw new UnsupportedTemporalTypeException("No " + f + " value available");
138+
}
139+
}
140+
141+
return field.getFrom(this);
142+
}
143+
144+
private static long extractValue(ChronoField field, Number value) {
145+
if (value == null) {
146+
throw new UnsupportedTemporalTypeException("No " + field + " value available");
147+
}
148+
return value.longValue();
149+
}
150+
}

0 commit comments

Comments
 (0)