This Question originally asked for dates. After Answers were posted, the Question was changed to ask for LocalDateTime. I’ll leave this Answer posted, as it (a) answered the Question as originally posted, and (b) might be helpful to others.
The other Answers look interesting and possibly correct. But I find the following code easier to follow and to verify/debug.
Caveat: I do not claim this code is the best, the leanest, nor the fastest. Frankly, my attempt here was just an exercise to push the limits of my own understanding of using Java streams and lambdas.
Do not invent your own class to hold the start/end dates. The ThreeTen-Extra library provides a LocalDateRange class to represent a span-of-time attached to the timeline as a pair of java.time.LocalDate objects. LocalDateRange offers several methods for:
- Comparison such as
abuts and overlaps.
- Factory methods such as
union and intersection.
We can define the inputs using the convenient List.of methods in Java 9 and later, to make an unmodifiable list of LocalDateRange.
List < LocalDateRange > dateRanges =
List.of(
LocalDateRange.of( LocalDate.of( 2019 , 1 , 1 ) , LocalDate.of( 2019 , 5 , 1 ) ) ,
LocalDateRange.of( LocalDate.of( 2019 , 3 , 1 ) , LocalDate.of( 2019 , 6 , 1 ) ) ,
LocalDateRange.of( LocalDate.of( 2019 , 2 , 1 ) , LocalDate.of( 2019 , 7 , 1 ) ) ,
LocalDateRange.of( LocalDate.of( 2019 , 8 , 1 ) , LocalDate.of( 2019 , 12 , 1 ) ) , // Not connected to the others.
LocalDateRange.of( LocalDate.of( 2018 , 12 , 1 ) , LocalDate.of( 2019 , 1 , 31 ) ) // Earlier start, in previous year.
);
Determine the overall range of dates involved, the very first start and the very last end.
Keep in mind that we are dealing with a list of date-ranges (LocalDateRange), each of which holds a pair of date (LocalDate) objects. The comparator is comparing the starting/ending LocalDate object stored within each LocalDateRange, to get min or max. The get method seen here is getting a LocalDateRange, so we then call getStart/getEnd to retrieve the starting/ending LocalDate stored within.
LocalDate start = dateRanges.stream().min( Comparator.comparing( localDateRange -> localDateRange.getStart() ) ).get().getStart();
LocalDate end = dateRanges.stream().max( Comparator.comparing( localDateRange -> localDateRange.getEnd() ) ).get().getEnd();
Make a list of all the dates within that interval. The LocalDate#datesUntil method generates a stream of LocalDate objects found between a start and end pair of dates. Start is inclusive, while the ending is exclusive.
List < LocalDate > dates =
start
.datesUntil( end )
.collect( Collectors.toList() );
For each of those dates, get a list of the date-ranges containing that date.
Map < LocalDate, List < LocalDateRange > > mapDateToListOfDateRanges = new TreeMap <>();
for ( LocalDate date : dates )
{
List < LocalDateRange > hits = dateRanges.stream().filter( range -> range.contains( date ) ).collect( Collectors.toList() );
System.out.println( date + " ➡ " + hits ); // Visually interesting to see on the console.
mapDateToListOfDateRanges.put( date , hits );
}
For each of those dates, get a count of date-ranges containing that date. We want a count of each List we put into the map above. Generating a new map whose values are the count of a collection in an original map is discussed on my Question, Report on a multimap by producing a new map of each key mapped to the count of elements in its collection value, where I pulled code from Answer by Syco.
Map < LocalDate, Integer > mapDateToCountOfDateRanges =
mapDateToListOfDateRanges
.entrySet()
.stream()
.collect(
Collectors.toMap(
( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getKey(); } ,
( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getValue().size(); } ,
( o1 , o2 ) -> o1 ,
TreeMap :: new
)
);
Unfortunately, there seems to be no way to get a stream to filter more than one entry in a map by maximum value. See: Using Java8 Stream to find the highest values from map.
So first we find the maximum number in a value for one or more entries of our map.
Integer max = mapDateToCountOfDateRanges.values().stream().max( Comparator.naturalOrder() ).get();
Then we filter for only entries with a value of that number, moving those entries to a new map.
Map < LocalDate, Integer > mapDateToCountOfDateRangesFilteredByHighestCount =
mapDateToCountOfDateRanges
.entrySet()
.stream()
.filter( e -> e.getValue() == max )
.collect(
Collectors.toMap(
Map.Entry :: getKey ,
Map.Entry :: getValue ,
( o1 , o2 ) -> o1 ,
TreeMap :: new
)
);
Dump to console.
System.out.println( "dateRanges = " + dateRanges );
System.out.println( "start/end = " + LocalDateRange.of( start , end ).toString() );
System.out.println( "mapDateToListOfDateRanges = " + mapDateToListOfDateRanges );
System.out.println( "mapDateToCountOfDateRanges = " + mapDateToCountOfDateRanges );
System.out.println( "mapDateToCountOfDateRangesFilteredByHighestCount = " + mapDateToCountOfDateRangesFilteredByHighestCount );
Short results.
[Caveat: I have not manually verified these results. Use this code at your own risk, and do your own verification.]
mapDateToCountOfDateRangesFilteredByHighestCount = {2019-03-01=3, 2019-03-02=3, 2019-03-03=3, 2019-03-04=3, 2019-03-05=3, 2019-03-06=3, 2019-03-07=3, 2019-03-08=3, 2019-03-09=3, 2019-03-10=3, 2019-03-11=3, 2019-03-12=3, 2019-03-13=3, 2019-03-14=3, 2019-03-15=3, 2019-03-16=3, 2019-03-17=3, 2019-03-18=3, 2019-03-19=3, 2019-03-20=3, 2019-03-21=3, 2019-03-22=3, 2019-03-23=3, 2019-03-24=3, 2019-03-25=3, 2019-03-26=3, 2019-03-27=3, 2019-03-28=3, 2019-03-29=3, 2019-03-30=3, 2019-03-31=3, 2019-04-01=3, 2019-04-02=3, 2019-04-03=3, 2019-04-04=3, 2019-04-05=3, 2019-04-06=3, 2019-04-07=3, 2019-04-08=3, 2019-04-09=3, 2019-04-10=3, 2019-04-11=3, 2019-04-12=3, 2019-04-13=3, 2019-04-14=3, 2019-04-15=3, 2019-04-16=3, 2019-04-17=3, 2019-04-18=3, 2019-04-19=3, 2019-04-20=3, 2019-04-21=3, 2019-04-22=3, 2019-04-23=3, 2019-04-24=3, 2019-04-25=3, 2019-04-26=3, 2019-04-27=3, 2019-04-28=3, 2019-04-29=3, 2019-04-30=3}
Full code
For your copy-paste convenience, here is an entire class to run this example code.
package work.basil.example;
import org.threeten.extra.LocalDateRange;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
public class DateRanger
{
public static void main ( String[] args )
{
DateRanger app = new DateRanger();
app.demo();
}
private void demo ( )
{
// Input.
List < LocalDateRange > dateRanges =
List.of(
LocalDateRange.of( LocalDate.of( 2019 , 1 , 1 ) , LocalDate.of( 2019 , 5 , 1 ) ) ,
LocalDateRange.of( LocalDate.of( 2019 , 3 , 1 ) , LocalDate.of( 2019 , 6 , 1 ) ) ,
LocalDateRange.of( LocalDate.of( 2019 , 2 , 1 ) , LocalDate.of( 2019 , 7 , 1 ) ) ,
LocalDateRange.of( LocalDate.of( 2019 , 8 , 1 ) , LocalDate.of( 2019 , 12 , 1 ) ) , // Not connected to the others.
LocalDateRange.of( LocalDate.of( 2018 , 12 , 1 ) , LocalDate.of( 2019 , 1 , 31 ) ) // Earlier start, in previous year.
);
// Determine first start and last end.
LocalDate start = dateRanges.stream().min( Comparator.comparing( localDateRange -> localDateRange.getStart() ) ).get().getStart();
LocalDate end = dateRanges.stream().max( Comparator.comparing( localDateRange -> localDateRange.getEnd() ) ).get().getEnd();
List < LocalDate > dates =
start
.datesUntil( end )
.collect( Collectors.toList() );
// For each date, get a list of the date-dateRanges containing that date.
Map < LocalDate, List < LocalDateRange > > mapDateToListOfDateRanges = new TreeMap <>();
for ( LocalDate date : dates )
{
List < LocalDateRange > hits = dateRanges.stream().filter( range -> range.contains( date ) ).collect( Collectors.toList() );
System.out.println( date + " ➡ " + hits ); // Visually interesting to see on the console.
mapDateToListOfDateRanges.put( date , hits );
}
// For each of those dates, get a count of date-ranges containing that date.
Map < LocalDate, Integer > mapDateToCountOfDateRanges =
mapDateToListOfDateRanges
.entrySet()
.stream()
.collect(
Collectors.toMap(
( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getKey(); } ,
( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getValue().size(); } ,
( o1 , o2 ) -> o1 ,
TreeMap :: new
)
);
// Unfortunately, there seems to be no way to get a stream to filter more than one entry in a map by maximum value.
// So first we find the maximum number in a value for one or more entries of our map.
Integer max = mapDateToCountOfDateRanges.values().stream().max( Comparator.naturalOrder() ).get();
// Then we filter for only entries with a value of that number, moving those entries to a new map.
Map < LocalDate, Integer > mapDateToCountOfDateRangesFilteredByHighestCount =
mapDateToCountOfDateRanges
.entrySet()
.stream()
.filter( e -> e.getValue() == max )
.collect(
Collectors.toMap(
Map.Entry :: getKey ,
Map.Entry :: getValue ,
( o1 , o2 ) -> o1 ,
TreeMap :: new
)
);
System.out.println( "dateRanges = " + dateRanges );
System.out.println( "start/end = " + LocalDateRange.of( start , end ).toString() );
System.out.println( "mapDateToListOfDateRanges = " + mapDateToListOfDateRanges );
System.out.println( "mapDateToCountOfDateRanges = " + mapDateToCountOfDateRanges );
System.out.println( "mapDateToCountOfDateRangesFilteredByHighestCount = " + mapDateToCountOfDateRangesFilteredByHighestCount );
}
}
rin the sorted list, check how many of the ranges does the start ofrfall in. Find the max.