Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parsing date string (MM-dd) to java date in default year

I'd like to parse string in MM-dd format to java date. Since year is not specified, parsed date should be in current year. Only valid date string should be parsed, so I should use setLenient(false) in SimpleDateFormat.

public static Date parseDate(String ds) throws ParseException {
    SimpleDateFormat df = new SimpleDateFormat("MM-dd");
    df.setLenient(false);
    Date d = df.parse(ds);
    Calendar cal = Calendar.getInstance();
    int year = cal.get(Calendar.YEAR);
    cal.setTime(d);
    cal.set(Calendar.YEAR, year);
    return cal.getTime();
}

This seems to work well until I pass an argument "02-29". This year(2012) is leap year and 2012-02-29 is valid date, "02-29" should have been parsed successfully.

I found that when I don't specify year part in SimpleDateFormat, it parse to year 1970. And 1970 is not a leap year, "02-29" fails to parse. So, parsing to date of year 1970 and set current year after parsing strategy is not perfect.

What is the best way to parse MM-dd format string to date (date should be set to current year) in Java?

PS1: I searched this topic and found many questions and answers in this site, but I couldn't find the satisfactory answer. PS2: df.setLenient(false); is important because only valid date string should be parsed successfully. Invalid date strings like "01-32", "02-30", etc. shouldn't be parsed.

Thanks in advance.

like image 407
ntalbs Avatar asked Dec 27 '22 21:12

ntalbs


1 Answers

tl;dr

parse string in MM-dd format … in current year

MonthDay               // Represent a month-day as such, in a class designed for that purpose.
.parse (               // By default parses strings in standard ISO 8601 format.
    "--" + "02-29"     // Prepending a double-hyphen to make this input comply with ISO 8601.
)                      // Returns a `MonthDay` object.
.atYear(               // Get the date of this month-day in a specified year.
    Year.now( ZoneId.of( "Asia/Tokyo" ) ).getValue()  // Getting current year requires a time zone.
)                      // Returns a `LocalDate` object, a year-month-day without time zone and without time-of-day. 

See this code run live at IdeOne.com.

2019-02-28

java.time

The modern solution uses the industry-leading java.time classes built into Java 8 and later, with a back-port available for Java 6 & 7 and early Android.

MonthDay

A month-with-day is represented by the appropriately-named MonthDay class.

The standard format for a month-day defined in ISO 8601 is a --MM-DD where the first dash is a placeholder for year. The ISO 8601 formats are used by default in the java.time classes for parsing/generating strings.

Your input nearly complies. You could define a formatting pattern with a DateTimeFormatter object. But I would just prepend a -- onto the input.

String input = "02-29" ;
String inputModified = "--" + input ;

And then parse by default.

MonthDay md = MonthDay.parse( inputModified ) ;

See this code run live at IdeOne.com.

md.toString(): --02-29

Leap year

Note that your leap year problem goes away. By use an appropriate type that truly represents a month-and-day instead of a moment, we need not worry about leap year.

To get a date for this month-day, simply call MonthDay::atYear to obtain a LocalDate object. Pass a year number.

LocalDate leapYear2012 = md.atYear( 2012 ) ;

leapYear2012.toString(): 2012-02-29

Current year

Getting a date in the current year has a twist that may be surprising to you. Note that getting the current year requires getting the current date. And getting the current date requires a time zone.

A time zone is crucial in determining a date. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

If no time zone is specified, the JVM implicitly applies its current default time zone. That default may change at any moment during runtime(!), so your results may vary. Better to specify your desired/expected time zone explicitly as an argument. If you want to use the JVM’s current default time zone, make your intention clear by calling ZoneId.systemDefault(). If critical, confirm the zone with your user.

Specify a proper time zone name in the format of Continent/Region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "America/Montreal" ) ;  
LocalDate today = LocalDate.now( z ) ;

In our case, we care only about the year. So we can use the Year class rather than LocalDate. But same idea with the time zone. If the current moment happens to be around New Years Eve/Day cutover, the year will vary around the globe by time zone.

ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
Year y = Year.now( z ) ;
LocalDate currentYear = md.atYear( y.getValue() ) ;

currentYear.toString(): 2019-02-28

Notice in the result above that leap year is handled automatically. There is no February 29th in 2019, so java.time adjusted to the 28th.

Parse as LocalDate

Alternatively, you could parse directly into a LocalDate. You would need to use the DateTimeFormatterBuilder class to build a DateTimeFormatter that defaults to a certain year.

Something like this:

ZoneId zKolkata = ZoneId.of( "Asia/Kolkata" ) ;
long yearNumber = Year.now( zKolkata ).getValue() ;
DateTimeFormatter formatter = new DateTimeFormatterBuilder().parseDefaulting( ChronoField.YEAR , yearNumber ).appendPattern( "MM-dd").toFormatter() ;
LocalDate ld = LocalDate.parse( "02-28" , formatter ) ;
System.out.println( "ld.toString(): " + ld ) ;

But I do not recommend this. The approach with MonthDay object is much more clear as to your problem, solution, and intention. Another benefit: if you are getting such inputs, I suspect you will likely need to be working with the month-day as such, and with MonthDay class you have an object at hand to do the job.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

  • Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
    • Java 9 adds some minor features and fixes.
  • Java SE 6 and Java SE 7
    • Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
  • Android
    • Later versions of Android bundle implementations of the java.time classes.
    • For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
like image 143
Basil Bourque Avatar answered Jan 13 '23 00:01

Basil Bourque