Generate & parse “Year-Month” values in text from

2020-02-16 05:42发布

问题:

I have an HTML popup menu listing various year-month combinations. Each menu item has a localized string representation such as Nov 2015 for content display, along with an attribute of a coded value such as 201511.

For example:

<select name="selectPeriod">
    <option value="201511">Nov 2015</option>
    <option value="201510">Oct 2015</option>
    <option value="201509">Sept 2015</option>

I generate the coded value string using java.util.Calendar.

int month = Calendar.getInstance().get(Calendar.MONTH)+1;
int year = Calendar.getInstance().get(Calendar.YEAR);
String v = String.valueOf( year ) + String.valueOf( month );

This works, but it seems weird to be using Calendar (a date plus a time-of-day) when all I really care about is year + month without a date or time-of-day.

Is there some better way in modern Java to handle a year + month?

回答1:

Indeed there is a more elegant way to handle YearMonth in modern Java, with the java.time framework built into Java 8 and later.

Also your example code suffers from a couple other problems:

  • Getting current moment twice.
    Getting the current moment twice, redundantly, is a bad practice. If those two lines of code happen to occur around the stroke of midnight, you will be working with two different dates and possibly two different months or even years!
  • Ignoring the issue of time zone.
    Discussed below.

java.time

The old java.util.Calendar/.Date classes are notoriously troublesome and should be avoided. They have been supplanted in Java 8 and later by the java.time framework.

The new classes are inspired by the highly successful Joda-Time framework, intended as its successor, similar in concept but re-architected. Defined by JSR 310. Extended by the ThreeTen-Extra project. See the Tutorial.

YearMonth

The new classes include a class, YearMonth, exactly for your purpose: To represent a year+month without any date, time-of-day, or time zone.

I recommend using objects of this class in your business logic, resorting to strings only where necessary such as generating that HTML.

Time Zone

While no time zone is included inside a YearMonth, note that a time zone is crucial in determining the current YearMonth. The current date, and therefore YearMonth, varies around the world at any moment. A new day dawns in the east such as Paris earlier than in the west such as Montréal where it is still “yesterday”.

If the time zone is omitted as in your example code, the JVM’s current default time zone is implicitly used. This implicit use means your results may vary for any of several reasons: Your code may move to another machine, a sysadmin may change the host machine’s default, or even worse, any code in any thread of any app running within the same JVM can change the time zone at runtime. Better to specify the desired/expected time zone (ZoneId in java.time).

ZoneId zoneId = ZoneId.of( "America/Montreal" );
YearMonth yearMonth = YearMonth.now( zoneId );

String Formats

The toString method by default in java.time classes use the standard ISO 8601 formats. Your format, YYYYMM is similar to the standard formats but in this particular case (year-month) a hyphen is required to avoid ambiguity, YYYY-MM. Most of the standard formats allow a "basic" variation omitting the hyphens, but not here.

I strongly suggest making your String representations of date-time values in ISO 8601 format whenever possible.

String output = yearMonth.toString ();

2015-12

I strongly suggest using that format in your HTML attribute. Sticking with ISO 8601 will simplify your life. Like this (note the hyphen):

<option value="2015-11">Nov 2015</option>

But if you insist, you could build the string without the hyphen by specifying a custom formatter pattern.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern ( "yyyyMM" );
String output = yearMonth.format ( formatter );

201512

Localized Strings

Let java.time do the work of localizing the display text. Notice the Locale passed to the DateTimeFormatter rather than relying implicitly on the JVM’s current default Locale.

Locale locale = Locale.CANADA_FRENCH;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern ( "MMM yyyy" , locale );
String output = yearMonth.format ( formatter );

déc. 2015

Date

If you need a specific date, specify a day-of-month to combine with this year and month.

LocalDate ld = ym.atDay( 1 ) ;  // Get first day of that year-month. 

From there you can get a specific moment, such as the first moment of the day. Do not assume the day starts at 00:00:00. Let java.time determine the first moment.

ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdt = ld.atStartOfDay( z ) ;

If you need same moment viewed in UTC, extract an Instant.

Instant instant = zdt.toInstant() ;