Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Instant toString prepends plus

We have records in our datastore that have effective & expiry time. This information is stored using the string representation of Instant.

There are some records that would never expire. But since the value for expiry date is mandatory, we decided to store string representation of Instant.MAX.

So far so good. We have search use-case to return all the records that are active within the input time range [s,e]. We query the datastore and return all such records [Si, Ei] which satisfy the condition Si < s && e < Ei Note that string representations are being compared here.

Now the problem is that + is being prepended to the string representation of Instant.MAX. This is failing the condition e < Ei since ASCII('+') < ASCII(digit).

I've written a piece of code to know after which second, the + starts getting prepended:

Long e = Instant.now().getEpochSecond()*1000;
for (int i = 0; i < 5; i++) {
    System.out.println(e + "->" + Instant.ofEpochMilli(e));
    e *= 10;
}

which prints:

1471925168000->2016-08-23T04:06:08Z
14719251680000->2436-06-07T17:01:20Z
147192516800000->6634-05-07T02:13:20Z
1471925168000000->+48613-06-14T22:13:20Z
14719251680000000->+468404-07-08T06:13:20Z

I've the option of truncating the + before persisting in datastore. I'm more interested in why this is happening and how can we explicitly avoid it?

like image 769
nitish712 Avatar asked Aug 23 '16 04:08

nitish712


3 Answers

The answer to your question 'why?' is hidden in the implementation of DateTimeFormatterBuilder.InstantPrinterParser.format() (I left out irrelevant code for brevity):

// use INSTANT_SECONDS, thus this code is not bound by Instant.MAX
Long inSec = context.getValue(INSTANT_SECONDS);
if (inSec >= -SECONDS_0000_TO_1970) {
    // current era
    long zeroSecs = inSec - SECONDS_PER_10000_YEARS + SECONDS_0000_TO_1970;
    long hi = Math.floorDiv(zeroSecs, SECONDS_PER_10000_YEARS) + 1;
    long lo = Math.floorMod(zeroSecs, SECONDS_PER_10000_YEARS);
    LocalDateTime ldt = LocalDateTime.ofEpochSecond(lo - SECONDS_0000_TO_1970, 0, ZoneOffset.UTC);
    if (hi > 0) {
         buf.append('+').append(hi);
    }
    buf.append(ldt);
}

As you can see, it checks boundaries of 10000-year periods and if the value exceeds at least one of them, it adds + and the quantity of such periods.

So, to prevent such behaviour, keep your maximum date within the epoch bounds, don't use Instant.MAX.

like image 181
Andrew Lygin Avatar answered Nov 12 '22 00:11

Andrew Lygin


If you want to be able to support sorting of string value, you need to ensure that you never exceed the year range of 0000 to 9999.

That means to replace Instant.MAX with Instant.parse("9999-12-31T23:59:59Z"), which is also the maximum date most RDBMS's can handle.

To skip the parse step, use Instant.ofEpochSecond(253402300799L).


However, rather than setting a "max" value for an open-ended date range, use a null value, i.e. there is no "max" value.

You would when change your Si < s && e < Ei condition to:

Si < s && (Ei == null || e < Ei)

Also, you're likely missing an = in that condition. In Java, ranges are usually lower-inclusive, upper-exclusive (e.g. see substring(), subList(), etc.), so use this:

Si <= s && (Ei == null || e < Ei)

like image 40
Andreas Avatar answered Nov 12 '22 00:11

Andreas


The default behavior for most date formatters is to prepend a plus sign to a year if it's longer than 4 digits. This applies to both parsing and formatting. See for example Year.parse() and the Year section here. The format is pretty much baked into DateTimeFormatter.ISO_INSTANT (see @AndrewLygin's answer), so if you want to alter it, you'll have to convert your instant to a ZonedDateTime (since instants don't have natural fields) and use a custom formatter:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .appendValue(ChronoField.YEAR, 4, 10, SignStyle.NORMAL)
        .appendLiteral('-')
        .appendValue(ChronoField.MONTH_OF_YEAR, 2)
        .appendLiteral('-')
        .appendValue(ChronoField.DAY_OF_MONTH, 2)
        .appendLiteral('T')
        .append(DateTimeFormatter.ISO_OFFSET_TIME)
        .toFormatter();

String instant = formatter
        .format(Instant.ofEpochMilli(e).atOffset(ZoneOffset.UTC));

The key here is SignStyle.NORMAL, instead of the default SignStyle.EXCEEDS_PAD which would prepend + if the year goes over the 4-digit padding.

like image 31
shmosel Avatar answered Nov 11 '22 23:11

shmosel