I have a seemingly simple task, I wish to solve it in pure Java 21 - although will use a library if I must.
I need to count the number of completed half hour intervals, from midnight on the same day - this is specially for the UK electricity market and there are specific rules for this counting. There must be
- 48 periods numbered
1..48on a normal day - 46 periods numbered
1..46on a "short day" (clocks go forward) - 50 periods numbered
1..50on a "long day" (clocks go back)
You can think of this as the count of half hours of "wall time" from the start of the day until now() (or a specific instant). Time zones are relevant only in that the change in zone causes there to be one more or less hour in the day in the UK.
So the method signature is something like
int settlementPeriod(final OffsetDateTime date)
i.e. given an arbitrary point on the timeline, determine the correct period in the UK.
My first attempt was very simple, essentially get the start of the day in the UK and that same point in the UK; take the duration between them and divide by the half hour
public static int settlementPeriod(final OffsetDateTime readingTime) {
final var zone = ZoneId.of("Europe/London");
final var startOfDay = readingTime.truncatedTo(ChronoUnit.DAYS).atZoneSameInstant(zone);
final var duration = Duration.between(startOfDay, readingTime.atZoneSameInstant(zone));
return (int) duration.dividedBy(Duration.ofMinutes(30)) + 1;
}
That works for normal days but for both "short" and "long" days fails miserably.
On a "short" day, once the transition occurs, the index jumps one hour.
On a "long" day, once the transition occurs, the index repeats one hour.
Right, so the time zone I get when I convert the readingTime to the local zone is the time zone after the transition - so then I'm comparing
- for a "short" day midnight BST to >=2AM BST
- for a "long" day midnight GMT to >= 1AM GMT
Where, the startOfDay should be in the pre-transition time zone.
I came up with the following which almost passes my tests
public static int settlementPeriod(final OffsetDateTime readingTime) {
final var zone = ZoneId.of("Europe/London");
final var transition = zone.getRules().previousTransition(readingTime.toInstant());
final ZonedDateTime startOfDay;
if (Objects.equals(transition.getDateTimeBefore().toLocalDate(), readingTime.toLocalDate())) {
startOfDay = readingTime.toLocalDate().atStartOfDay(transition.getOffsetBefore());
} else {
startOfDay = readingTime.truncatedTo(ChronoUnit.DAYS).atZoneSameInstant(zone);
}
final var duration = Duration.between(startOfDay, readingTime.atZoneSameInstant(zone));
return (int) duration.dividedBy(Duration.ofMinutes(30)) + 1;
}
But has a couple of problems
- it's rather ugly, having to determine if today is a transition day
- if the passed in
readingTimeis exaclty on the transition time,previousTransitionreturns the wrong transition (makes sense)
What is the "correct" way to do this with java.time?
You should first do
atZoneSameInstant(zone), thentruncatedTo(ChronoUnit.DAYS).Doing
truncatedToon anOffsetDateTimewill keep the offset unchanged, but this is not what happens on a day with a time zone transition. The start of day might have a different offset than the offset ofreadingTime. By changing it to aZonedDateTimefirst, you make sure thattruncatedTotakes the time zone transitions into account.Side note: I feel this method should take an
Instant, orZonedDateTimeas parameter.