@@ -35,6 +35,25 @@ class EpochTime {
3535
3636 private static final ValueRange POSITIVE_LONG_INTEGER_RANGE = ValueRange .of (0 , Long .MAX_VALUE );
3737
38+ // TemporalField is only present in the presence of a rounded timestamp
39+ private static final long ROUNDED_SIGN_PLACEHOLDER = -2 ;
40+ private static final EpochField ROUNDED_SIGN_FIELD = new EpochField (
41+ ChronoUnit .FOREVER ,
42+ ChronoUnit .FOREVER ,
43+ ValueRange .of (ROUNDED_SIGN_PLACEHOLDER , ROUNDED_SIGN_PLACEHOLDER )
44+ ) {
45+ // FIXME: what should this be?
46+ @ Override
47+ public boolean isSupportedBy (TemporalAccessor temporal ) {
48+ return temporal .isSupported (ChronoField .INSTANT_SECONDS ) && temporal .getLong (ChronoField .INSTANT_SECONDS ) < 0 ;
49+ }
50+
51+ @ Override
52+ public long getFrom (TemporalAccessor temporal ) {
53+ return ROUNDED_SIGN_PLACEHOLDER ;
54+ }
55+ };
56+
3857 // TemporalField is only present in the presence of a negative (potentially fractional) timestamp.
3958 private static final long NEGATIVE_SIGN_PLACEHOLDER = -1 ;
4059 private static final EpochField NEGATIVE_SIGN_FIELD = new EpochField (
@@ -161,6 +180,10 @@ public TemporalAccessor resolve(
161180 Long nanosOfMilli = fieldValues .remove (NANOS_OF_MILLI );
162181 long secondsAndMillis = fieldValues .remove (this );
163182
183+ // this flag indicates whether we were asked to round up and we defaulted to 999_999 nanos or nanos were given by the users
184+ // specifically we do not wnat to confuse defaulted 999_999 nanos with user supplied 999_999 nanos
185+ boolean roundUp = fieldValues .remove (ROUNDED_SIGN_FIELD ) != null ;
186+
164187 long seconds ;
165188 long nanos ;
166189 if (isNegative != null ) {
@@ -169,10 +192,18 @@ public TemporalAccessor resolve(
169192 nanos = secondsAndMillis % 1000 * 1_000_000 ;
170193 // `secondsAndMillis < 0` implies negative timestamp; so `nanos < 0`
171194 if (nanosOfMilli != null ) {
172- // aggregate fractional part of the input; subtract b/c `nanos < 0`
173- nanos -= nanosOfMilli ;
195+ if (roundUp ) {
196+ // these are not the nanos you think they are; these are "round up nanos" not the fractional part of the input
197+ // this is the case where we defaulted the value to 999_999 and the intention for rounding is that the value
198+ // moves closer to positive infinity
199+ nanos += nanosOfMilli ;
200+ } else {
201+ // aggregate fractional part of the input; subtract b/c `nanos < 0`
202+ // this is the case where the user has supplied a nanos value and we'll want to shift toward negative infinity
203+ nanos -= nanosOfMilli ;
204+ }
174205 }
175- if (nanos != 0 ) {
206+ if (nanos < 0 ) {
176207 // nanos must be positive. B/c the timestamp is represented by the
177208 // (seconds, nanos) tuple, seconds moves 1s toward negative-infinity
178209 // and nanos moves 1s toward positive-infinity
@@ -235,38 +266,70 @@ public long getFrom(TemporalAccessor temporal) {
235266 .appendLiteral ('.' )
236267 .toFormatter (Locale .ROOT );
237268
238- // this supports milliseconds
239- public static final DateTimeFormatter MILLISECONDS_FORMATTER1 = new DateTimeFormatterBuilder ().optionalStart ()
269+ static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter (
270+ "epoch_second" ,
271+ new JavaTimeDateTimePrinter (SECONDS_FORMATTER1 ),
272+ JavaTimeDateTimeParser .createRoundUpParserGenerator (builder -> builder .parseDefaulting (ChronoField .NANO_OF_SECOND , 999_999_999L )),
273+ new JavaTimeDateTimeParser (SECONDS_FORMATTER1 ),
274+ new JavaTimeDateTimeParser (SECONDS_FORMATTER2 )
275+ );
276+
277+ public static final DateTimeFormatter MILLISECONDS_FORMATTER_BASE = new DateTimeFormatterBuilder ().optionalStart ()
240278 .appendText (NEGATIVE_SIGN_FIELD , Map .of (-1L , "-" )) // field is only created in the presence of a '-' char.
241279 .optionalEnd ()
242280 .appendValue (UNSIGNED_MILLIS , 1 , 19 , SignStyle .NOT_NEGATIVE )
281+ .toFormatter (Locale .ROOT );
282+
283+ // FIXME: clean these up and append one to the other
284+ // this supports milliseconds
285+ public static final DateTimeFormatter MILLISECONDS_FORMATTER = new DateTimeFormatterBuilder ().append (MILLISECONDS_FORMATTER_BASE )
243286 .optionalStart ()
244287 .appendFraction (NANOS_OF_MILLI , 0 , 6 , true )
245288 .optionalEnd ()
246289 .toFormatter (Locale .ROOT );
247290
248- // this supports milliseconds ending in dot
249- private static final DateTimeFormatter MILLISECONDS_FORMATTER2 = new DateTimeFormatterBuilder ().optionalStart ()
250- .appendText (NEGATIVE_SIGN_FIELD , Map .of (-1L , "-" )) // field is only created in the presence of a '-' char.
251- .optionalEnd ()
252- .appendValue (UNSIGNED_MILLIS , 1 , 19 , SignStyle .NOT_NEGATIVE )
253- .appendLiteral ('.' )
291+ // this supports milliseconds
292+ public static final DateTimeFormatter MILLISECONDS_PARSER_W_NANOS = new DateTimeFormatterBuilder ().append (MILLISECONDS_FORMATTER_BASE )
293+ .appendFraction (NANOS_OF_MILLI , 0 , 6 , true )
254294 .toFormatter (Locale .ROOT );
255295
256- static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter (
257- "epoch_second" ,
258- new JavaTimeDateTimePrinter (SECONDS_FORMATTER1 ),
259- JavaTimeDateTimeParser .createRoundUpParserGenerator (builder -> builder .parseDefaulting (ChronoField .NANO_OF_SECOND , 999_999_999L )),
260- new JavaTimeDateTimeParser (SECONDS_FORMATTER1 ),
261- new JavaTimeDateTimeParser (SECONDS_FORMATTER2 )
262- );
296+ // we need an additional parser to detect the difference between user provided nanos and defaulted ones because of the necessity
297+ // to parse the two differently in the round up case
298+ public static final DateTimeFormatter MILLISECONDS_PARSER_WO_NANOS = new DateTimeFormatterBuilder ().append (MILLISECONDS_FORMATTER_BASE )
299+ .toFormatter (Locale .ROOT );
263300
301+ // we need an additional parser to detect the difference between user provided nanos and defaulted ones because of the necessity
302+ // to parse the two differently in the round up case
303+ public static final DateTimeFormatter MILLISECONDS_PARSER_WO_NANOS_ROUNDING = new DateTimeFormatterBuilder ().append (
304+ MILLISECONDS_FORMATTER_BASE
305+ ).parseDefaulting (EpochTime .ROUNDED_SIGN_FIELD , -2L ).parseDefaulting (EpochTime .NANOS_OF_MILLI , 999_999L ).toFormatter (Locale .ROOT );
306+
307+ // this supports milliseconds ending in dot
308+ private static final DateTimeFormatter MILLISECONDS_PARSER_ENDING_IN_PERIOD = new DateTimeFormatterBuilder ().append (
309+ MILLISECONDS_FORMATTER_BASE
310+ ).appendLiteral ('.' ).toFormatter (Locale .ROOT );
311+
312+ /*
313+ We separately handle the rounded and non-rounded uses cases here with different parsers. The reason is because of how we store and
314+ handle negative milliseconds since the epoch. If a user supplies nanoseconds as part of a negative millisecond since epoch value
315+ then we need to round toward negative infinity. However, in the case where nanos are not supplied, and we are requested to
316+ round up we will default the value of nanos to 999_999 and need to delineate that this rounding was intended to push the value
317+ toward positive infinity not negative infinity. Differentiating these two cases during parsing requires a flag called out above
318+ the ROUNDED_SIGN_FIELD flag. In addition to this flag we need to know that we are in the "rounding up" state. So any time we are
319+ asked to round up we will force setting the ROUNDED_SIGN_FIELD flag and be able to detect that when parsing and
320+ storing the time information and be able to make the correct decision to round toward positive infinity.
321+ */
264322 static final DateFormatter MILLIS_FORMATTER = new JavaDateFormatter (
265323 "epoch_millis" ,
266- new JavaTimeDateTimePrinter (MILLISECONDS_FORMATTER1 ),
267- JavaTimeDateTimeParser .createRoundUpParserGenerator (builder -> builder .parseDefaulting (EpochTime .NANOS_OF_MILLI , 999_999L )),
268- new JavaTimeDateTimeParser (MILLISECONDS_FORMATTER1 ),
269- new JavaTimeDateTimeParser (MILLISECONDS_FORMATTER2 )
324+ new JavaTimeDateTimePrinter (MILLISECONDS_FORMATTER ),
325+ new JavaTimeDateTimeParser [] {
326+ new JavaTimeDateTimeParser (MILLISECONDS_PARSER_WO_NANOS_ROUNDING ),
327+ new JavaTimeDateTimeParser (MILLISECONDS_PARSER_W_NANOS ),
328+ new JavaTimeDateTimeParser (MILLISECONDS_PARSER_ENDING_IN_PERIOD ) },
329+ new JavaTimeDateTimeParser [] {
330+ new JavaTimeDateTimeParser (MILLISECONDS_PARSER_WO_NANOS ),
331+ new JavaTimeDateTimeParser (MILLISECONDS_PARSER_W_NANOS ),
332+ new JavaTimeDateTimeParser (MILLISECONDS_PARSER_ENDING_IN_PERIOD ) }
270333 );
271334
272335 private abstract static class EpochField implements TemporalField {
0 commit comments