14

How can I best convert a java.util.Date to a Java 8 java.time.YearMonth?

Unfortunately the following throws a DateTimeException:

YearMonth yearMonth = YearMonth.from(date.toInstant());

results in:

java.time.DateTimeException: Unable to obtain YearMonth from TemporalAccessor: 2015-01-08T14:28:39.183Z of type java.time.Instant
at java.time.YearMonth.from(YearMonth.java:264)
...

I need this functionality since I want to store YearMonth values in a database using JPA. Currently JPA does not support YearMonth's, so I've come up with the following YearMonthConverter (imports omitted):

// TODO (future): delete when next version of JPA (i.e. Java 9?) supports YearMonth. See https://java.net/jira/browse/JPA_SPEC-63
@Converter(autoApply = true)
public class YearMonthConverter implements AttributeConverter<YearMonth, Date> {

  @Override
  public Date convertToDatabaseColumn(YearMonth attribute) {
    // uses default zone since in the end only dates are needed
    return attribute == null ? null : Date.from(attribute.atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
  }

  @Override
  public YearMonth convertToEntityAttribute(Date dbData) {
    // TODO: check if Date -> YearMonth can't be done in a better way
    if (dbData == null) return null;
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(dbData);
    return YearMonth.of(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1);
  }
}

Isn't there a better (cleaner, shorter) solution (for both directions)?

wassgren
  • 16,439
  • 5
  • 53
  • 71
Sebastian
  • 744
  • 1
  • 7
  • 19
  • 1
    Personally I would avoid mapping to `java.util.Date` because a) it is not `java.sql.*`-type, b) includes implicit timezone (bad performance and fuzzy use of system timezone), c) SQL does not define such a type for year-month. Better map `YearMonth` to integer using [PROLEPTIC_MONTH](http://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoField.html#PROLEPTIC_MONTH) – Meno Hochschild Jan 08 '15 at 16:48
  • @MenoHochschild: Thats a good idea - only caveat is that it is not readable when looking at the database itself. No problem in my case... – Sebastian Jan 08 '15 at 18:02
  • Well, just an integer is not readable for direct view of db content, but be aware, mapping to j.u.Date can change the visualized date and month (as an db administrator sees it) due to timezone conversions and db/server-settings. – Meno Hochschild Jan 08 '15 at 18:52
  • I went for that solution - does not answer my original question, but helps me in my special situation... public Integer convertToDatabaseColumn(YearMonth yearMonth) { return (yearMonth == null) ? null : (int) yearMonth.getLong(ChronoField.PROLEPTIC_MONTH); } public YearMonth convertToEntityAttribute(Integer dbData) { return (dbData == null) ? null : YearMonth.from(YearMonth.now().with(ChronoField.PROLEPTIC_MONTH, dbData)); } – Sebastian Jan 08 '15 at 21:15

1 Answers1

20

Short answer:

// From Date to YearMonth
YearMonth yearMonth =
        YearMonth.from(date.toInstant()
                           .atZone(ZoneId.systemDefault())
                           .toLocalDate());

// From YearMonth to Date
// The same as the OP:s answer
final Date convertedFromYearMonth = 
        Date.from(yearMonth.atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant());

Explanation:

The JavaDoc of the YearMonth.from(TemporalAccessor)-method says:

The conversion extracts the YEAR and MONTH_OF_YEAR fields. The extraction is only permitted if the temporal object has an ISO chronology, or can be converted to a LocalDate.

So, you need to either be able to:

  1. extract the YEAR and MONTH_OF_YEAR fields, or
  2. you should use something that can be converted to a LocalDate.

Lets try it!

final Date date = new Date();
final Instant instant = date.toInstant();
instant.get(ChronoField.YEAR); // causes an error

This is not possible, an exception is thrown:

java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: Year at java.time.Instant.get(Instant.java:571) ...

This means that alternative 1 goes out the window. The reason for is explained in this excellent answer about how to convert Date to LocalDate.

Despite its name, java.util.Date represents an instant on the time-line, not a "date". The actual data stored within the object is a long count of milliseconds since 1970-01-01T00:00Z (midnight at the start of 1970 GMT/UTC).

The equivalent class to java.util.Date in JSR-310 is Instant, thus there is a convenient method toInstant() to provide the conversion.

So, a Date can be converted to an Instant but that did not help us, did it?

Alternative 2 however proves to be successful. Convert the Instant to a LocalDate and then use the YearMonth.from(TemporalAccessor)-method.

    Date date = new Date();

    LocalDate localDate = date.toInstant()
                              .atZone(ZoneId.systemDefault())
                              .toLocalDate();

    YearMonth yearMonth = YearMonth.from(localDate);
    System.out.println("YearMonth: " + yearMonth);

The output is (since the code was executed in January 2015 ;):

YearMonth: 2015-01

Community
  • 1
  • 1
wassgren
  • 16,439
  • 5
  • 53
  • 71