Barium Scoorge
Barium Scoorge

Reputation: 2008

Validating Java 8 dates

I'd like to validate several date formats, as below examples :

YYYY
YYYY-MM
YYYY-MM-DD

Validation must ensure that date format is correct and the date exists.

I'm aware that Java 8 provides a new Date API, so I'm wondering if it's able to do such job.

Is there a better way using Java 8 date API ? Is it still a good practice to use Calendar class with lenient parameter ?

Upvotes: 10

Views: 25895

Answers (6)

Olivier Grégoire
Olivier Grégoire

Reputation: 35417

Use optional fields and parseBest

You only want to validate, I understand that, but afterwards you'll most likely want to extract the data in an appopriate way. Fortunately, and indeed as you wrote, Java 8 provides such a method, parseBest.

parseBest works with optional fields. So define the format you want to parse first: yyyy[-MM[-dd]], with the brackets ([ and ]) wrapping the optional fields.

parseBest also requires you to provide several TemporalQuery<R>. Actually, it's just a functional wrapper around a template method <R> R queryFrom(TemporalAccessor). So we can actually define a TemporalQuery<R> as simply as Year::from. Good: that's exactly what we want. The thing is that parseBest is not really well named: it will parse all in order and stop after the first proper TemporalQuery that matches. So in your case, we have to go from most precise to less precise. Here are the various types that you want to handle: LocalDate, YearMonth and Year. So let's just define the TemporalQuery[] as LocalDate::from, YearMonth::from, Year::from. Now, if parseBest doesn't recognize your input, it will throw an exception.

All in all, we'll construct the parseBest as follow:

parseBest(DateTimeFormatter.ofPattern("yyyy[-MM[-dd]]"), LocalDate::from, YearMonth::from, Year::from);

So let's write it properly:

static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy[-MM[-dd]]");

static TemporalAccessor parseDate(String dateAsString) {
  return FORMATTER.parseBest(dateAsString, LocalDate::from, YearMonth::from, Year::from);
}

But... you only want to validate... Well in that case, a date is computed and the expensive job is already done. So let's just define the validation as follow:

public static boolean isValidDate(String dateAsString) {
  try {
    parseDate(dateAsString);
    return true;
  } catch (DateTimeParseException e) {
    return false;
  }
}

I know, it's bad to use exceptions to handle cases like this, but while the current API is very powerful this very specific case wasn't taken in account so let's just stick to it and use it as is.

But now, we have this issue: invalid dates can be parsed. For instance "2018-15" will be parsed as Year{2018} because Year is the best TemporalAccessor that the parser can use: YearMonth would fail, but it is a YearMonth, so it must be valid according to that. So let's just pass it again in the FORMATTER.

public static boolean isValidDate(String dateAsString) {
  try {
    TemporalAccessor date = parseDate(dateAsString);
    return FORMATTER.format(date).equals(dateAsString);
  } catch (DateTimeParseException e) {
    return false;
  }
}

Here's the full code:

import java.time.*;
import java.time.format.*;
import java.time.temporal.*;
class Main {

  private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy[-MM[-dd]]");

  static TemporalAccessor parseDate(String dateAsString) {
    return FORMATTER.parseBest(dateAsString, LocalDate::from, YearMonth::from, Year::from);
  }

  public static boolean isValidDate(String dateAsString) {
    try {
      TemporalAccessor parsedDate = parseDate(dateAsString);
      return FORMATTER.format(parsedDate).equals(dateAsString);
    } catch (DateTimeParseException e) {
      return false;
    }
  }

  public static void main(String[] args) {   &#32;
    String[] datesAsString = {
      "2018",
      "2018-05",
      "2018-05-22",
      "abc",
      "2018-",
      "2018-15",
    };
    for (String dateAsString: datesAsString) {
      System.out.printf("%s: %s%n", dateAsString, isValidDate(dateAsString) ? "valid" : "invalid");
    }
  }
}

Try it online!

Output:

2018: valid
2018-05: valid
2018-05-22: valid
abc: invalid
2018-: invalid
2018-15: invalid

You want more than validating, like getting the actual value?

