Data structure for representing a calendar schedule with a set time granularity?

Viewed 758

I've been trying to think of a purely Java way to represent a schedule for a whole calendar year. Each schedule is for a 30 minute slot (half hour granularity).

This would be abstracted through a repository methods like findByDateTime()

Essentially I need to model time slots at 30 min granularites for each day.

The way I have hacked it together is like so

public Map<Integer, Map<Integer, Programme>> enumerateMapOfMapSchedules() {

    int numberOfSlots = 48; //48 half hours in a day

    Map<Integer, Map<Integer, Programme>> dayToTimeScheduleMap = new HashMap<>();

    //create a key value map for each day of the year
    for (int i = 1; i < 366; i++) {

        Map<Integer, Programme> dayProgrammeScheduleMap = new HashMap<>();

        for (int y = 1; y < numberOfSlots; y++) {
            dayProgrammeScheduleMap.put(y, null);
        }

        dayToTimeScheduleMap.put(i, dayProgrammeScheduleMap);
    }
    //creates a map with 365 days and each day having a map of 48 schedule slots
    return dayToTimeScheduleMap;
}

I appreciate this solution doesn't handle or have a concept of year, however since these are for mocks/tests then I am ok with this.

Also it doesn't handle a schedule that overlaps, if programme spans two half hour slots.

My query method is quite simple for finding what is in a particular schedule slot.

public Programme findByDateTime(LocalDateTime dateTime) {

    int scheduleSlot = dateTime.getHour() * 2;

    //if its the after 30 minute schedule slot
    if (dateTime.getMinute() > 30) {
        scheduleSlot++;
    }

    return scheduleMap.get(dateTime.getDayOfYear()).get(scheduleSlot);
}

However for iterating through all the data structure to see how many occurences of a particular programme exist.

My question, is there an easier way of doing this?

I tried doing it with a relational DB but it was hard to represent time periods easily without a lot of SQL.

Any suggestions or implementation advice welcomed!

1 Answers

Days vary in length

A calendar schedule only makes sense in the context of a time zone and a year. Politicians frequently change the offset used by the time zone(s) of their jurisdiction. This means days are not always 24-hours long. Anomalies such as Daylight Saving Time (DST) mean a day might be 23 hours long, 25 hours long, or something else such as 23.5 hours long.

Start at the beginning, count by 30-minute increments

So if what you want is to chop up the entire year in 30-minute segments, you must start at the first moment of the first day of a specific year in a specific time zone, and add 30 minutes at a time until reaching the new year.

ZoneId z = ZoneId.of( "America/Montreal" );
Year year = Year.of( 2021 );
LocalDate firstOfYear = year.atDay( 1 );
ZonedDateTime start = firstOfYear.atStartOfDay( z );
List < ZonedDateTime > zdts = new ArrayList <>();

Duration duration = Duration.ofMinutes( 30 );
ZonedDateTime zdt = start;
while ( zdt.getYear() == year.getValue() )
{
    zdts.add( zdt );
    // Setup the next loop.
    zdt = zdt.plus( duration );
}

Return a non-modifiable copy of that list.

List < ZonedDateTime > slots = List.copyOf( zdts );

When run. Notice what happens at 1 or 2 AM on Mar 14, 2021 and Nov 7, 2021.

slots = [2021-01-01T00:00-05:00[America/Montreal], 2021-01-01T00:30-05:00[America/Montreal], 2021-01-01T01:00-05:00[America/Montreal], 2021-01-01T01:30-05:00[America/Montreal], 2021-01-01T02:00-05:00[America/Montreal],

…

2021-03-14T01:00-05:00[America/Montreal], 2021-03-14T01:30-05:00[America/Montreal], 2021-03-14T03:00-04:00[America/Montreal],

…

2021-11-07T00:30-04:00[America/Montreal], 2021-11-07T01:00-04:00[America/Montreal], 2021-11-07T01:30-04:00[America/Montreal], 2021-11-07T01:00-05:00[America/Montreal], 2021-11-07T01:30-05:00[America/Montreal], 2021-11-07T02:00-05:00[America/Montreal],

…

2021-12-31T22:00-05:00[America/Montreal], 2021-12-31T22:30-05:00[America/Montreal], 2021-12-31T23:00-05:00[America/Montreal], 2021-12-31T23:30-05:00[America/Montreal]]

Future projections unreliable!

But beware: politicians frequently change the offset used in a zone! This happens much more often than you likely realize. Politicians have even gotten worse at this reducing their forewarning from years to a few months, or even several weeks as seen recently in Turkey and Morocco, and even no forewarning at all as seen in North Korea.

So you cannot reliably project into the future using the approach seen above.

Slot math

I suppose you could approach the slots-of-year problem in another way. Calculate the number of whole slots during the year this way.

ZoneId z = ZoneId.of( "America/Montreal" );
Year year = Year.of( 2021 );
LocalDate firstOfYear = year.atDay( 1 );
ZonedDateTime start = firstOfYear.atStartOfDay( z );
ZonedDateTime end = start.plusYears( 1 );

Duration slotLength = Duration.ofMinutes( 30 );
long wholeSlotsInYear = Duration.between( start , end ).dividedBy( slotLength );

Then you could jump to a point in the year by multiplying duration, and adding the result to the start of year.

int slotNumber = 22;
Duration jump = slotLength.multipliedBy( slotNumber - 1 );  // Subtract one to change an ordinal number into a zero-based index. 
ZonedDateTime slot22 = start.plus( jump );

Appointment book tracking

If you are doing appointments such as at a hair salon or dental clinic, the usual approach is to track a year-month-day with a particular time of day. But track the time zone separately. So use a LocalDateTime with a separate ZoneId in your Java model. In your database table, use a pair of columns, one of type akin to the SQL-standard type TIMESTAMP WITHOUT TIME ZONE and another column of a text type holding the name of the time zone such as America/Montreal or Africa/Tunis.

When building a schedule, apply the zone to determine a moment. In Java, that means applying a ZoneId to a LocalDateTime to get a ZonedDateTime.

You need to be clear on the fundamental idea that a LocalDateTime object does not represent a moment. In our example here, 3 PM on the 23rd of next year could mean 3 PM in Tokyo Japan or 3 PM in Toledo Ohio US, two very different moments several hours apart. A LocalDateTime is inherently ambiguous. Thus the need to store a time zone as well, but kept separate.

LocalDateTime ldt = LocalDateTime.of( 2021 , 1 , 23 , 15 , 0 , 0 , 0 ) ;
ZoneId z = ZoneId.of( "America/Montreal" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ;  // Determine a moment.

See that same moment in UTC by extracting a Instant.

Instant instant = zdt.toInstant() ;
Related