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

4

4

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.

Kobe answered 15/7, 2012 at 16:26 Comment(3)
You could use "yyyy-MM-dd" and add the actual year in the String before parsing it.Ardie
Could you explain why you just unaccepted my answer?Halford
I recommend you don’t use SimpleDateFormat, Date and Calendar. Those classes are poorly designed and long outdated, the first in particular notoriously troublesome. Instead use LocalDate, DateTimeFormatterBuilder and DateTimeFormatter, all from java.time, the modern Java date and time API.Aprilette
M
4

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?

Matney answered 30/10, 2019 at 23:13 Comment(2)
Thanks for answering this old (outdated) question. At the time Java8 wasn't available but could have used JodaTime. And MonthDay... I didn't know this class. Thanks again.Kobe
@Kobe Remember, these Answers are for posterity, for the three thousand other people reading this page later, not for you personally. Stack Overflow is meant to be more like Wikipedia, less like a chatroom. As for MonthDay, yes java.time has many surprisingly handy classes - and even more in the ThreeTen-Extra project led by the same man, Stephen Colebourne (he also led Joda-Time).Matney
H
4

This could be considered a little hacky, but you could always just tack the year onto the end of the date string before parsing, like this:

ds += "-" + Calendar.getInstance().get(Calendar.YEAR);
SimpleDateFormat df = new SimpleDateFormat("MM-dd-yyyy");
// Parse date as usual
Halford answered 15/7, 2012 at 16:32 Comment(1)
This doesn't quite answer the question for a situationally important reason: If you do not know whether there is a year on the input String. Before you suggest that you can check for that first: in an even more situational case, you might not even know what format the input date is in. Let's say you had N formatters that try and parse an input String for various known date formats, it'd be nice if you could set a default date on the parser to handle those without years provided.Jaworski
M
4

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?

Matney answered 30/10, 2019 at 23:13 Comment(2)
Thanks for answering this old (outdated) question. At the time Java8 wasn't available but could have used JodaTime. And MonthDay... I didn't know this class. Thanks again.Kobe
@Kobe Remember, these Answers are for posterity, for the three thousand other people reading this page later, not for you personally. Stack Overflow is meant to be more like Wikipedia, less like a chatroom. As for MonthDay, yes java.time has many surprisingly handy classes - and even more in the ThreeTen-Extra project led by the same man, Stephen Colebourne (he also led Joda-Time).Matney
D
2

Get the year from the calendar as you do in the code, set the parse format string to MM-dd-yyyy and then do

 Date d = df.parse(ds + "-" + year);
Dinothere answered 15/7, 2012 at 16:33 Comment(0)
J
0

If you know your Strings are in the correct format, the other answers about appending the current year should suffice.

If your need is to handle input strings of unknown formats (you are unsure if the year is on the String or not), you can first attempt to parse the Date using the full format, then fall back on the shorter format with an override for the year.

public static Date parseDate(String ds) throws ParseException {
    SimpleDateFormat fullFormat = new SimpleDateFormat("MM-dd-yyyy");
    fullFormat.setLenient(false);
    try {
      return fullFormat.parse(ds);
    } catch (ParseException e) {}

    // Full format unsuccessful. Attempt short format.
    SimpleDateFormat shortFormat = new SimpleDateFormat("MM-dd");
    shortFormat.setLenient(false);
    Date d = shortFormat.parse(ds);
    Calendar cal = Calendar.getInstance();
    int year = cal.get(Calendar.YEAR);
    cal.setTime(d);
    cal.set(Calendar.YEAR, year);
    return cal.getTime();
}

Bonus: If you want a "catch-all" parser for some reason, define a bunch of non-lenient date formats and check them one by one. Note that order matters; the first to match will return. If you want to set the default year, you'll have to take it a step further and check if the default 1970 year is somehow referred in the input:

public static Date parseDate(String ds) throws ParseException {
  Calendar cal = Calendar.getInstance();
  int currentYear = cal.get(Calendar.YEAR);

  for (DateFormat knownFormat : knownFormats) {
    try {
      Date d = knownFormat.parse(ds);
      cal.setTime(d);

      if (cal.get(Calendar.YEAR) == 1970 && !ds.contains("70")) {
        cal.set(Calendar.YEAR, currentYear);
      }

      return cal.getTime();
    } catch (ParseException e) {}
  }

  throw new ParseException("Unknown date format for String: " + ds);
}
Jaworski answered 30/10, 2019 at 21:26 Comment(3)
Thank you for wanting to contribute. This question was asked in 2012, and back than using Date , SimpleDateFormat and Calendar was reasonable. Posting an answer using those classes in 2019 is leading astray. We have so much better in java.time, the modern Java date and time API.Aprilette
It's hardly misleading, since there are plenty of codebases still on JDK7. Providing modern alternatives is fine, but historic answers aren't leading folx astray.Jaworski
Thanks for your reply. Opinions differ. If you had stated in your answer that it was for Java 4 (and up to 7 for folks who hate all kinds of external dependencies) I would have been able to see your point. My opinion stays what I wrote earlier.Aprilette

© 2022 - 2024 — McMap. All rights reserved.