Hot questions for Using Joda-Time in date parsing

Top Java Programmings / Joda-Time / date parsing

Question:

I am expecting an exception to be thrown for following code and input:

SimpleDateFormat getDateTimeFormat(String requiredFormat)
{
    SimpleDateFormat dateTimeFormat = new SimpleDateFormat(requiredFormat);
    //this will make sure that if parsing fails exception is thrown
    dateTimeFormat.setLenient(false);
    return dateTimeFormat;
}
Date date = getDateTimeFormat("MM/dd/yyyy HH:mm").parse(estimatedDeliveryDttm);

Input: 10/25/16 17:46

I am expecting a parsing exception since year is not given as 'yyyy', but I am getting year as '0016' which I do not want. I cann't use JAVA 8. I already tried JAVA 7 (including parsing position approach) and JODA Date Time API.


Answer:

From the Javadocs of SimpleDateFormat

Year: If the formatter's Calendar is the Gregorian calendar, the following rules are applied.

For formatting, if the number of pattern letters is 2, the year is truncated to 2 digits; otherwise it is interpreted as a number.

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.

For parsing with the abbreviated year pattern ("y" or "yy"),SimpleDateFormat must interpret the abbreviated year relative to some century. It does this by adjusting dates to be within 80 years before and 20 years after the time the SimpleDateFormat instance is created. For example, using a pattern of "MM/dd/yy" and a SimpleDateFormat instance created on Jan 1, 1997, the string "01/11/12" would be interpreted as Jan 11, 2012 while the string "05/04/64" would be interpreted as May 4, 1964. During parsing, only strings consisting of exactly two digits, as defined by Character.isDigit(char), will be parsed into the default century. Any other numeric string, such as a one digit string, a three or more digit string, or a two digit string that isn't all digits (for example, "-1"), is interpreted literally. So "01/02/3" or "01/02/003" are parsed, using the same pattern, as Jan 2, 3 AD. Likewise, "01/02/-3" is parsed as Jan 2, 4 BC.

See the bold point above which explains the behavior you are seeing.

Question:

I am using Angular date picker to send my MVC controller a date, using a Javascript Date object which is an ISO date/time.

On deserializing java.util.Date it works like a charm and Hibernate will care about cutting that Datetime to a plain Date when inserting records.

But now that I am transitioning from java.util.Date to org.joda.time.[APPROPRIATE_CLASS_HERE] I am facing this deserialization issue.

It is my understanding that if I force DateTime in my DTOs Jackson will deserialize them correctly, while instead I prefer to drop time information when the target type is a Date.

E.g.

public class UserDto {

    private LocaLDate passwordExpirationDate;

}


{
   "username":"9493",
   "completeName":"ljdjf",
   "email":"wesf@dsgfds",
   "cultureId":"IT",
   "enabled":false,
   "passwordExpirationDate":"2017-07-13T10:00:00.000Z",
   "accountExpirationDate":"2017-07-20T10:00:00.000Z"
}

Instead I get this:

