The Answer by Alexander Ivanchenko is very good. Here is an alternative, as food for thought.
By the way, your day
field is redundant. You know both the date and date-time from the starting LocalDateTime
. I suggest eliminating that from your model.
Add TimeSlot#contains
method
Let's define your TimeSlot
class more briefly as a record. And I will use UUID
as an identifier, to not fiddle with counting integers.
record TimeSlot( UUID id , LocalDateTime start , LocalDateTime end ) {}
The trick is to add a contains
method, telling the caller whether a particular LocalDateTime
happens to lay within the bounds of our time slot. We use Half-Open approach, where the span of time is defined with the beginning being inclusive while the ending is exclusive.
Notice that “not before” is a shorter way of saying “is equal to or is later than”.
record TimeSlot( UUID id , LocalDateTime start , LocalDateTime end )
{
boolean contains ( LocalDateTime localDateTime )
{
return ( ( ! localDateTime.isBefore( this.start ) ) && localDateTime.isBefore( this.end ) );
}
}
Sample data.
List < TimeSlot > timeslots =
List.of(
new TimeSlot(
UUID.fromString( "0500ce28-ad96-43d0-9b3d-b907cadd27f9" ) ,
LocalDateTime.of( 2022 , 04 , 16 , 9 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 10 , 00 )
) ,
new TimeSlot(
UUID.fromString( "887d72df-4787-4974-ab6d-f2a46cb7d7af" ) ,
LocalDateTime.of( 2022 , 04 , 16 , 10 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 11 , 00 )
)
);
And some sample inputs.
List < LocalDateTime > inputs =
List.of(
LocalDateTime.of( 2022 , 04 , 16 , 8 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 9 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 10 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 11 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 12 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 13 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 14 , 00 ) ,
LocalDateTime.of( 2022 , 04 , 16 , 15 , 00 )
);
Make a new list of TimeSlot
objects, skipping over spans of time take by our existing TimeSlot
objects.
List < TimeSlot > resultingTimeSlots = new ArrayList <>( inputs.size() );
Loop the input LocalDateTime
objects. For each, ask each of the original time slots if they happen to contain that date-time value. If so, add that found time slot to our list. If not, create a new TimeSlot
object using the input LocalDateTime
as the start, and assuming the end should be an hour later.
for ( LocalDateTime input : inputs )
{
Optional < TimeSlot > hit = timeslots.stream().filter( timeSlot -> timeSlot.contains( input ) ).findAny();
resultingTimeSlots.add( hit.orElse( new TimeSlot( UUID.randomUUID() , input , input.plusHours( 1 ) ) ) );
}
That code could be optimized. Using Optional#orElse
is actually executing a new TimeSlot
on every loop of our for
, whether needed or not.
We can verify this behavior by adding a compact constructor to our record.
record TimeSlot( UUID id , LocalDateTime start , LocalDateTime end )
{
TimeSlot
{
System.out.println( "running constructor for " + id );
}
boolean contains ( LocalDateTime localDateTime )
{
return ( ( ! localDateTime.isBefore( start ) ) && localDateTime.isBefore( end ) );
}
}
Let’s replace that with Optional#orElseGet
while transforming our new TimeSlot
into a lambda that implements the required Supplier
interface. This change means a new TimeSlot
object will be instantiated only when we truly need it.
for ( LocalDateTime input : inputs )
{
Optional < TimeSlot > hit = timeslots.stream().filter( timeSlot -> timeSlot.contains( input ) ).findAny();
resultingTimeSlots.add( hit.orElseGet( ( ) -> new TimeSlot( UUID.randomUUID() , input , input.plusHours( 1 ) ) ) );
}
Dump to console.
System.out.println( "resultingTimeSlots.size(): " + resultingTimeSlots.size() );
System.out.println( "resultingTimeSlots.containsAll( timeslots ): " + resultingTimeSlots.containsAll( timeslots ) );
System.out.println( resultingTimeSlots );
When run.
running constructor for 0500ce28-ad96-43d0-9b3d-b907cadd27f9
running constructor for 887d72df-4787-4974-ab6d-f2a46cb7d7af
running constructor for ce2b9c66-ef69-4ecd-a451-7a51ebbb259e
running constructor for c77cf83f-5ea8-4da3-9a44-104a56f4de03
running constructor for 139280b6-20c4-4428-b2cb-80717a00756b
running constructor for 1d219e16-0513-466e-9b84-091312e4ff5e
running constructor for 4b0b6c11-c6ae-4e04-a8fe-6c1245f7e80b
running constructor for 1ccdbd7f-ff4c-4d7d-b900-54d14898a50f
resultingTimeSlots.size(): 8
resultingTimeSlots.containsAll( timeslots ): true
[TimeSlot[id=ce2b9c66-ef69-4ecd-a451-7a51ebbb259e, start=2022-04-16T08:00, end=2022-04-16T09:00], TimeSlot[id=0500ce28-ad96-43d0-9b3d-b907cadd27f9, start=2022-04-16T09:00, end=2022-04-16T10:00], TimeSlot[id=887d72df-4787-4974-ab6d-f2a46cb7d7af, start=2022-04-16T10:00, end=2022-04-16T11:00], TimeSlot[id=c77cf83f-5ea8-4da3-9a44-104a56f4de03, start=2022-04-16T11:00, end=2022-04-16T12:00], TimeSlot[id=139280b6-20c4-4428-b2cb-80717a00756b, start=2022-04-16T12:00, end=2022-04-16T13:00], TimeSlot[id=1d219e16-0513-466e-9b84-091312e4ff5e, start=2022-04-16T13:00, end=2022-04-16T14:00], TimeSlot[id=4b0b6c11-c6ae-4e04-a8fe-6c1245f7e80b, start=2022-04-16T14:00, end=2022-04-16T15:00], TimeSlot[id=1ccdbd7f-ff4c-4d7d-b900-54d14898a50f, start=2022-04-16T15:00, end=2022-04-16T16:00]]
If you want to skip the original items, change that for
loop to this.
for ( LocalDateTime input : inputs )
{
Optional < TimeSlot > hit = timeslots.stream().filter( timeSlot -> timeSlot.contains( input ) ).findAny();
if ( hit.isEmpty() ) { resultingTimeSlots.add( new TimeSlot( UUID.randomUUID() , input , input.plusHours( 1 ) ) ); }
}
Tracking appointments
If you are trying to make a future appointment tracking app, yours is the wrong approach.
You should track only the start as a LocalDateTime
, not the end. Instead of an end, track the length of the appointment as a Duration
object. And crucially, add a field for the time zone (ZoneId
) as the intended context for this date and time.
The concept to understand is that political time, as opposed to natural time, varies. Days are not necessarily 24 hours long. They may be 23, 23.5, 25, or other number of hours. So a 1 hour appointment might start at 2 but end at 4.
When you need to build a schedule of moments, specific points on the timeline, apply the time zone to the start. And add duration for the end.
ZonedDateTime start = startLocalDateTime.atZone( storedZoneId ) ;
ZonedDateTime end = start.plus( storedDuration ) ;
But never store these ZonedDateTime
objects. They would become invalid if the politicians change the rules of the time zone(s) in their jurisdictions. Politicians around the world do so with surprising frequency.
I and others have written on this subject many times on Stack Overflow. So search to learn more.