Note that you can still use the data retrieved from parseBest for further uses like this:

TemporalAccessor dateAccessor = parseDate(dateAsString);
if (dateAccessor instanceof Year) {
  Year year = (Year)dateAccessor;
  // Use year
} else if (dateAccessor instanceof YearMonth) {
  YearMonth yearMonth = (YearMonth)dateAccessor;
  // Use yearMonth
} else if (dateAccessor instanceof LocalDate) {
  LocalDate localDate = (LocalDate)dateAccessor;
  // Use localDate
}

But be careful because as mentioned earlier, "2018-15" will still be parsed, as Year{2018} and not YearMonth{2018,15}, or "2018-02-30" will still be parsed as a YearMonth and not as a LocalDate.

Upvotes: 6

rahulnikhare
rahulnikhare

Reputation: 1486

Please use the below code. This will validate and also works in every format.

public class DateValidator {
    public static void main(String[] args) {

        String datetoCheck = "999999";

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
        try {
            LocalDate localDate = LocalDate.parse(datetoCheck, formatter);
            System.out.println(localDate);
        } catch ( DateTimeException ex ) {
            ex.printStackTrace();
        }
    }
}

Upvotes: 0

Sakai
Sakai

Reputation: 11

You can use lenient, is your default is true:

in SimpleDAteFormat

SimpleDateFormat sdf = new SimpleDateFormat(dateFromat);
sdf.setLenient(false);

or in JSON validate:

@JsonFormat(lenient = OptBoolean.FALSE)

Upvotes: 0

makson
makson

Reputation: 2243

public static final boolean validateInputDate(final String isoDate, final String dateFormat){
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
    try {
        final Date date = simpleDateFormat.parse(isoDate);
        System.out.println("Date: " + date);
        return true;
    } catch (ParseException e) {
        e.printStackTrace();
        return false;
    }
}

Upvotes: 0

Jean-Fran&#231;ois Savard
Jean-Fran&#231;ois Savard

Reputation: 21004

To validate the YYYY-MM-DD format, you can simply use LocalDate.parse introduced in java.time since JDK 8.

Obtains an instance of LocalDate from a text string such as 2007-12-03.

The string must represent a valid date and is parsed using DateTimeFormatter.ISO_LOCAL_DATE.

A DateTimeParseException will be thrown if the date is invalid.

For the other two formats you gave us, the exception would be thrown. That is logical because they are not real date, simply part of a date.


LocalDate also provide a method of(int year, int month, int dayOfMonth) thus if you really want to validate simply the year in some case, the year with the month in other case or the full date then you could do something like this :

public static final boolean validateInputDate(final String isoDate)
{
    String[] dateProperties = isoDate.split("-");

    if(dateProperties != null)
    {
        int year = Integer.parseInt(dateProperties[0]);

        // A valid month by default in the case it is not provided.
        int month = dateProperties.length > 1 ? Integer.parseInt(dateProperties[1]) : 1;

        // A valid day by default in the case it is not provided.
        int day = dateProperties.length > 2 ? Integer.parseInt(dateProperties[2]) : 1;

        try
        {
            LocalDate.of(year, month, day);
            return true;
        }
        catch(DateTimeException e)
        {
            return false;
        }
    }

    return false;
}

Note that you mentionned several formats but did not provide them, so I assumed these were the only 3.

Upvotes: 4

Tagir Valeev
Tagir Valeev

Reputation: 100199

You can specify missing fields with parseDefaulting to make all the formatters working:

public static boolean isValid(String input) {
    DateTimeFormatter[] formatters = {
            new DateTimeFormatterBuilder().appendPattern("yyyy")
                    .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
                    .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
                    .toFormatter(),
            new DateTimeFormatterBuilder().appendPattern("yyyy-MM")
                    .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
                    .toFormatter(),
            new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd")
                    .parseStrict().toFormatter() };
    for(DateTimeFormatter formatter : formatters) {
        try {
            LocalDate.parse(input, formatter);
            return true;
        } catch (DateTimeParseException e) {
        }
    }
    return false;
}

Upvotes: 6

Related Questions