Reputation: 89
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
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.
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
.
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.
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
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
Period.bewteen()
Upvotes: 1