0

Im trying to parse a string that contain a date in an unknown format and the way I choose (not the best) is to try all the possible formats until parse correctlly. To do this Im using Vavr library and till now I've created something like this:

// My unknown date
    String date = "2020-11-12T15:15:15.345";


    // Date format that works for my unknown date (just for testing)
    DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
            .appendPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]")
            .parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
            .toFormatter();
    OffsetDateTime value = OffsetDateTime.parse(date, FORMATTER);                   // PARSE CORRECTLY 


// Try all possible formats until one works
    Try<OffsetDateTime> myParsedDate = Try.of(()->date)
            .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
            .onFailure(x->System.out.println("NO yyyy-MM-dd"))

            .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm")))
            .onFailure(x->System.out.println("NO yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm"))

            .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm")))
            .onFailure(x->System.out.println("NO yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm"))

            .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")))
            .onFailure(x->System.out.println("NO yyyy-MM-dd'T'HH:mm:ss.SSS"))

            .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")))
            .onFailure(x->System.out.println("NO yyyy-MM-dd'T'HH:mm:ss.SSSZ"))

            .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))
            .onFailure(x->System.out.println("NO yyyy-MM-dd'T'HH:mm:ss"))

            .map(x->OffsetDateTime.parse(date, FORMATTER))                          // DOSENT WORK
            .onFailure(x->System.out.println("NO yyyy-MM-dd'T'HH:mm:ss[.SSS]"));
    if(myParsedDate.isSuccess()) {
        System.out.println("OK");
    }else {
        System.out.println("KO");
    }

Output:

NO yyyy-MM-dd
NO yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm
NO yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm
NO yyyy-MM-dd'T'HH:mm:ss.SSS
NO yyyy-MM-dd'T'HH:mm:ss.SSSZ
NO yyyy-MM-dd'T'HH:mm:ss
NO yyyy-MM-dd'T'HH:mm:ss[.SSS]

The question is: how to concatenate many try/catch or in this case using VAVR many actions that when one action fail try the next one and so one ? Thanks

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
Ares91
  • 506
  • 2
  • 8
  • 27

3 Answers3

2

An answer with vavr

import io.vavr.collection.Iterator;

String[] patterns = new String[] {
   "yyyy-MM-dd",
   "yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm",
   "yyyy-MM-dd'T'HH:mm:ss.SSS",
   "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
   "yyyy-MM-dd'T'HH:mm:ss"
};

final Option<OffsetDateTime> offsetDateTimeOption =
   Iterator.of(patterns)                                                     // 1
      .map(DateTimeFormatter::ofPattern)                                     // 2
      .concat(Iterator.of(FORMATTER))                                        // 3
      .map(formatter -> Try.of(() -> OffsetDateTime.parse(date, formatter))) // 4
      .flatMap(Try::iterator)                                                // 5
      .headOption();                                                         // 6

Steps

  1. Start with a lazy Iterator over the array of patterns
  2. Convert to formatters
  3. Append the fallback formatter to the Iterator
  4. Parse the date using the formatter, wrapping the result in Try
  5. Flatten Iterator<Try<OffsetDateTime>> to Iterator<OffsetDateTime> by creating an iterator from each Try. An iterator on try will be a single element iterator if it's a success or an empty iterator if it's a failure
  6. Take the first element of the resulting iterator and return it as a Some if it's not empty or return a None

The above pipeline is lazy, that is, it only tries as many of the patterns/formatters as needed to find the first successful one, because vavr Iterator is itself lazy.

My answer only focuses on how to do a lazy evaluation until first success with vavr, I didn't try to correct other aspects of your question that would result in your patterns not matching date strings that are apparently conforming to some of those patterns. Other answers to your question go into great details about that which I don't want to repeat here.

Nándor Előd Fekete
  • 6,988
  • 1
  • 22
  • 47
1

Put your formatter into a Java Stream and try on each of them until one successful:

import io.vavr.control.Try;

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.Optional;
import java.util.stream.Stream;

public class Test {
    public static void main(String[] args) {
        String date = "2020-11-12T15:15:15.345";

        DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
                .appendPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]")
                .parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
                .toFormatter();
        OffsetDateTime value = OffsetDateTime.parse(date, FORMATTER);

