Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculating years between from a leap year

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?

like image 820
Evan Knowles Avatar asked Oct 05 '15 08:10

Evan Knowles


People also ask

How do you calculate a leap year between two years?

Any year that is evenly divisible by 4 is a leap year: for example, 1988, 1992, and 1996 are leap years.

How do you calculate the age of a leap year?

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.

How many leap years are there between 2000 and 2022?

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.

Are leap years always 4 years apart?

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.


1 Answers

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!).

like image 143
diginoise Avatar answered Sep 23 '22 13:09

diginoise