I have a map of working shifts indexed by day Map<LocalDate, Collection<Shift>> shiftsAllDays
such as:
"2020-03-26": [
{
"id": 4,
"startTime": "21:00:00"
},
{
"id": 5,
"startTime": "09:00:00",
}
],
"2020-03-27": [
{
"id": 4,
"startTime": "22:00:00"
},
{
"id": 5,
"startTime": "10:00:00",
}
],
...
]
as it can be seen, the shifts have an id and a start hour.
But to have the map indexed by day is a waste: the shifts happen to be consistently the same, except for one thing. The start hour is LocalTime
, so the only difference in the shifts, is that after applying the timezone, the resulting shifts only differ in hour when a DST flip takes place.
I want to do some sort of groupBy
in the collector, to have something of the like:
Range.of(2020-01-01 ... 2020-03-26) [
{
"id": 4,
"startTime": "21:00:00"
},
{
"id": 5,
"startTime": "09:00:00",
}
],
Range.of(2020-03-27 ... 2020-10-30) [
{
"id": 4,
"startTime": "22:00:00"
},
{
"id": 5,
"startTime": "10:00:00",
}
],
Range.of(2020-10-31 ... 2021-01-01) [
{
"id": 4,
"startTime": "21:00:00"
},
{
"id": 5,
"startTime": "09:00:00",
}
]
...grouped in Ranges
from DST flip to DST flip. I'm having a hard time writing a Collectors.groupBy(...)
and transforming the key from a single LocalDate
day, to a Range<LocalDate>
To begin with, you need to define a class Range
with two fields (start date and end date), and create a list of ranges. Since only three instance are required, it makes sense to declare this list as a public static final
field within a utility class.
Below is an example of Range
, for the purpose of conciseness I've implemented it as Java 16 record. It is given one utility method which is meant to check whether the given date is within this instance of range:
public record Range(LocalDate start, LocalDate end) {
public boolean isWithinRange(LocalDate date) { // start inclusive, end exclusive
return date.isBefore(start) || date.isBefore(end) && date.isAfter(start);
}
}
A list of ranges:
public static final List<Range> RANGES =
List.of(new Range(LocalDate.of(2020,1, 1), LocalDate.of(2020,3, 27)),
new Range(LocalDate.of(2020,3, 27), LocalDate.of(2020,10, 31)),
new Range(LocalDate.of(2020,10, 31), LocalDate.of(2021,1, 1)));
Utility method getRange()
that is responsible for retrieving an instance of Range
from the list of ranges based on the given date:
public static Range getRange(LocalDate data) {
return RANGES.stream()
.filter(range -> range.isWithinRange(data))
.findFirst()
.orElseThrow();
}
Dummy record Shift
(used for testing purposes):
public record Shift(int id, LocalTime startTime) {}
In order to approach the conversion of a Map<LocalDate, Collection<Shift>>
into a Map<Range, List<Shift>>
with streams, we need to create a stream of map entries. Than make use of collector groupingBy()
in order to group the data by range. As the downstream collector of groupingBy()
we have to provide flatMapping()
in order to flatten the data associated with a particular data (so that all Shift
objects that will be mapped to the same range would be placed in the same collection). And a downstream of flatMapping()
we need to provide a collector which defines how store (or how to perform reduction) flattened elements. In the data collector toList()
is used to store the data mapped to the same key in a list.
main()
- demo
public static void main(String[] args) {
Map<LocalDate, Collection<Shift>> shiftsByDate =
Map.of(LocalDate.of(2020,3, 26),
List.of(new Shift(4, LocalTime.of(21, 0, 0)), new Shift(5, LocalTime.of(9, 0, 0))),
LocalDate.of(2020,3, 27),
List.of(new Shift(4, LocalTime.of(22, 0, 0)), new Shift(5, LocalTime.of(10, 0, 0)))
);
Map<Range, List<Shift>> shiftsByRange = shiftsByDate.entrySet().stream()
.collect(Collectors.groupingBy(entry -> getRange(entry.getKey()),
Collectors.flatMapping(entry -> entry.getValue().stream(),
Collectors.toList())));
shiftsByRange.forEach((k, v) -> System.out.println(k + " : " + v));
}
Output
Range[start=2020-10-31, end=2021-01-01] : [Shift[id=4, startTime=22:00], Shift[id=5, startTime=10:00]]
Range[start=2020-01-01, end=2020-03-27] : [Shift[id=4, startTime=21:00], Shift[id=5, startTime=09:00]]
The previous version required moderate only changes in order to use Range<T>
from the Spring Data project. We need to fix the list RANGES
where these objects are instantiated, and replace the type Range
with the Range<T>
.
There's caveat: if you expect generic type parameter <T>
to be LocalDate
- it's not correct and wouldn't work.
Range<T>
class expects its generic type to be comparable, i.e. T extends Comparable<T>
and LocalDate
doesn't extend comparable directly, it implements ChronoLocalDate
interface which in turn implements Comparable
interface and LocalDate
inherits its default implementation of compareTo()
.
In other words:
LocalDate
extends Comparable<LocalDate>
LocalDate
extends Comparable<ChronoLocalDate>
.Therefore, we have to use ChronoLocalDate
as a generic type.
The RANGES
list will look like that:
public static final List<Range<ChronoLocalDate>> RANGES =
List.of(Range.rightOpen(LocalDate.of(2020,1, 1), LocalDate.of(2020,3, 27)),
Range.rightOpen(LocalDate.of(2020,3, 27), LocalDate.of(2020,10, 31)),
Range.rightOpen(LocalDate.of(2020,10, 31), LocalDate.of(2021,1, 1)));
Method getRange()
which is responsible for retrieving an instance of Range
from the list of ranges based on the given date:
public static Range<ChronoLocalDate> getRange(LocalDate data) {
return RANGES.stream()
.filter(range -> range.contains(data))
.findFirst()
.orElseThrow();
}
main()
- demo
public static void main(String[] args) {
Map<LocalDate, Collection<Shift>> shiftsByDate =
Map.of(LocalDate.of(2020,3, 26),
List.of(new Shift(4, LocalTime.of(21, 0, 0)), new Shift(5, LocalTime.of(9, 0, 0))),
LocalDate.of(2020,3, 27),
List.of(new Shift(4, LocalTime.of(22, 0, 0)), new Shift(5, LocalTime.of(10, 0, 0)))
);
Map<Range<ChronoLocalDate>, List<Shift>> shiftsByRange = shiftsByDate.entrySet().stream()
.collect(Collectors.groupingBy(entry -> getRange(entry.getKey()),
Collectors.flatMapping(entry -> entry.getValue().stream(),
Collectors.toList())));
shiftsByRange.forEach((k, v) -> System.out.println(k + " : " + v));
}
Output:
[2020-03-27-2020-10-31) : [Shift[id=4, startTime=22:00], Shift[id=5, startTime=10:00]]
[2020-01-01-2020-03-27) : [Shift[id=4, startTime=21:00], Shift[id=5, startTime=09:00]]