3

While writing unit tests for some date helpers i stumbled across a particular behaviour of DateTimeFormatter that i would like to understand how to get around.

When outputting years >9999, it always adds a plus sign in front of the year number. Some quick code to illustrate this:

LocalDate localDate = LocalDate.of(9999, 1, 1);
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
cal.set(9999, 0, 1, 12, 0 , 0);

// following assertion passes as both strings are "01-01-9999"
Assertions.assertEquals(
    new SimpleDateFormat("dd-MM-yyyy").format(cal.getTime()),
    localDate.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"))
);


localDate = localDate.plusDays(365);
cal.add(Calendar.DAY_OF_MONTH, 365);

// following assertion passes (lengthy workaround using SimpleDateFormat)
Assertions.assertEquals(
    new SimpleDateFormat("dd-MM-yyyy").format(cal.getTime()),
    new SimpleDateFormat("dd-MM-yyyy").format(Timestamp.valueOf(localDate.atTime(LocalTime.MIDNIGHT)))
);

// following assertion fails:
// Expected : "01-01-10000"
// Actual   : "01-01-+10000"
Assertions.assertEquals(
    new SimpleDateFormat("dd-MM-yyyy").format(cal.getTime()),
    localDate.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"))
);

Now the docs states for year patterns that:

If the count of letters is less than four (but not two), then the sign is only output for negative years as per SignStyle.NORMAL. Otherwise, the sign is output if the pad width is exceeded, as per SignStyle.EXCEEDS_PAD.

So this gives a hint, but i'm still clueless about:

How to make DateTimeFormatter output exactly the same string for Y10K+ dates that SimpleDateFormat.format() does in my example (=unsigned for positive years >9999)?

Philzen
  • 3,945
  • 30
  • 46
  • I suggest you abandon the use of the legacy date-classes including `Date`, `Calendar`, and `DateTimeFormatter`. These terrible classes were years ago supplanted by the modern *java.time* classes defined in JSR 310. – Basil Bourque Sep 22 '21 at 02:19
  • 1
    Good suggestion – the java ecosystem is indeed full of legacy (pre java8+) libs that suffer from quirky interfaces or produce funny results. Wish there was a hard(er) deprecation policy or clearer hints at what the most modern libraries are in integrated docs, currently it feels harder than necessary for non-experienced folks to see which those are. Going forward, i'm striving to only use the most modern libs for new code – but as i'm currently tasked with writing unit tests for existing code, it was important to understand these subtle differences in detail and how to work around them. – Philzen Sep 22 '21 at 08:45
  • 2
    @BasilBourque `DateTimeFormatter` actually is a *java.time* class – i assume you meant to suggest abandoning `java.text.SimpleDateFormat`, correct? – Philzen Sep 22 '21 at 08:52
  • @Philsen, Oops! Yes, my first comment above should have read: *I suggest you abandon the use of the legacy date-classes including `Date`, `Calendar`, and `SimpleDateFormat`. These terrible classes were years ago supplanted by the modern java.time classes defined in JSR 310, such as `LocalDate` and `DateTimeFormatter`.* – Basil Bourque Sep 22 '21 at 16:11

1 Answers1

3

ISO 8601 does permit this year format. From Wikipedia:

To represent years before 0000 or after 9999, the standard also permits the expansion of the year representation but only by prior agreement between the sender and the receiver. An expanded year representation [±YYYYY] must have an agreed-upon number of extra year digits beyond the four-digit minimum, and it must be prefixed with a + or − sign instead of the more common AD/BC (or CE/BCE) notation;

However, since it only permits this "by prior agreement between the sender and the receiver", it is quite strange that adding the sign is the default behaviour of LocalDate.toString.

According to the docs:

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. If the count of letters is less than four (but not two), then the sign is only output for negative years as per SignStyle.NORMAL. Otherwise, the sign is output if the pad width is exceeded, as per SignStyle.EXCEEDS_PAD.

So if you don't want the sign, you can use 3 "y"s, or just 1 "y", since 3 and 1 are both "less than four (but not two)".

Also, since "y" means "year of era", there won't be any negative years, so you don't need to worry about it outputting a sign for negative years either.

Example:

System.out.println(
    LocalDate.of(10000, 1, 1)
        .format(DateTimeFormatter.ofPattern("dd-MM-yyy"))
); // 01-01-10000

More generally, you can specify the sign style using the appendValue(TemporalField, int, int, SignStyle) method in DateTimeFormatterBuilder. You can specify SignStyle.NEVER to make it never output a sign.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    This is an excellent answer – standards citation, docs analysis, straightforward solution that i had missied so far... all the bells and whistles :) Accepted. – Philzen Sep 22 '21 at 08:55
  • Actually – i *do worry* about signed BC years in a perfect world – which i realise may be achieved with uppercase year pattern, i.e. `"dd-MM-YYY". Although that yields different date results as it's week-based ... if anybody knows a simple, consistent pattern solution, that'd be cool, otherwise i realise this is out of the scope of my initial question and as such, this is an excellent answer nonetheless. – Philzen Sep 22 '21 at 09:03
  • 1
    @Philzen Do you want BC years to be signed? How about `dd-MM-uuu`? – Sweeper Sep 22 '21 at 09:13
  • 1
    No, upper case `YYY` is wrong (it’s a week-based year and only useful with a week number). `uuu` (lower case) gives you a signed year. Or simply check that `getYear()` returns a positive number and reject the date if not. See also [`uuuu` versus `yyyy` in `DateTimeFormatter` formatting pattern codes in Java?](https://stackoverflow.com/questions/41177442/uuuu-versus-yyyy-in-datetimeformatter-formatting-pattern-codes-in-java) – Ole V.V. Sep 22 '21 at 09:14