When calculating years between two dates, where the second date is calculated from the first one (this is a simplified example of what I'm working on), LocalDate
and Period
seem to calculate a year slightly differently.
For example,
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusYear = date.plusYears(1);
System.out.println(Period.between(date, plusYear).getYears());
while
LocalDate date = LocalDate.of(1996, 3, 29);
LocalDate plusYear = date.plusYears(1);
System.out.println(Period.between(date, plusYear).getYears());
Despite having explicitly added a year, first Period
return the years as 0
, while the second case returns 1
.
Is there a neat way around this?
Any year that is evenly divisible by 4 is a leap year: for example, 1988, 1992, and 1996 are leap years.
If you were born on Leap Day 1920, you would be 100 years old, or 25 in Leap Day years. The year must be evenly divisible by 4. If the year can be evenly divided by 100, it is not a leap year unless the year is also evenly divisible by 400, according to mathisfun.com.
The complete list of leap years in the first half of the 21st century is therefore 2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044, and 2048.
Generally, a leap year happens every four years, which, thankfully, is a fairly simple pattern to remember. However, there is a little more to it than that. Here are the rules of leap years: A year may be a leap year if it is evenly divisible by 4.
This question has a philosophical nature and spans few problems like time measurements, and date format conventions.
LocalDate
is an implementation of ISO 8601 date exchange standard.
Java Doc states explicitly that this class does not represent time but provides only standard date notation.
The API provides only simple operations on the notation itself and all calculations are done by incrementing the Year, or Month, or Day of a given date.
In other words, when calling LocalDate.plusYears()
you are adding conceptual years of 365 days each, rather than the exact amount of time within a year.
This makes Day the lowest unit of time which one can add to a date expressed by LocalDate
.
In human understanding, date is not a moment in time, but it is a period.
It starts with 00h 00m 00s (...) and finishes with 23h 59m 59s (...).
LocalDate
however avoids problems of time measurement and vagueness of human time units (hour, day, month, and a year can all have different length) and models date notation simply as a tuple of:
(years, months within a year, days within a month )
calculated since the beginning of the era.
In this interpretation, it makes sense that Day is the smallest unit affecting the date.
As an example following:
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusSecond = date.plus(1, ChronoUnit.SECONDS);
returns
java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
... which shows, that using LocalDate
and adding the number of seconds (or smaller units to drive the precision), you could not overcome the limitation listed in your question.
Looking at the implementation you find that LocalDate.plusYears()
after adding the years, calls resolvePreviousValid()
. This method then checks for leap year and modifies the day field in the following manner:
day = Math.min(day, IsoChronology.INSTANCE.isLeapYear((long)year)?29:28);
In other words it corrects it by effectively deducting 1 day.
You could use Year.length()
which returns the number of days for given year and will return 366 for leap years. So you could do:
LocalDate plusYear = date.plus(Year.of(date.getYear()).length(), ChronoUnit.DAYS);
You will still run into following oddities (call to Year.length()
replaced with the day counts for brevity):
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusYear = date.plus(365, ChronoUnit.DAYS);
System.out.println(plusYear);
Period between = Period.between(date, plusYear);
System.out.println( between.getYears() + "y " +
between.getMonths() + "m " +
between.getDays() + "d");
returns
1997-02-28
0y 11m 30d
then
LocalDate date = LocalDate.of(1996, 3, 29);
LocalDate plusYear = date.plus(365, ChronoUnit.DAYS);
System.out.println(plusYear);
Period between = Period.between(date, plusYear);
System.out.println( between.getYears() + "y " +
between.getMonths() + "m " +
between.getDays() + "d");
returns
1997-03-29
1y 0m 0d
and finally:
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusYear = date.plus(366, ChronoUnit.DAYS);
System.out.println(plusYear);
Period between = Period.between(date, plusYear);
System.out.println( between.getYears() + "y " +
between.getMonths() + "m " +
between.getDays() + "d");
returns:
1997-03-01
1y 0m 1d
Please note that moving the date by 366 instead of 365 days increased the period from 11 months and 30 days to 1 year and 1 day (2 days increase!).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With