Baiso
Baiso

Reputation: 89

How to get right number of months and day between several dates?

I need to compute the proper number of months and days between a list of intervals:

31/03/2017  30/09/2017
01/10/2017  31/03/2018
01/04/2018  30/09/2018
01/10/2018  31/12/2019

If I compute all days between these intervals and then divide by 30, I got

MONTHS: 33 DAYS: 12

but what I want is:

MONTHS: 33 DAYS: 1 (the only day to compute is the first day of first interval)

Upvotes: 0

Views: 448

Answers (2)

Basil Bourque
Basil Bourque

Reputation: 338181

The Answer by Ole V.V. is correct, and wisely uses the modern java.time classes.

LocalDateRange

As a bonus, I will mention adding the excellent ThreeTen-Extra library to your project to access the LocalDateRange class. This class represents a span of time as a pair of LocalDate objects. That seems a match to your business problem.

LocalDate start = … ;
LocalDate stop = … ;
LocalDateRange range = LocalDateRange.of( start , stop ) ;

From this LocalDateRange object you can obtain the Period objects used in that Answer by Ole V.V.

Period period = range.toPeriod() ;

Notice that LocalDateRange has handy methods for comparison, such as abuts, contains, overlaps, and so on.

Half-Open

You may be confused about how to handle end-of-month/first-of-month.

Generally in date-time handling, it is best to represent a span-of-time using the Half-Open approach. In this approach, the beginning is inclusive while the ending is exclusive.

So a month starts on the first of the month and runs up to, but does not include the first of the next month.

LocalDateRange rangeOfMonthOfNovember2019 = 
    LocalDateRange.of(
        LocalDate.of( 2019 , Month.NOVEMBER , 1 ) ,
        LocalDate.of( 2019 , Month.DECEMBER , 1
    )
;

You can ask each LocalDateRange for days elapsed: LocalDateRange::lengthInDays.

DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu" );
List < LocalDateRange > ranges = new ArrayList <>( 4 );
ranges.add(
        LocalDateRange.of(
                LocalDate.parse( "31/03/2017" , f ) ,
                LocalDate.parse( "30/09/2017" , f )
        )
);
ranges.add(
        LocalDateRange.of(
                LocalDate.parse( "01/10/2017" , f ) ,
                LocalDate.parse( "31/03/2018" , f )
        )
);
ranges.add(
        LocalDateRange.of(
                LocalDate.parse( "01/04/2018" , f ) ,
                LocalDate.parse( "30/09/2018" , f )
        )
);
ranges.add(
        LocalDateRange.of(
                LocalDate.parse( "01/10/2018" , f ) ,
                LocalDate.parse( "31/12/2019" , f )
        )
);

// Sum the periods, one from each range.
Period period = Period.ZERO;
int days = 0;
for ( LocalDateRange range : ranges )
{
    days = ( days + range.lengthInDays() );
    period = period.plus( range.toPeriod() ).normalized();
}

Dump to console.

System.out.println( "ranges: " + ranges );
System.out.println( "days: " + days + "  |  pseudo-months: " + ( days / 30 ) + " and days: " + ( days % 30 ) );
System.out.println( "period: " + period );

ranges: [2017-03-31/2017-09-30, 2017-10-01/2018-03-31, 2018-04-01/2018-09-30, 2018-10-01/2019-12-31]

days: 1002 | pseudo-months: 33 and days: 12

period: P2Y5M119D

If you insist on fully-closed rather than half-open, LocalDateRange can still help you with its ofClosed method. But I strongly suggest you adopt half-open instead, as I believe you will find it makes your life easier to use one consistent approach across all your code. And educate your users. I have seen much confusion among office staff making incorrect assumptions about inclusive/exclusive dates. Your practice of tracking end-of-month to end-of-month seems likely to engender ever more confusion.

YearMonth

Another class you might find useful is built into Java: YearMonth. This class represents a particular month as a whole unit.

YearMonth yearMonth = YearMonth.of( 2019 , Month.NOVEMBER ) ;

Notice methods such as atDay to produce a LocalDate from a YearMonth.

ISO 8601

Tip: Make a habit of using only the ISO 8601 standard formats rather than localized formats when serializing date-time values as text.

So for a date, use YYYY-MM-DD.

The java.time classes use these formats by default when parsing/generating text. So no need to specify a formatting pattern.

LocalDate.parse( "2019-01-23" ) 

The standard ISO 8601 format for an entire month is YYYY-MM such as 2019-11.

YearMonth yearMonth = YearMonth.parse( "2019-11" ) ;  // Entire month of November 2019.

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Upvotes: 2

Anonymous
Anonymous

Reputation: 86139

You may ignore the dates in between and just calculate the difference between the first and the last date:

    LocalDate first = LocalDate.of(2017, 3, 31);
    LocalDate lastInclusive = LocalDate.of(2019, 12, 31);
    LocalDate lastExclusive = lastInclusive.plusDays(1);
    Period between = Period.between(first, lastExclusive);

    System.out.format("Months: %s days: %d%n",
            between.toTotalMonths(), between.getDays());

Output is in this case:

Months: 33 days: 1

Since Period.between() calculates the difference up to the end date exclusive and you want the end date included, I add one day to your end date.

Months can be 28, 29, 30 and 31 days long and are longer than 30 days on the average, which is why dividing by 30 gave you too many days. You may read up on the exact working of Period.between() and check if it always gives you what you want.

Links

Upvotes: 1

Related Questions