org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Invalid format: "2017-07-13T10:00:00.000Z" is malformed at "T10:00:00.000Z"; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Invalid format: "2017-07-13T10:00:00.000Z" is malformed at "T10:00:00.000Z" (through reference chain: it.phoenix.web.data.dtos.admin.profile.UserDTO["passwordExpirationDate"])
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:244) ~[spring-web-4.3.9.RELEASE.jar:4.3.9.RELEASE]
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Invalid format: "2017-07-13T10:00:00.000Z" is malformed at "T10:00:00.000Z" (through reference chain: it.phoenix.web.data.dtos.admin.profile.UserDTO["passwordExpirationDate"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:388) ~[jackson-databind-2.8.9.jar:2.8.9]
Caused by: java.lang.IllegalArgumentException: Invalid format: "2017-07-13T10:00:00.000Z" is malformed at "T10:00:00.000Z"
    at org.joda.time.format.DateTimeFormatter.parseLocalDateTime(DateTimeFormatter.java:900) ~[joda-time-2.9.9.jar:2.9.9]
    at org.joda.time.format.DateTimeFormatter.parseLocalDate(DateTimeFormatter.java:844) ~[joda-time-2.9.9.jar:2.9.9]
    at com.fasterxml.jackson.datatype.joda.deser.LocalDateDeserializer.deserialize(LocalDateDeserializer.java:39) ~[jackson-datatype-joda-2.8.9.jar:2.8.9]
    at com.fasterxml.jackson.datatype.joda.deser.LocalDateDeserializer.deserialize(LocalDateDeserializer.java:15) ~[jackson-datatype-joda-2.8.9.jar:2.8.9]
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:504) ~[jackson-databind-2.8.9.jar:2.8.9]
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:104) ~[jackson-databind-2.8.9.jar:2.8.9]
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:357) ~[jackson-databind-2.8.9.jar:2.8.9]
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:148) ~[jackson-databind-2.8.9.jar:2.8.9]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3814) ~[jackson-databind-2.8.9.jar:2.8.9]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2938) ~[jackson-databind-2.8.9.jar:2.8.9]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:241) ~[spring-web-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    ... 92 more

Question is: is there a smart way so that Jackson can decode DateTime object into a Joda LocalDate by simply stripping the time part at default/current time zone?

Notes: - I already have Jackson Joda module dependency - Jackson is 2.8.9 - I am forced to use Java 7. In a related Java 8 project I have no such problem with java.time stuff (and Jackson JSR310 module)


Answer:

According to the error message, the date/time input is coming as 2017-07-13T10:00:00.000Z and LocalDate by default can't handle it.

You can config this format by using the com.fasterxml.jackson.annotation.JsonFormat annotation in the LocalDate field:

@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private LocalDate passwordExpirationDate;

This will make Jackson parse the date correctly.

Question:

I need to convert "11/17" string date (November 2017) to "2017-11-01" (November 1, 2017).

What is the best way to achieve this in Java?

I tried:

String dayMonthYear = "11/17";
dayMonthYear = "01/" + dayMonthYear;

DateTimeFormatter formatter = DateTimeFormat.forPattern("dd/MM/yy");
DateTime dt = formatter.parseDateTime(dayMonthYear);
dt.year().setCopy(dt.getYear() + 2000);

DateTimeFormatter dtfOut = DateTimeFormat.forPattern("yyyy-MM-dd");
dayMonthYear = dtfOut.print(dt);
System.out.println(dayMonthYear); // "2017-11-01";

And:

SimpleDateFormat dateFormatTwoDigitsYear = new SimpleDateFormat(TWO_DIGITS_YEAR_DATE_FORMAT);  //"dd/MM/yy"
SimpleDateFormat dateFormatFourDigitsYear = new SimpleDateFormat(FOUR_DIGITS_YEAR_DATE_FORMAT);//  "yyyy-MM-dd"
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, -100);//??? I AM NOT SURE WHY DO I NEEED THIS??
dateFormatTwoDigitsYear.set2DigitYearStart(calendar.getTime());
try
{
    dayMonthYear = dateFormatFourDigitsYear.format(dateFormatTwoDigitsYear.parse(dayMonthYear));
}
catch (ParseException e)
{
    log.error("Error while formatting date in yyyy-MM-dd format. Date is " + dayMonthYear);
}

I am not sure why I need this line in second appproach:

calendar.add(Calendar.YEAR, -100);

Both of them are working. But I am not sure if there is better solution.


Answer:

You don't need to add values to the year. The 2 digit value (17) is automatically ajusted to 2017. Also, there's no need to append day 1 in the input. When the day is not present, SimpleDateFormat automatically sets to 1:

// input format: MM/yy
SimpleDateFormat parser = new SimpleDateFormat("MM/yy");
// output format: yyyy-MM-dd
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(formatter.format(parser.parse("11/17"))); // 2017-11-01

