Parsing String to Local Date Doesn't Use Desired Century

Parsing string to local date doesn't use desired century

Since this question is really about new java.time-package and NOT SimpleDateFormat I will cite following relevant section:

Year: The count of letters determines the minimum field width below
which padding is used. If the count of letters is two, then a reduced
two digit form is used. For printing, this outputs the rightmost two
digits. For parsing, this will parse using the base value of 2000,
resulting in a year within the range 2000 to 2099 inclusive.

We see that Java-8 uses the range 2000-2099 per default, not like SimpleDateFormat the range -80 years until +20 years relative to today.

If you want to configure it then you have to use appendValueReduced(). This is designed in an inconvenient way, but possible, see here:

String s = "150790";

// old code with base range 2000-2099
DateTimeFormatter dtf1 =
new DateTimeFormatterBuilder().appendPattern("ddMMyy").toFormatter();
System.out.println(dtf1.parse(s)); // 2090-07-15

// improved code with base range 1935-2034
DateTimeFormatter dtf2 =
new DateTimeFormatterBuilder().appendPattern("ddMM")
.appendValueReduced(
ChronoField.YEAR, 2, 2, Year.now().getValue() - 80
).toFormatter();
System.out.println(dtf2.parse(s)); // 1990-07-15

By the way, if you really want week-based years then you have to use Y instead of y or the appropriate field IsoFields.WEEK_BASED_YEAR. Regarding the fact that you don't have any other week-related fields I would assume the normal calendar year, not the week-based one.

How to force java.time.LocalDate to assume 19th century as YY year pattern?

The base year can be controlled using DateTimeFormatterBuilder.appendValueReduced().

This code would parse with a base date of 1900 rather than 2000:

DateTimeFormatter f = new DateTimeFormatterBuilder()
.appendPattern("ddMM")
.appendValueReduced(ChronoField.YEAR, 2, 2, 1900)
.toFormatter();
LocalDate date = LocalDate.parse("070886", f);

How to change the base date for parsing two letter years with Java 8 DateTimeFormatter?

You can create a custom formatter, for example for the d/M/yy pattern:

new DateTimeFormatterBuilder()
.appendPattern("d/M/")
.appendValueReduced(ChronoField.YEAR_OF_ERA, 2, 2, LocalDate.now().minusYears(80))

Example usage:

public static void main(String[] args) throws Exception {
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
.appendPattern("d/M/")
.appendValueReduced(ChronoField.YEAR_OF_ERA, 2, 2, LocalDate.now().minusYears(80))
.toFormatter();
parse("13/5/99", fmt);
parse("13/5/36", fmt);
parse("13/5/35", fmt);
parse("13/5/34", fmt);
parse("13/5/33", fmt);
}

private static void parse(String date, DateTimeFormatter fmt) {
System.out.println(date + " = " + LocalDate.parse(date, fmt));
}

which prints:

13/5/99 = 1999-05-13

13/5/36 = 1936-05-13

13/5/35 = 1935-05-13

13/5/34 = 2034-05-13

13/5/33 = 2033-05-13

Java 8 DateTimeFormatter two digit year 18 parsed to 0018 instead of 2018?

There is not a single string of y or u that will allow you to parse both two and four digit years. However, you may use optional parts in the format pattern string to specify that a two or four digit year may be present:

public static LocalDate parseDateString(CharSequence date) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/[uuuu][uu]");
return LocalDate.parse(date, formatter);
}

Try it:

    System.out.println(parseDateString("01/05/18"));
System.out.println(parseDateString("01/06/2018"));

This printed:

2018-01-05
2018-01-06

In the format pattern string you need to put the four digit year first. With the opposite order, when trying to parse a four digit year, the formatter will parse two digits, decide it was successful this far, and then complain about unparsed text after the two digits.

If you want more precise control over how two digit years are interpreted:

    DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendPattern("M/d/")
.optionalStart()
.appendPattern("uuuu")
.optionalEnd()
.optionalStart()
.appendValueReduced(ChronoField.YEAR, 2, 2, 1920)
.optionalEnd()
.toFormatter();

Using this formatter in the above method let’s try:

    System.out.println(parseDateString("01/05/22"));

This prints:

1922-01-05

