将日期字符串(MM-dd)解析为默认年份的 java 日期

2022-01-11 00:00:00 date calendar date-format java

我想将 MM-dd 格式的字符串解析为 java 日期.由于未指定年份,因此解析日期应为当年.应该只解析有效的日期字符串,所以我应该在 SimpleDateFormat 中使用 setLenient(false).

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

在我通过参数02-29"之前,这似乎运作良好.今年(2012)是闰年,2012-02-29是有效日期,02-29"应该已经解析成功了.

我发现当我在SimpleDateFormat中不指定年份部分时,它会解析到1970年.而1970年不是闰年,02-29"无法解析.因此,解析到 1970 年的日期并在解析后设置当前年份的策略并不完美.

在 Java 中解析 MM-dd 格式字符串到日期(日期应设置为当前年份)的最佳方法是什么?

PS1:我搜索了这个话题,在本站找到了很多问题和答案,但都没有找到满意的答案.PS2:df.setLenient(false); 很重要,因为只有有效的日期字符串才能成功解析.不应解析01-32"、02-30"等无效日期字符串.

提前致谢.

解决方案

tl;dr

<块引用>

以 MM-dd 格式解析字符串……在当年

MonthDay//在为此目的设计的类中表示一个月的一天..parse (//默认情况下以标准 ISO 8601 格式解析字符串."--" + "02-29"//在前面加上双连字符以使此输入符合 ISO 8601.)//返回一个 `MonthDay` 对象..atYear(//获取指定年份本月日的日期.Year.now( ZoneId.of( "Asia/Tokyo" ) ).getValue()//获取当前年份需要时区.)//返回一个 `LocalDate` 对象,一个没有时区和没有时间的年月日.

查看此在 IdeOne.com 上运行的代码.

<块引用>

2019-02-28

java.time

现代解决方案使用 Java 8 及更高版本中内置的行业领先的 java.time 类,并具有适用于 Java 6 和更高版本的后端端口.7 和早期的 Android.

月日

月份与日期由适当命名的 MonthDay 类.

ISO 8601 中定义的月日标准格式是 --MM-DD,其中第一个破折号是年份的占位符.java.time 类中默认使用 ISO 8601 格式来解析/生成字符串.

您的输入几乎符合要求.您可以使用 DateTimeFormatter 对象定义格式模式.但我只会在输入前添加一个 --.

字符串输入=02-29";字符串 inputModified = "--" + 输入;

然后默认解析.

MonthDay md = MonthDay.parse(inputModified);

查看此在 IdeOne.com 上运行的代码.

<块引用>

md.toString(): --02-29

闰年

请注意,您的闰年问题消失了.通过使用真正代表月份和日期而不是时刻的适当类型,我们不必担心闰年.

要获取本月的日期,只需调用 MonthDay::atYear 获取 LocalDate 对象.通过年号.

LocalDate jumpYear2012 = md.atYear(2012);

<块引用>

leapYear2012.toString(): 2012-02-29

当年

在当年获得一个日期可能会让您感到惊讶.请注意,获取当前年份需要获取当前日期.获取当前日期需要时区.

时区对于确定日期至关重要.对于任何给定的时刻,日期在全球范围内因区域而异.例如,在 Paris France 午夜过后几分钟是新的一天,但仍然昨天"在 蒙特利尔魁北克.

如果没有指定时区,JVM 会隐式应用其当前的默认时区.该默认值可能 在运行时(!)期间随时更改,因此您的结果可能会有所不同.最好将您想要/预期的时区明确指定为参数.如果您想使用 JVM 当前的默认时区,请通过调用 ZoneId.systemDefault() 明确您的意图.如果关键,请与您的用户确认该区域.

大陆/地区,例如 America/MontrealAfrica/Cas​​ablancaPacific/Auckland.切勿使用 2-4 字母缩写,例如 ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的(!).

ZoneId z = ZoneId.of("美国/蒙特利尔");今天的 LocalDate = LocalDate.now( z ) ;

在我们的例子中,我们只关心年份.所以我们可以使用 Year 类而不是 LocalDate.但与时区相同的想法.如果当前时刻恰好是在新年前夜/新年前夜切换,那么全球各地的年份会因时区而异.

ZoneId z = ZoneId.of("非洲/突尼斯");年份 y = Year.now( z ) ;LocalDate currentYear = md.atYear( y.getValue() ) ;

<块引用>

currentYear.toString(): 2019-02-28

请注意,上述结果中的闰年是自动处理的.2019年没有2月29日,所以java.time调整为28日.

解析为 LocalDate

或者,您可以直接解析为 LocalDate.您需要使用 DateTimeFormatterBuilder 类来构建默认为特定年份的 DateTimeFormatter.

类似这样的:

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);

但我不建议这样做.MonthDay 对象的方法对于您的问题、解决方案和意图更加清晰.另一个好处:如果你得到这样的输入,我怀疑你可能需要像这样处理月日,并且使用 MonthDay 类你手头有一个对象来完成这项工作.

<小时>

关于java.time

java.time 框架内置于 Java 8 及更高版本中.这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.Date, 日历, &SimpleDateFormat.

要了解更多信息,请参阅 Oracle 教程.并在 Stack Overflow 上搜索许多示例和解释.规范是 JSR 310.

Joda-Time 项目,现在在 维护模式,建议迁移到 java.time 类.

您可以直接与您的数据库交换 java.time 对象.使用符合 JDBC 驱动程序/jeps/170" rel="nofollow noreferrer">JDBC 4.2 或更高版本.不需要字符串,不需要 java.sql.* 类.

从哪里获得 java.time 类?

  • Java SE 8,Java SE 9, Java SE 10, Java SE 11 及更高版本 - 具有捆绑实现的标准 Java API 的一部分.
    • Java 9 添加了一些小功能和修复.
  • Java SE 6 和 Java SE 7
    • 大部分 java.time 功能都向后移植到 Java 6 &7 在 ThreeTen-Backport.
  • Android
    • java.time 类的 Android 捆绑包实现的更高版本.
    • 对于早期的 Android (<26),ThreeTenABP 项目改编 ThreeTen-Backport(如上所述).请参阅如何使用 ThreeTenABP….

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.

解决方案

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….

相关文章