        Optional<OffsetDateTime> res = Stream.concat(Stream.of(
                "yyyy-MM-dd",
                "yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm",
                "yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm",
                "yyyy-MM-dd'T'HH:mm:ss.SSS",
                "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
                "yyyy-MM-dd'T'HH:mm:ss")
                .map(p -> DateTimeFormatter.ofPattern(p)), Stream.of(FORMATTER))
                .map(fmt -> Try.of(() -> OffsetDateTime.parse(date, fmt)))
                .filter(Try::isSuccess)
                .map(Try::get)
                .findFirst();

        System.out.println(res);  //prints Optional[2020-11-12T15:15:15.345Z]
    }
}

Stream.concat is used for adding the FORMATTER with the rest of the Formatters.

In the end you will get an Optional<OffsetDateTime>. It will be a None if everything failed, or a Some if one of them succeeded. The Java Stream is lazy so that once one match found, it will stop the rest from being executed.

If you want to print out all the failed cases too, you can add onFailure before filter.


Edit: adding the case for special FORMATTER

SwiftMango
  • 15,092
  • 13
  • 71
  • 136
0

I don’t know Vavr. My answer has two parts:

  1. How to parse your string without Vavr.
  2. How to fix your Vavr solution so that it works.

I don’t think you need Vavr

    String date = "2020-11-12T15:15:15.345";

    DateTimeFormatter flexibleFormatter = new DateTimeFormatterBuilder()
            .append(DateTimeFormatter.ISO_LOCAL_DATE)
            .optionalStart()
            .appendLiteral('T')
            .append(DateTimeFormatter.ISO_LOCAL_TIME)
            .optionalStart()
            .appendOffsetId()
            .optionalEnd()
            .optionalEnd()
            .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
            .parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
            .toFormatter();

    OffsetDateTime value = OffsetDateTime.parse(date, flexibleFormatter);

    System.out.println(value);

Output:

2020-11-12T15:15:15.345Z

I have tried to build a formatter that handles all of the format variants that you try to take into account in your Vavr Try construct.

If you prefer to fix your Vavr solution

There are some issues with the formatters you are trying to use on the Vavr construct. Let’s fix them in turn.

        .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")))

Parsing using format yyyy-MM-dd will not give you enough information for an OffsetDateTime. You would be getting the date but neither the time of day nor the offset. Instead I would parse into a LocalDate and convert afterwards:

        .map(x->LocalDate.parse(date).atStartOfDay(ZoneOffset.UTC).toOffsetDateTime())

I am exploiting the fact that LocalDate parses your format as its default, without any explicit formatter. The one-arg atStartOfDay method gives us a ZonedDateTime, so we need one more conversion step after that. Another solution would have been to fit the formatter with default time of day and default offset. This would be similar to what you are doing in your formatter that works, only with two calls to parseDefaulting().

Next issue:

        .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm")))

It seems you’ve been trying to parse a string like 2020-11-12T15:15:15.34+01:00. +HH:mm is incorrect for this. HH is for hour of day and mm for minute of the hour. But the +01:00 at the end is an offset from UTC, not a time of the day, so +HH:mm won’t work. Also the + is a sign, the offset could also have been negative, like -04:00. Again a default format saves us, OffsetDateTime parses strings like the one mentioned without any explicit formatter:

        .map(x->OffsetDateTime.parse(date))

The default format is ISO 8601. Link at the bottom.

        .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS+HH:mm")))

This repeats what you were trying before, including the same error. It doesn’t harm, but I suggest you leave it out.

        .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")))

This should have matched your “unknown” string. You are parsing date and time but are missing the offset. My solution would be to parse into a LocalDateTime and then convert. Again the default ISO 8601 format saves me from building a formatter.

        .map(x->LocalDateTime.parse(date).atOffset(ZoneOffset.UTC))

Next:

        .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")))

While I don’t know, I suspect that you tried to match a string with a trailing Z for UTC, like 2020-11-12T15:15:15.345Z. The one-arg OffsetDateTime.parse() used earlier also accepts this variant, so you can leave this part out. BTW one pattern letter Z is for offset without colon, such as +0000 and cannot parse Z.

Next:

        .map(x->OffsetDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))

Again we are missing an offset, and again this is parsed by LocalDateTIme previously. In ISO 8601 the fraction of second is optional, so the one-arg LocalDateTime.parse() accepts strings with and without it. Leave out this part.

Finally:

        .map(x->OffsetDateTime.parse(date, FORMATTER))                          // DOSENT WORK

I can’t tell why this doesn’t work when the formatter does in isolation. I am thinking that it could be something Vavr specific that I don’t understand. Still it leaves me wondering. In any case your string should have been taken care of by one of the previous entries, so it may not matter.

Link

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161