Giving 1920 as base (as in my example code) will cause two digit years to end up in the interval from 1920 through 2019. Adjust the value to your requirements.

Parsing 2-digit years: Setting the pivot date with an unknown date pattern

Here the Java-8-solution (it rather looks like a hack):

String pattern = "MM/dd/yy 'US'"; // user-input
String text = "10/04/69 US"; // user-input
Locale locale = Locale.US; // user-input, too?

int yy = pattern.indexOf("yy");
DateTimeFormatter dtf;

if (
(yy != -1) // explanation: condition ensures exactly two letters y
&& ((yy + 2 >= pattern.length()) || pattern.charAt(yy + 2) != 'y')
) {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
String part1 = pattern.substring(0, yy);
if (!part1.isEmpty()) {
builder.appendPattern(part1);
}
builder.appendValueReduced(ChronoField.YEAR, 2, 2, 1970);
String part2 = pattern.substring(yy + 2);
if (!part2.isEmpty()) {
builder.appendPattern(part2);
}
dtf = builder.toFormatter(locale);
} else {
dtf = DateTimeFormatter.ofPattern(pattern, locale);
}

LocalDate ld = LocalDate.parse(text, dtf);
System.out.println("user-date: " + ld); // 2069-10-04

There is only one tiny caveat: If any user gets the crazy idea to define two times separately "yy" in the pattern then the proposed solution will fail. The correct solution would then require some kind of iterating over the pattern chars but I think that is overkill, so I leave it out here.

Just for comparison, using my external library Time4J enables following short solution because its parse engine also has the concept of setting any suitable format attributes:

LocalDate ld = 
ChronoFormatter.ofDatePattern(pattern, PatternType.CLDR, locale)
.with(Attributes.PIVOT_YEAR, 2070).parse(text).toTemporalAccessor();

Year 2038 issue still happens with Jackson of date deserialization in Java 8

While I don't know about Jackson, i can tell you that you've taken a poor approach to handling this credit card expiration data.

Firstly you chose to roll-your-own rather than look to existing classes and standards. Always search for prior work; roll-your-own approach should be a last resort, not first. Less wild-cowboy, more judge-guided-by-precedent.

Secondly, you are using troublesome old date-time classes that are legacy, now supplanted by the java.time classes.

YearMonth

To represent a year and a month, use the YearMonth class built into Java. The months are numbered sanely, 1-12 for January-December, unlike the legacy classes.

YearMonth ym = YearMonth.of( 2040 , 3 ) ; 

By the way, you may find the Month enum handy in your work.

YearMonth ym = YearMonth.of( 2040 , Month.MARCH ) ; 

Use this YearMonth class as your member rather than a specific date.

When comparing to a date such as today, get the YearMonth for that date.

ZoneId z = ZoneId.of( "Asia/Amman" ) ;
LocalDate today = LocalDate.now( z ) ;
YearMonth ymToday = YearMonth.from( today ) ;

Boolean isExpired = ymToday.isAfter( ym ) ;

ISO 8601

The ISO 8601 standard defines many practical sensible formats for representing date-time values as text.

The standard format for year-month is YYYY-MM. So March 2040 is 2040-03.

The java.time classes use the standard formats by default when parsing or generating strings.

Generate a string.

ym.toString()  

2040-03

Parse a string.

YearMonth ym = YearMonth.parse( "2040-03" );

Always use 4-digit years

For both storage and presentation, always use four digits for years. The endless confusion, errors, and ambiguity is not worth saving two octets of memory/storage or half a centimeter of space on paper.

How to convert two digit year to full year using Java 8 time API

The problem is that you can't parse a year on its own into a LocalDate. A LocalDate needs more information than that.

You can use the parse method of the formatter, which will give you a TemporalAccessor, and then get the year field from that:

int year = TWO_YEAR_FORMATTER.parse("99").get(ChronoField.YEAR);
System.out.println(year);

Addressing the discrepancy between the two: these are two distinct APIs. Yes, they are very similar, and the java.time package was informed by design decisions of JodaTime, but it was never intended to be a drop-in replacement for it.

See this answer if you would like to change the pivot year (by default '99' will resolve to 2099 and not 1999).



Related Topics



Leave a reply



Submit