9

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?

user990423
  • 1,397
  • 2
  • 12
  • 32
Evan Knowles
  • 7,426
  • 2
  • 37
  • 71
  • I believe your production code is not actually adding 1 year and trying to count how many years it has just added. What are you actually trying to achieve? Give us actual input and expected output. – dotvav Oct 05 '15 at 08:56
  • 3
    @dotvav: The example given seems pretty clear to me... I don't see anything wrong with it. – Jon Skeet Oct 05 '15 at 08:57
  • If you really want one year difference instead of zero then simply subtract the year numbers not taking into account any month or day-of-month-parts. – Meno Hochschild Oct 05 '15 at 08:58
  • 1
    It's not that they're computing a year differently - it's just a quirk of calendrical arithmetic. As you say, the behaviour is reasonable. Can you describe *exactly* what you'd want the behaviour to be? – Jon Skeet Oct 05 '15 at 08:58
  • 2
    (Having said that it's reasonable, Noda Time actually *does* give a period of 1 year in this case :) – Jon Skeet Oct 05 '15 at 08:59
  • I'd suggest a special calculation if the source date is Feb' 29th ... – Fildor Oct 05 '15 at 08:59
  • @JonSkeet the example is fine, and the behaviour seems correct. I just don't understand if OP is looking for a way to correctly **add** 1 year our **count** 1 year. – dotvav Oct 05 '15 at 09:00
  • I'll go with checking for this case in particular and adding one then, thanks guys :) – Evan Knowles Oct 05 '15 at 09:00
  • @JonSkeet Oh really one year? Then NodaTime has a bug IMHO. – Meno Hochschild Oct 05 '15 at 09:09
  • 1
    @MenoHochschild: No, just a different algorithm - it calculates the largest number of years such that start + n years <= end, and that gives 1 year. (It would then move on to months etc.) Seems reasonable to me - and it's the behaviour documented at http://nodatime.org/1.3.x/userguide/arithmetic.html – Jon Skeet Oct 05 '15 at 09:10
  • @JonSkeet Sorry but the page you cited does not contain this special case (years.between(2012-02-29, 2013-02-28) == 1 year). For me, it is still not a full year because I just apply the same criterion as I do for all other dates, namely: If month or day-of-month is smaller after addition of years then reduce year count by one (and same rule applied on month deltas, too). Indeed we have different views what the best arithmetic is, see [here](http://time4j.net/javadoc-en/net/time4j/engine/AbstractDuration.html#algorithm) – Meno Hochschild Oct 05 '15 at 09:29
  • 1
    @MenoHochschild: It doesn't explicitly refer to the special case, but it doesn't need to because the general case describes it. I apply the same criteria I do to all other dates as well - it's not the same criteria that you apply, but that's fine, it just means we have different criteria. So long as in both cases they're well documented, I don't see the problem. This isn't much of a special case, IMO - because you see exactly the same thing for months. I treat January 29th - February 28th 1997 as 1 month, because adding 1 month to January 29th gives February 28th... presumably you wouldn't. – Jon Skeet Oct 05 '15 at 09:32
  • 2
    @MenoHochschild: I strongly suspect that both "modes" of arithmetic are useful in some cases and less useful in others. I don't think it's reasonable to claim Noda Time has a *bug* when it's operating exactly as documented, intentionally. (Note that it's also the behaviour the OP here desired, suggesting I'm right in thinking it's useful in some cases...) – Jon Skeet Oct 05 '15 at 09:34
  • @JonSkeet We should also consider real world not just home-grown technical specifications. Imagine what lawyers will say if someone has been born on a leap day and does almost 18 years later (on 28th of Feb) a criminal action. Youth law or adult law? – Meno Hochschild Oct 05 '15 at 09:36
  • 1
    @MenoHochschild: I don't know - do *you* know what the law of every single country would say? I wouldn't be surprised to see a variety of results. If you really have the appropriate legislation for every case of every law of every country, and they all agree on one particular way, *then* I'd accept that it's a bug in the design... although only in respect of legal calculations, which doesn't mean it's wrong for every single usage of the library. Anyway, I think we're now way beyond where a Stack Overflow comment thread should be. If you still feel this is a bug, file it for Noda Time... – Jon Skeet Oct 05 '15 at 09:37
  • 1
    Just to get it correctly. You wish that `Period.between(date1, date2)` is capable of telling apart whether the second one was constructed using `plusYear(1)` or `plusDays(364)`? – Holger Oct 05 '15 at 09:51
  • 1
    Both 1y and 11m30d are valid answers to this question. Both of these `Period`s would arrive at the same date when added to feb 29. Although it does seem a bit unexpected that `ChronoUnit.YEARS.between(date, date.plusYears(1))` can return `0` – Misha Oct 05 '15 at 09:53
  • @JonSkeet Well, lawyers and judges have room for interpretations, but I also have the astronomy in mind. In this special case, there is simply not yet a full revolution of the earth around the sun. That is fact. And the gregorian year is intended to (approximately) reflect the length of the tropical year. So I strongly feel, the JDK is right. Anyway, the OP is free to set up his own custom `TemporalAmount`-implementation. – Meno Hochschild Oct 05 '15 at 09:57
  • 3
    There isn't a full revolution of the earth around the sun between midnight on January 1st 1997 and midnight on January 1st 1998, given that a solar year is more than 365 days... so much for treating astronomy as the ultimate source of truth here. As Misha says - either way is reasonable IMO, so long as it's documented. – Jon Skeet Oct 05 '15 at 10:00
  • 1
    @Holger Basically I was hoping for a neat solution, where adding a year would return a period of one year, for my use-case of one year. – Evan Knowles Oct 05 '15 at 10:02
  • Can you have a look at Joda library for Java .. Seems possible with it. – Vimal Jain Oct 10 '15 at 15:54

1 Answers1

3

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

diginoise
  • 7,352
  • 2
  • 31
  • 39