1

I'm trying to find how many months are between 2 dates. My code is something like this right now

ChronoUnit.MONTHS.between(d1, d2)

The problem is that the result is a long. For example if the dates differ only in a few days I should get a result something like 0.34 instead of 0.

Also I need my code to account for the calendar, I cant assume each month has 31 days.

Diff between 1999-05-12 and 1999-08-24
Assuming all months have 31 days for simplicity

result = (19/31 + 31/31 + 31/31 + 24/31) = 2.793
According to the calendar we replace the 31s with the correct number of days for that specific year and month
oren revenge
  • 143
  • 7
  • 1
    Your question is not clear, possibly need some example input and expected output. Since you need to account for calendar, what if d1 and d2 is in different month? – samabcde Oct 04 '22 at 11:29
  • Have you tried [Duration.between](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#between-java.time.temporal.Temporal-java.time.temporal.Temporal-) instead? Then, from the resulting duration object you could get months plus days, hours, minutes and seconds to calculate your fractional value. – Edwin Dalorzo Oct 04 '22 at 11:41
  • I updated my example, also I wanted to know if there is something easier to implement than making the fractional part myself – oren revenge Oct 04 '22 at 11:45
  • @orenrevenge I believe you mean `20/31 + 30/30 + 31/31 + 23/31`? There are 20 days in the range in May, and June has 30 days, the upper bound is exclusive, like in your first example. – Sweeper Oct 04 '22 at 12:08
  • _if there is something easier to implement than making the fractional part myself_ - I assume this is homework; the point is for you to write it. If it's not: This is a bit bizarre - the general way durations are spelled out is either in whole atomaries ('484 days' - days is then the chosen atomary. You can pick seconds too, whatever you want) - or in e.g. '2 years, 5 months, 3 days'. '5.4823 months' is not common. Given that it isn't common, the `java.time` library doesn't cater to the use case. – rzwitserloot Oct 04 '22 at 12:16
  • 1
    Depending on how you want to define fractional months one option is to define `double monthLengthDays = (double) ChronoUnit.MONTHS.getDuration().toSeconds() / (double) ChronoUnit.DAYS.getDuration().toSeconds();` and then calculate your difference as `ChronoUnit.DAYS.between(d1, d2) / monthLengthDays`. The difference between 1999-05-12 and 1999-08-24 is then 3.4169079447216575 months. Beware that in a non-leap year the difference between Jan 31 and Mar 1 is less than a month because it’s 30 days and an average month is longer than that. – Ole V.V. Oct 04 '22 at 14:28
  • 1
    @AlexanderIvanchenko Actually, I would say there is 20 days in May and 23 days in August in that range, since a common convention is that the lower bound is inclusive and the upper bound is exclusive, rather than the other way round. At least we both agree on the actual value being about 3.387. Do note however, that's not what your answer produces. – Sweeper Oct 04 '22 at 17:38
  • @Sweeper You're right. `20` days of May and `23` of August should contribute the result. And thanks for pointing at my mistake, fixed the answer. – Alexander Ivanchenko Oct 04 '22 at 18:50
  • 1
    @rzwitserloot Can you define "atomary"? I've never heard the word, and don't find it in dictionaries. – erickson Oct 04 '22 at 18:57
  • Among all the posts (including questions, answers, and comments), the comment by @OleV.V. makes the most sense. Even if you want to keep `monthLengthDays` = 31, it will be `ChronoUnit.DAYS.between(d1, d2) / 31.0 = 3.35` i.e. by any logical calculation, it will be between 3 and 4 months, never 2.786, the way you have imagined. If [Stephen Colebourne](https://www.linkedin.com/in/stephencolebourne) will see your calculation, he will faint. – Arvind Kumar Avinash Oct 04 '22 at 20:28
  • 1
    @erickson `atom` = smallest unit. In physics it's backed by the laws of nature, but in human systems, it's by decree. A cent is the atomary unit of dollars and euros - not because the notion of 'half a cent' is unthinkable (half a cent is an easy notion to understand, of course), but simply because virtually all monetary systems have decreed that they shall not worry about things smaller than it. Perhaps 'granularity' is the right idea, but what word would you use for the unit of the smallest thing that is _above_ the chosen granularity? – rzwitserloot Oct 04 '22 at 21:40
  • So "atomary" ≡ "atom". – erickson Oct 21 '22 at 15:52

2 Answers2

4

Here is my solution:

public static double monthsBetween(LocalDate start, LocalDate end) {
    if (start.isAfter(end)) throw new IllegalArgumentException("Start must be before end!");

    var lastDayOfStartMonth = start.with(TemporalAdjusters.lastDayOfMonth());
    var firstDayOfEndMonth = end.with(TemporalAdjusters.firstDayOfMonth());
    var startMonthLength = (double)start.lengthOfMonth();
    var endMonthLength = (double)end.lengthOfMonth();
    if (lastDayOfStartMonth.isAfter(firstDayOfEndMonth)) { // same month
        return ChronoUnit.DAYS.between(start, end) / startMonthLength;
    }
    long months = ChronoUnit.MONTHS.between(lastDayOfStartMonth, firstDayOfEndMonth);
    double startFraction = ChronoUnit.DAYS.between(start, lastDayOfStartMonth.plusDays(1)) / startMonthLength;
    double endFraction = ChronoUnit.DAYS.between(firstDayOfEndMonth, end) / endMonthLength;
    return months + startFraction + endFraction;
}

The idea is that you find the last day of start's month (lastDayOfStartMonth), and the first day of end's month (firstDayOfEndMonth) using temporal adjusters. These two dates are very important. The number you want is the sum of:

  • the fractional number of a month between start and lastDayOfStartMonth
  • the whole number of months between lastDayOfStartMonth and firstDayOfEndMonth.
  • the fractional number of a month between firstDayOfEndMonth and end.

Then there is the edge case of when both dates are within the same month, which is easy to handle.

By using this definition, the nice property that the number of months between the first day of any two months is always a whole number is maintained.

Note that in the first calculation, you have to add one day to lastDayOfStartMonth, because ChronoUnit.between treats the upper bound as exclusive, but we actually want to count it as one day here.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
0

To approach this problem, you need to consider the following cases:

  • dates belong to the same year and month;

  • dates belong to different year and/or month;

  • dates are invalid.

When dates belong to the same year and month, then the result would be the difference in days between the two dates divided by the number of days in this month, which can be found using LocalDate.lengthOfMonth().

In the general case, the range of dates can be split into three parts:

  • two fractional parts at the beginning and at the end of the given range of dates (both could be evaluated using the approach for the simplest case when both data belong to the same year/month)
  • the whole part, we can use ChronoUnit.MONTHS.between() to calculate it.

Here's how implementation might look like (d1 - inclusive, d2 - exclusive):

public static double getFractionalMonthDiff(LocalDate d1, LocalDate d2) {
    if (d1.isAfter(d2)) throw new IllegalArgumentException(); // or return a value like -1
    
    if (d1.getYear() == d2.getYear() && d1.getMonth() == d2.getMonth()) { // dates belong to same month and year
        
        return getFractionalPartOfMonth(d2.getDayOfMonth() - d1.getDayOfMonth(), d1.lengthOfMonth());
    }
    
    int monthLen1 = d1.lengthOfMonth();
    
    return getFractionalPartOfMonth(monthLen1 - (d1.getDayOfMonth() - 1), monthLen1) // from the given day of month of the First Date d1 Inclusive to the Last day of month
        + getFractionalPartOfMonth(d2.getDayOfMonth() - 1, d2.lengthOfMonth())       // from the First day of month to given day of month of the Second Date d2 Exclusive (for that reason 1 day was subtracted, and similarly on the previous line 1 day was added)
        + ChronoUnit.MONTHS.between(d1.withDayOfMonth(monthLen1), d2.withDayOfMonth(1));
}

public static double getFractionalPartOfMonth(int daysInterval, int monthLength) {
    return daysInterval / (double) monthLength;
}
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46