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() ;