What Is the Use of "Lenient "

Mockito lenient() when to use

For example, it's very useful while migrating from Mockito 1 to Mockito 2 (the latter introduced strict stubbing), if you need to do it in a short period of time.

SimpleDateFormat leniency leads to unexpected behavior

Why does this (not) happen?

It’s not very well explained in the documentation.

With lenient parsing, the parser may use heuristics to interpret
inputs that do not precisely match this object's format. With strict
parsing, inputs must match this object's format.

The documentation does help a bit, though, by mentioning that it is the Calendar object that the DateFormat uses that is lenient. That Calendar object is not used for the parsing itself, but for interpreting the parsed values into a date and time (I am quoting DateFormat documentation since SimpleDateFormat is a subclass of DateFormat).

  • SimpleDateFormat, no matter if lenient or not, will accept 3-digit year, for example 199, even though you have specified yyyy in the format pattern string. The documentation says about year:

    For parsing, if the number of pattern letters is more than 2, the year
    is interpreted literally, regardless of the number of digits. So using
    the pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.

  • DateFormat, no matter if lenient or not, accepts and ignores text after the parsed text, like the small letter o in your first example. It objects to unexpected text before or inside the text, as when in your last example you put the letter o in front. The documentation of DateFormat.parse says:

    The method may not use the entire text of the given string.

  • As I indirectly said, leniency makes a difference when interpreting the parsed values into a date and time. So a lenient SimpleDateFormat will interpret 29.02.2019 as 01.03.2019 because there are only 28 days in February 2019. A strict SimpleDateFormat will refuse to do that and will throw an exception. The default lenient behaviour can lead to very surprising and downright inexplicable results. As a simple example, giving the day, month and year in the wrong order: 1990.03.12 will result in August 11 year 17 AD (2001 years ago).

The solution

VGR already in a comment mentioned LocalDate from java.time, the modern Java date and time API. In my experience java.time is so much nicer to work with than the old date and time classes, so let’s give it a shot. Try a correct date string first:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.mm.yyyy");
System.out.println(LocalDate.parse("03.12.1990", dateFormatter));

We get:

java.time.format.DateTimeParseException: Text '03.12.1990' could not
be parsed: Unable to obtain LocalDate from TemporalAccessor:
{Year=1990, DayOfMonth=3, MinuteOfHour=12},ISO of type
java.time.format.Parsed

This is because I used your format pattern string of dd.mm.yyyy, where lowercase mm means minute. When we read the error message closely enough, it does state that the DateTimeFormatter interpreted 12 as minute of hour, which was not what we intended. While SimpleDateFormat tacitly accepted this (even when strict), java.time is more helpful in pointing out our mistake. What the message only indirectly says is that it is missing a month value. We need to use uppercase MM for month. At the same time I am trying your date string with the typo:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
System.out.println(LocalDate.parse("03.12.199o", dateFormatter));

We get:

java.time.format.DateTimeParseException: Text '03.12.199o' could not
be parsed at index 6

Index 6 is where is says 199. It objects because we had specified 4 digits and are only supplying 3. The docs say:

The count of letters determines the minimum field width …

It would also object to unparsed text after the date. In short it seems to me that it gives you everything that you had expected.

Links

  • DateFormat.setLenient documentation
  • Oracle tutorial: Date Time explaining how to use java.time.

Lenient string matching with Mockito Mock

@Mock(lenient = true) was introduced in Mockito version 2.23.3.

You either have to upgrade or use the other way to write this:

lenient().when(specialtyRepository.save(argThat(argument -> argument.getDescription().equals(MATCH_ME)))).willReturn(savedSpeciality);

DateTimeFormatter - Strict vs Lenient unexpected behaviour

Having posted this as a bug - https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8228353

I got the following reply:

According to DateTimeFormatterBuilder's spec, appendPattern("yy") translates to appendValueReduced(ChronoField.YEAR_OF_ERA, 2, 2000), where "2" applies both for with and maxWidth. In the method description, it reads:

For strict parsing, the number of characters allowed by width to maxWidth are parsed. For lenient parsing, the number of characters must be at least 1 and less than 10.

Thus in this case, only "11" is read for "yy" in strict mode, then produces the year "2011" with the base value 2000. But in lenient mode, "yy" tries to read greedily just before the ":" and produces the year "1104", then parser throws the exception trying to parse ":" with "HH" pattern.

SimpleDateFormat Exception on setting lenient to false

Because that time does not exist in your default time zone—it was daylight savings time change day, and time jumped from 2:00 a.m. to 3:00 a.m., so there was no 2:46 that morning. :P

Since you’re parsing UTC, set the SimpleDateFormat instance time zone to UTC like so:

f.setTimeZone(TimeZone.getTimeZone("UTC"));

and your problem will go away.

Use Gson RuntimeTypeAdapterFactory with lenient = true setting

It is not quite a bug from gson-extras since com.google.gson.TypeAdapter class comes from core library. I would suggest to register one more TypeAdapterFactory and force lenient for all JsonReader instances:

class AlwaysLenientTypeAdapterFactory implements TypeAdapterFactory {

public final <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return createCustomTypeAdapter(delegate);
}

private <T> TypeAdapter<T> createCustomTypeAdapter(TypeAdapter<T> delegate) {
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}

@Override
public T read(JsonReader in) throws IOException {
in.setLenient(true);
return delegate.read(in);
}
};
}
}

Usage:

Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(adapterFactory)
.registerTypeAdapterFactory(new AlwaysLenientTypeAdapterFactory())
.create();

See also:

  • Is there an easy way to make Gson skip a field if there's an error
    deserializing it?


Related Topics



Leave a reply



Submit