PS: if you need to set to another day (other than 1), you can set the date to a Calendar and change it before formatting:

// parse the date
Date date = parser.parse("11/17");
// set to a Calendar
Calendar cal = Calendar.getInstance();
cal.setTime(date);
// change to whatever day you want
cal.set(Calendar.DAY_OF_MONTH, whateverDayIwant);
// format it
System.out.println(formatter.format(cal.getTime()));

Joda-Time

The old classes (Date, Calendar and SimpleDateFormat) have lots of problems and design issues, and they're being replaced by the new APIs.

I've seen you're using Joda-Time, so here's how to do it with this API.

In Joda-Time, when you parse to a DateTime, it sets default values for all the missing fields (in this case, day, hour, minute, etc). But this API has lots of other types that can suit best for each use case.

As the input has only month and year, you can parse it to a org.joda.time.YearMonth and then set the day to 1. This will create a org.joda.time.LocalDate, which can be printed directly (as the toString() method already returns the date in the format you want):

DateTimeFormatter parser = DateTimeFormat.forPattern("MM/yy");
// parse to YearMonth
YearMonth ym = YearMonth.parse("11/17", parser);
// set day to 1
System.out.println(ym.toLocalDate(1)); // 2017-11-01

I prefer this approach because you can set it to whatever day you want, and don't need to create a full DateTime object with fields that you don't need/care about (such as hour, minutes, etc).

If you need to store this value in a String, just call the toString() method:

// store "2017-11-01" in a String
String output = ym.toLocalDate(1).toString();

The format yyyy-MM-dd is the default used by toString(). If you need a different format, you can pass it to toString():

// convert to another format (example: dd/MM/yyyy)
String output = ym.toLocalDate(1).toString("dd/MM/yyyy"); // 01/11/2017

Or you can use another DateTimeFormatter:

// convert to another format (example: dd/MM/yyyy)
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd/MM/yyyy");
String output = fmt.print(ym.toLocalDate(1)); // 01/11/2017

PS: dt.year().setCopy returns a new object, and as you don't assign it to any variable, this value is lost - this method doesn't change the original DateTime object, so dt is not changed by its line of code.


Java new date/time API

Joda-Time is in maintainance mode and is being replaced by the new APIs, so I don't recommend start a new project with it. Even in joda's website it says: "Note that Joda-Time is considered to be a largely "finished" project. No major enhancements are planned. If using Java SE 8, please migrate to java.time (JSR-310).".

If you can't (or don't want to) migrate from Joda-Time to the new API, you can ignore this section.

If you're using Java 8, consider using the new java.time API. It's easier, less bugged and less error-prone than the old APIs.

If you're using Java 6 or 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. And for Android, you'll also need the ThreeTenABP (more on how to use it here).

