1

Given any date/time format (passed by the user), I need to parse the date with it and return milliseconds since java epoch, something that could be done with the following code using old date API:

//dateFormatter is SimpleDateFormat
Date d = dateFormatter.parse(value);
return d.getTime();

The only requirement for the format that it will contain the date part, for example all of the following are possible formats:

"dd/MM/yyyy"
"dd/MM/yyyy HH:mm X" //with timezone

The missing part are completed with start of the day/local time zone. So I came up with the following, which seems to be much more verbose and not sure if there is a more efficient way to do it:

        //dateFormatter is DateTimeFormatter
        TemporalAccessor ta = dateFormatter.parse(value);
        
        LocalDate ld = LocalDate.from(ta); //assume must have date part

        LocalTime lt = ta.query(TemporalQueries.localTime());
        if (lt == null) {
            lt = LocalTime.MIN;
        }
        
        ZoneId zoneId = ta.query(TemporalQueries.zone());
        if (zoneId == null) {
            zoneId = ZoneId.systemDefault();
        }
        
        Instant d = LocalDateTime.of(ld, lt).atZone(zoneId).toInstant();
        return d.toEpochMilli();

cpp2005
  • 51
  • 4
  • 1
    I think there's a flaw in the code, i.e. when the time is missing, you shouldn't apply time zone offset, i.e. the `zoneId` should be `ZoneOffset.UTC` if `lt == null`. – Andreas Jan 08 '21 at 13:01

2 Answers2

2

Use DateTimeFormatterBuilder.parseDefaulting to set default values for when that field is not found in the format string:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendPattern(patternFromTheUser)
        // make everything non-required default to 0
        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
        .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
        .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
        .parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
        .toFormatter();
System.out.println(ZonedDateTime.parse("01/01/2021 20:00", formatter)
        .toInstant()
        .toEpochMilli());
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • I can't use these methods, because like I said I get a pattern from the user. I don't know what is missing in the format. – cpp2005 Jan 08 '21 at 12:02
  • 1
    @cpp2005 It doesn't matter. You can still set all the fields' default value to 0, even if they are not missing. That's the whole point of a default value. If it is there, use its value, if it's not there, use the default. – Sweeper Jan 08 '21 at 12:03
  • 1
    I tried it, but my impression was that it is not very reliable, meaning it can create conflict if user does specify some of this info but using different format specifies. For example, see this: https://stackoverflow.com/questions/38307816/datetimeformatterbuilder-with-specified-parsedefaulting-conflicts-for-year-field – cpp2005 Jan 08 '21 at 12:20
  • 1
    @cpp2005 Indeed. I can't actually think of a solution to that. The problematic fields are the (clock-)hour-of-(am-pm) fields, am-pm, milli-of-day, and nano-of-day. If you don't want to disallow those fields in the user's pattern, then I think the approach shown in your question is fine. Now that I rethink about what you are actually doing, this is quite an obscure thing to do, and I would expect there to be no built-in API for this, and that you should clearly write out the logic to show intent, as you did. – Sweeper Jan 08 '21 at 12:55
2

As also shown in answer by Sweeper, you can specify default values in the formatter.

Zone ID is handled differently, where it is not just a default, it is also a kind of override, and has to be defined using withZone(ZoneId zone) after building the formatter.

If the format and value specifies a Zone ID, then it is used, all good. If it doesn't specify Zone ID or Zone Offset, the given "default" is used. However, if only Zone Offset is specified, it is used for parsing, but then the "default" Zone ID overrides the result, adjusting the time and Zone Offset accordingly.

Since you want epoch milliseconds, that doesn't actually affect this code, I just wanted to mention it for other users who might not do the final conversion to UTC.

The code should be:

public static long parseToEpochMillis(String format, String dateText) {
    return new DateTimeFormatterBuilder()
            .appendPattern(format)
            .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
            .toFormatter(Locale.US) // So e.g. MMM parses "Feb", not some other language
            .withZone(ZoneId.systemDefault())
            .parse(dateText, ZonedDateTime::from)
            .toInstant()
            .toEpochMilli();
}

Test

public static void main(String[] args) {
    test("M/d/u", "7/4/2015");
    test("M/d/u H:mm:ss VV", "7/4/2015 1:23:45 America/Denver");
    test("M/d/u H:mm:ssXXXXX", "7/4/2015 1:23:45-08:00");
    test("M/d/u H:mm:ssX", "7/4/2015 1:23:45Z");
}
static void test(String format, String dateText) {
    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern(format)
            .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
            .toFormatter(Locale.US)
            .withZone(ZoneId.systemDefault());
    ZonedDateTime dateTime = ZonedDateTime.parse(dateText, formatter);
    Instant instant = dateTime.toInstant();
    long epochMilli = instant.toEpochMilli();
    System.out.printf("%-45s -> %s -> %d%n", dateTime, instant, epochMilli);
}

Output

2015-07-04T00:00-04:00[America/New_York]      -> 2015-07-04T04:00:00Z -> 1435982400000
2015-07-04T01:23:45-06:00[America/Denver]     -> 2015-07-04T07:23:45Z -> 1435994625000
2015-07-04T05:23:45-04:00[America/New_York]   -> 2015-07-04T09:23:45Z -> 1436001825000
2015-07-03T21:23:45-04:00[America/New_York]   -> 2015-07-04T01:23:45Z -> 1435973025000
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • 1
    thanks I get the timezone part, however, about parseDefaulting(), see my answer to Sweeper, it seems unreliable in general case. For example, I suspect it will throw exception if I specify time differently, not as hour of the day in your example (like with K modifier), see this example https://stackoverflow.com/questions/38307816/datetimeformatterbuilder-with-specified-parsedefaulting-conflicts-for-year-field – cpp2005 Jan 08 '21 at 12:48
  • @cpp2005 You have a point there. If any format is allowed, then you have to do it like you're doing it in the question. – Andreas Jan 08 '21 at 12:59