How to create a custom variant of the ISO_INSTANT DateTimeFormatter that does not use colons?
Asked Answered
K

3

9

I would like to convert instances of class java.time.Instant to and from Strings.

I would like to use a format exactly like java.time.format.DateTimeFormatter.ISO_INSTANT with the only difference that the colons in the format are omitted or replaced by dots so that they can be used without escaping in file names and URLs.

Example: 2011-12-03T10.15.30.001Z instead of 2011-12-03T10:15:30.001Z

See Javadoc for ISO_INSTANT: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_INSTANT

Keyway answered 21/9, 2015 at 12:19 Comment(4)
It's probably not trivial, because ISO_INSTANT uses java.time.format.DateTimeFormatterBuilder.InstantPrinterParser and the colon is hardcoded there.Keyway
You case always input.replace(".", ":");... It's probably as good as a complex date time formatter.Phrygia
Yes, this is the pragmatic but very unelegant solution I am using now until I find something better.Keyway
If you have found a solution among the given answers, please mark it as accepted. Otherwise, please clarify the above question so that it can be better answered to suit the issue.Ligament
L
4

You could build your own formatter like this:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss.SSSVV")

The DateTimeFormatter Javadoc lists all possible tokens with their signification.

Lifesaving answered 21/9, 2015 at 12:23 Comment(3)
I think this is not sufficient, because ISO_INSTANT always uses UTC and has no fractional seconds if not needed.Keyway
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss.SSSV") throws java.lang.IllegalArgumentException: Pattern letter count must be 2: VKeyway
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss.SSSVV").format(Instant.EPOCH) produces java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEraKeyway
L
0

An Instant cannot be directly formatted with a DateTimeFormatter. It can be converted to a String using the .toString() method which will format it using the ISO-8601 representation (same as the ISO_INSTANT formatter).

Therefore, I propose two methods for carrying out the custom formatting, both including a conversion to another time type before formatting.

Method 1: Convert to a ZonedDateTime

If you convert to a ZonedDateTime, you can use the formatter proposed in Tunaki's answer.

Instant instant = Instant.now();
DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss.SSSVV");

String formatted = ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).format(FORMATTER);
System.out.println(formatted); // 2019-02-25T11.21.23.257Z

Method 2: Convert to a LocalDateTime

If you convert to a LocalDateTime, you lose any timezone-related information. Therefore using the formatter constructed from the V or O tokens would throw an exception. Just add the Z manually to the end of the string.

Instant instant = Instant.now();
DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss.SSS'Z'");

String formatted = LocalDateTime.ofInstant(instant, ZoneOffset.UTC).format(FORMATTER);
System.out.println(formatted); // 2019-02-25T11.21.23.257Z

Use whichever one you like. Both will result in 2019-02-25T11.21.23.257Z.

Ligament answered 25/2, 2019 at 11:29 Comment(0)
D
0

Looking at the source code of DateTimeFormatter, we see that ISO_INSTANT has its own legacy code for parsing and formatting. If we want to preserve that, in the sense that everything should work just like there, except for our separators, then the only clean way would be to overwrite the original methods. Alas, that cannot be done, because all relevant classes are either final or package-private.

In this situation, although it looks undesirable at first, the best solution is really to tamper directly with the string. Since our deviations from ISO format are simple and well-defined, and since ISO format itself has constant string length in most places, that string editing is not so complicated.

I have written two methods for parsing and formatting which will take any string as a date separator ('-' in original ISO) or time separator (':' in original ISO), including the empty string. We interpret null-separators as empty strings as well. Enjoy!

import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.List;

import static java.time.format.DateTimeFormatter.ISO_INSTANT;

public class IsoInstantExtendedDateTimeFormatter {

    public static String format(TemporalAccessor temporal, String dateSep, String timeSep) {
        String original = ISO_INSTANT.format(temporal);
        String[] dateTime = original.split("T");
        if (dateTime.length < 2) {
            return original;
        }
        return replaceFirstTwoChars(dateTime[0], "-", dateSep) + "T" + replaceFirstTwoChars(dateTime[1], ":", timeSep);
    }

    public static TemporalAccessor parse(CharSequence text, String dateSep, String timeSep) {
        String[] dateTime = text.toString().split("T");
        if (dateTime.length < 2) {
            return ISO_INSTANT.parse(text);
        }
        String input = replaceFirstTwoChars(dateTime[0], dateSep, "-", 4, 2)
                + "T" + replaceFirstTwoChars(dateTime[1], timeSep, ":", 2, 2);
        return ISO_INSTANT.parse(input);
    }

    private static String replaceFirstTwoChars(String text, String s1, String s2) {
        return replaceFirstTwoChars(text, s1, s2, -1, -1);
    }

    private static String replaceFirstTwoChars(String text, String s1, String s2, int lenFirstPart, int lenSecondPart) {

        if (text == null) {
            return null;
        }

        if (s1 == null) {
            s1 = "";
        }
        if (s2 == null) {
            s2 = "";
        }

        int endSecondPart;
        if (s1.isEmpty()) {
            if (s2.isEmpty()) {
                return text;
            }
            endSecondPart = lenFirstPart + lenSecondPart;
        } else {
            lenFirstPart = text.indexOf(s1);
            endSecondPart = text.indexOf(s1, lenFirstPart + s1.length());
        }

        if (lenFirstPart < 0 || endSecondPart < 0) {
            return text;
        }

        return text.substring(0, lenFirstPart)
                + s2
                + text.substring(lenFirstPart + s1.length(), endSecondPart)
                + s2
                + text.substring(endSecondPart + s1.length());

    }

    // --- All code below is just for demo purposes ---

    private static final String[] dateStrings = new String[]{
            "20220331T112233Z", "20220404T223344.555Z",
            "20220331T11.22.33Z", "20220404T22.33.44.555Z",
            "2022/03/31T11.22.33Z", "2022/04/04T22.33.44.555Z"
    };
    private static final String[] dateSeps = new String[]{ "", "", "", "", "/", "/" };
    private static final String[] timeSeps = new String[]{ "", "", ".", ".", ".", "." };

    public static void main(String[] args) {
        List<TemporalAccessor> dates = new ArrayList<>();
        for (int i = 0; i<dateStrings.length; i++) {
            dates.add(parse(dateStrings[i], dateSeps[i], timeSeps[i]));
        }
        for (TemporalAccessor date : dates) {
            System.out.println(format(date, "", "")
                    + " | " + format(date, "", ".")
                    + " | " + format(date, "/", ".")
                    + " | " + ISO_INSTANT.format(date));
        }
    }

}

Dearborn answered 1/7, 2022 at 14:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.