The code below works for both. The only difference is the package names (in Java 8 is java.time and in ThreeTen Backport (or Android's ThreeTenABP) is org.threeten.bp), but the classes and methods names are the same.

The code is very similar to Joda-Time (the API's are not exactly the same, but they have lots of similarities). You can use a Java 8's java.time.format.DateTimeFormatter and parse the input to a java.time.YearMonth (or, in Java 7's ThreeTen Backport, use a org.threeten.bp.format.DateTimeFormatter and parse to a org.threeten.bp.YearMonth):

DateTimeFormatter parser = DateTimeFormatter.ofPattern("MM/yy");
YearMonth ym = YearMonth.parse("11/17", parser);
System.out.println(ym.atDay(1)); // 2017-11-01

If you need to store this value in a String, just call the toString() method:

// store "2017-11-01" in a String
String output = ym.atDay(1).toString();

The format yyyy-MM-dd is the default used by toString(). If you need a different format, you can use another DateTimeFormatter:

// convert to another format (example: dd/MM/yyyy)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String output = fmt.format(ym.atDay(1)); // 01/11/2017

Question:

What is the Java8 java.time equivalent of

org.joda.time.formatDateTimeFormat.shortDate()

I've tried below way, but it fails to parse values such as "20/5/2016" or "20/5/16".

DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) 

Answer:

You are correct: A Joda-Time DateTimeFormatter (which is the type you get from DateTimeFormat.shortDate()) parses more leniently than a java.time DateTimeFormatter. In the English/New Zealand locale (en-NZ) shortDate uses the format pattern d/MM/yy and parses both 20/5/2016 and 20/5/16 into 2016-05-20.

I frankly find it nasty that it interprets both two-digit and four-digit years into the same year. When the format specifies two-digit year, I would have expected four digits to be an error for stricter input validation. Accepting one-digit month when the format specifies two digits is lenient too, but maybe not so dangerous and more in line with what we might expect.

java.time too uses the format pattern d/MM/yy (tested on jdk-11.0.3). When parsing is accepts one or two digits for day of month, but insist on two-digit month and two-digit year.

You may get the Joda-Time behaviour in java.time, but it requires you to specify the format pattern yourself:

    Locale loc = Locale.forLanguageTag("en-NZ");
    DateTimeFormatter dateFormatter
            = DateTimeFormatter.ofPattern("d/M/[yyyy][yy]", loc);

    System.out.println(LocalDate.parse("20/5/2016", dateFormatter));
    System.out.println(LocalDate.parse("20/5/16", dateFormatter));

Output is:

2016-05-20
2016-05-20

If you want an advanced solution that works in other locales, I am sure that you can write a piece of code that gets the format pattern from DateTimeFormatterBuilder.getLocalizedDateTimePattern and modifies it by replacing dd with d, MM with M and any number of y with [yyyy][yy]. Then pass the modified format pattern string to DateTimeFormatter.ofPattern.

Edit: I’m glad that you got something to work. In your comment you said that you used:

    Stream<String> shortFormPatterns = Stream.of(
            "[d][dd]/[M][MM]",
            "[d][dd]-[M][MM]",
            "[d][dd].[M][MM]",
            "[d][dd] [M][MM]",
            "[d][dd]/[M][MM]/[yyyy][yy]",
            "[d][dd]-[M][MM]-[yyyy][yy]",
            "[d][dd].[M][MM].[yyyy][yy]",
            "[d][dd] [M][MM] [yyyy][yy]");
  1. It covers more cases that your Joda-Time formatter. Maybe that’s good. Specifically your Joda-Time formatter insists on a slash / between the numbers and rejects either hyphen, dot or space. Also I believe that Joda-Time would object to the year being left out completely.
  2. While you do need [yyyy][yy], you don’t need [d][dd] nor [M][MM]. Just d and M suffice since they also accept two digits (what happens in your code is that for example [d] parses either one or two digits, so [dd] is never used anyway).
  3. If you prefer only one format pattern string, I would expect d[/][-][.][ ]M[/][-][.][ ][yyyy][yy] to work (except in hte cases where the year is omitted) (I haven’t tested).

Question:

Is Joda parsing this ISO 8601 datetime string incorrectly?

DateTimeFormatter f = DateTimeFormat.forPattern("EEE MMM ee kk:mm:ss z y").withZone(DateTimeZone.UTC).withLocale(Locale.US);    
DateTimeFormatter dtf = ISODateTimeFormat.dateTimeNoMillis();
dt = dtf.parseDateTime("2015-04-01T11:40:59Z");
System.out.println(f.print(dt));

This outputs Wed Apr 03 11:40:59 UTC 2015 but notice the input string specified April 01


Answer:

e is the day of the week. You're looking for d, which is the day of the month:

DateTimeFormatter f = DateTimeFormat.forPattern("EEE MMM dd kk:mm:ss z y").withZone(DateTimeZone.UTC).withLocale(Locale.US);