Also covers: [8249280](https://bugs.openjdk.org/browse/JDK-8249280)

I will first give a quick summary of the problem.
Put simply, the `LocalDate` form of the `java.sql.Date` is derived using the 
`getYear` method of `java.util.Date`. This in turn returns the year of the 
normalised internal calendar.
However, the internal calendar `getYear` has an extra layer of complexity.
The calendar has an additional era field, which captures BC/AD.
`getYear` therefore just returns the year _of that era_.
For example, the year 6BC and the year 6AD both return `getYear` as 6.

**This means that for BC dates, our `LocalDate` conversion loses the sign of 
the year.**

This leads to additional problems down the line, as the year 1BC is for 
calculations sake is considered to be year 0 (and 2BC us considered year -1 and 
so on). As a result, the various leap year calculations are WRONG for these 
years, causing year format validation failures in situations like 
marshalling/unmarshalling the dates with a DB.

There are two seemingly obvious fixes here, however I will attempt to explain 
why I did not proceed with them.

Firstly, it seems sensible is to derive the `LocalDate` from an `Instant` 
created from the millisecond representation of the `Date`. After all, why we 
are having to use the deprecated `getYear`, `getMonth` and `getDay` methods 
anyway?
The answer lies in [8061577](https://bugs.openjdk.org/browse/JDK-8061577).
The underlying millisecond representation between `java.time.Instant` and 
`java.util.Date` is fundamentally different. Read that ticket for a greater 
explanation.
Ultimately though, it means that the for older dates, the only real way to 
bridge between the two calendar systems is to use these year/month/day methods.

This is where the second possible solution appears.
The underlying calendar representation that `java.util.Date` uses actually does 
have a year method which gives you the correctly signed year, that being 
`getNormalizedYear`.
In fact, `java.util.Date` uses the setter counterpart `setNormalizedYear` is 
its `setYear` method.
Given this, it seems natural that `getYear` should similarly call 
`getNormalizedYear`.
I think this would be my ideal solution, however I recognise that `get`Year 
only returning a positive year is very long standing behaviour. Given how 
widely spread `java.util.Date` is, I felt it was perhaps better not to rock the 
boat too much.

I have therefore taken the decision to add an equivalent `getNormalizedYear` 
method to `java.util.Date`.
This is the simplest way to expose the real inner year of the date to 
`java.sql.Date` and `java.sql.Timestamp`.
This fixes the negative year issue in the LocalDate conversion in an incredibly 
low impact way, and without altering long standing `java.util.Date` behaviour.
I chose to make the method `protected` to prevent unwanted consumption of the 
method. However, I did not make it `Deprecated` like the other accessor methods 
which perhaps I should have done.

Let me know what you think.

---------
- [x] I confirm that I make this contribution in accordance with the [OpenJDK 
Interim AI Policy](https://openjdk.org/legal/ai).

-------------

Commit messages:
 - 8272194: fix negative year handling for Timestamp.toLocalDateTime
 - 8272194: fix negative year handling for Date.toLocalDate

Changes: https://git.openjdk.org/jdk/pull/31808/files
  Webrev: https://webrevs.openjdk.org/?repo=jdk&pr=31808&range=00
  Issue: https://bugs.openjdk.org/browse/JDK-8272194
  Stats: 40 lines in 5 files changed: 38 ins; 0 del; 2 mod
  Patch: https://git.openjdk.org/jdk/pull/31808.diff
  Fetch: git fetch https://git.openjdk.org/jdk.git pull/31808/head:pull/31808

PR: https://git.openjdk.org/jdk/pull/31808

Reply via email to