0
votes

Am using SimpleDateFormat to format or validate the dates, but I would like to make it thread-safe by using java 8 DateTimeFormatter. I am having trouble to achieve some requirement.

My application will accept only three types of formats. "yyyy-MM-dd", "yyyy-MM", "yyyy"

Existing Code gives me desired output:
/*simple date format to process yyyy-MM-dd format
SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd")
/*simple date format to process yyyy-MM format
SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM")

/*simple date format to process yyyy format
SimpleDateFormat simpleDateFormat3 = new SimpleDateFormat("yyyy")

/* to parse input
simpleDateFormat.parse(input)
/* to format
simpleDateFormat.format(simpleDateFormat1)

Here is the input and expected output:

  input             expected
'2018-03-19'       '2018-03-19'
'2018-03'          '2018-03'
'2018'             '2018'
'2017-02-54'       '2017-02'
'2016-13-19'       '2016'
  1. How can I achieve same result in java 8 DateTimeFormaenter code heretter?

    /* java 8 date time formatter DateTimeFormatter dateTimeFormatter = new DateTimeFormatter("yyyy-MM-dd")

The above snippet works when all year and month and date values are correct. Any help would be highly appreciated.

2
I’m confused why you have these peculiar requirements and also confused how the old code you’ve posted can produce the results you want. If you explain deeper, we may be able to suggest better. Under all circumstances congrats on the decision to move to java.time, the modern Java date and time API.Ole V.V.

2 Answers

5
votes

Am using SimpleDateFormat to format or validate the dates

Never use SimpleDateFormat.

The terrible date-time classes bundled with the earliest versions of Java were years ago supplanted by the modern java.time classes defined in JSR 310.

thread-safe by using java 8 DateTimeFormatter

Yes, unlike the legacy date-time classes, the java.time classes use immutable objects and are thread-safe by design.

Here is the input and expected output:

Some of your inputs could be detected simply by their length.

// Ten-digits long, assume ISO 8601 date.
LocalDate ld = LocalDate.parse( "2018-03-19" ) ;

// Seven digits long, assume ISO 8601 year-month.
YearMonth ym = YearMonth.parse( "2018-03" ) ;

// Four digits, assume year.
Year y = Year.parse( "2018" ) ;

Notice that the above inputs all comply with ISO 8601. The java.time classes use ISO 8601 formats by default when parsing/generating strings. So no need to specify a formatting pattern. And therefore no need for an explicit DateTimeFormatter object.

'2017-02-54' '2017-02'

This example puzzles me. If you mean "When encountering a date with invalid day-of-month, just use the year and month while ignoring the day", I suppose you might be able to do that. Look into "lenient" mode on a DateTimeFormatter. Perhaps use DateTimeFormatterBuilder to build a flexible DateTimeFormatter. But frankly, I would reject such data as faulty inputs. It should be the job of the publisher of the data to produce reliable data, not the job of the consumer to guess the intention behind faulty data.

input expected

'2016-13-19' '2016'

Again, trying to guess the valid parts of invalid inputs in a dangerous game I would not play. If the month and day are invalid, how do you know the year is valid? Even worse, if the publisher of this data can emit such erroneous data, how do you know an apparently valid 2018-03-19 input is actually correct? If month 13 is a mistake, how do know an input with month of 03 is not a mistake too?

Teach the publisher of this problematic data about the ISO 8601 standard, and ask them to fix their bugs.

2
votes

Like Basil Bourque in the other answer I am not necessarily convinced that what you are asking for is also what will serve you the best. In any case, as a small supplement to that good answer I would like to present an approach to handling the last two cases, the invalid dates, the way you said.

For validation of the format I am first parsing the strings without validating the numbers. This will reject strings that are not in any of your three formats, for example 2016-03-19T12:00 or 2016 and some nonsense. The DateTimeFormatter.parseUnresolved method takes care of this part. In case of a parsing error, this method sets an error index in the ParsePosition object that we passed to it and returns null (so does not throw any exception). So I check whether null is returned.

private static DateTimeFormatter yearMonthFormatter = DateTimeFormatter.ofPattern("uuuu-MM")
        .withResolverStyle(ResolverStyle.STRICT);
private static DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("uuuu")
        .withResolverStyle(ResolverStyle.STRICT);

public static Temporal parse(String input) {
    // First try the three formats, uuuu-MM-dd, uuuu-MM, uuuu, in turn without resolving
    TemporalAccessor parsed = null;
    for (DateTimeFormatter formatter : Arrays.asList(DateTimeFormatter.ISO_LOCAL_DATE,
            yearMonthFormatter, yearFormatter)) {
        ParsePosition position = new ParsePosition(0);
        TemporalAccessor parseAttempt = formatter.parseUnresolved(input, position);
        if (parseAttempt != null && position.getIndex() == input.length()) {
            // Success; exit loop
            parsed = parseAttempt;
            break;
        }
    }
    if (parsed == null) { // didn’t match any of the three formats
        throw new IllegalArgumentException("Invalid format: " + input);
    }

    // Try resolving to either LocalDate, YearMonth or Year
    try {
        return LocalDate.of(parsed.get(ChronoField.YEAR),
                parsed.get(ChronoField.MONTH_OF_YEAR),
                parsed.get(ChronoField.DAY_OF_MONTH));
    } catch (DateTimeException dteRLd) {
        try {
            return YearMonth.of(parsed.get(ChronoField.YEAR),
                    parsed.get(ChronoField.MONTH_OF_YEAR));
        } catch (DateTimeException dteRYm) {
            return Year.of(parsed.get(ChronoField.YEAR));
        }
    }
}

Let’s try your examples:

    String[] inputs = {
            "2018-03-19",
            "2018-03",
            "2018",
            "2017-02-54",
            "2016-13-19"
    };

    for (String input : inputs) {
        Temporal parsed = parse(input);
        System.out.format("%-10s  %-10s  %s%n", input, parsed, parsed.getClass().getName());
    }

Output is:

2018-03-19  2018-03-19  java.time.LocalDate
2018-03     2018-03     java.time.YearMonth
2018        2018        java.time.Year
2017-02-54  2017-02     java.time.YearMonth
2016-13-19  2016        java.time.Year