Avoid the use of type ZonedDateTime
- see my update below
Provided your recurrent period always consists of days only, I see following optimization (especially for the case if your start date-time is much earlier than your dateTime-argument):
public boolean includes(ZonedDateTime dateTime) {
ZonedDateTime tmpStart = start;
ZonedDateTime tmpEnd = end;
int distance = (int) ChronoUnit.DAYS.between(start, dateTime) - 1;
if (distance > 0) {
int factor = (int) (distance / recurrence.getDays());
if (factor > 0) {
Period quickAdvance = recurrence.multipliedBy(factor);
tmpStart = start.plus(quickAdvance);
tmpEnd = end.plus(quickAdvance);
}
}
while (!tmpStart.isAfter(dateTime)) { // includes equality - okay for you?
if (tmpEnd.isAfter(dateTime)) {
return true;
}
tmpStart = tmpStart.plus(recurrence);
tmpEnd = tmpEnd.plus(recurrence);
}
return false;
}
About DST, well as long as you are fine with the standard strategy of JDK to push forward invalid local times by the size of the gap (change from winter to summer time) the class ZonedDateTime
fits into this scheme. For overlaps, this class offers the special methods withEarlierOffsetAtOverlap()
and withLaterOffsetAtOverlap()
.
However, these features are not really relevant for the question how to find out if a given recurrent interval contains a given date-time because the timezone correction will be applied on all involved date-time-objects in the same way (start
, end
and dateTime
) provided all objects have the same timezone. DST is instead important if you want to determine the duration between start
and end
(or tmpStart
and tmpEnd
).
Update:
I have discovered a hidden bug which is related to daylight-saving effects. In detail:
ZoneId zone = ZoneId.of("Europe/Berlin");
ZonedDateTime start = LocalDate.of(2015, 3, 25).atTime(2, 0).atZone(zone); // inclusive
ZonedDateTime end = LocalDate.of(2015, 3, 25).atTime(10, 0).atZone(zone); // exclusive
Period recurrence = Period.ofDays(2);
ZonedDateTime test = LocalDate.of(2015, 3, 31).atTime(2, 30).atZone(zone); // exclusive
System.out.println("test=" + test); // test=2015-03-31T02:30+02:00[Europe/Berlin]
start = start.plus(recurrence);
end = end.plus(recurrence);
System.out.println("start + 2 days = " + start); // 2015-03-27T02:00+01:00[Europe/Berlin]
System.out.println("end + 2 days = " + end); // 2015-03-27T10:00+01:00[Europe/Berlin]
start = start.plus(recurrence); // <- DST change to summer time!!!
end = end.plus(recurrence);
System.out.println("start + 4 days = " + start); // 2015-03-29T03:00+02:00[Europe/Berlin]
System.out.println("end + 4 days = " + end); // 2015-03-29T10:00+02:00[Europe/Berlin]
start = start.plus(recurrence);
end = end.plus(recurrence);
System.out.println("start + 6 days = " + start); // 2015-03-31T03:00+02:00[Europe/Berlin]
System.out.println("end + 6 days = " + end); // 2015-03-31T10:00+02:00[Europe/Berlin]
boolean includes = !start.isAfter(test) && end.isAfter(test);
System.out.println("includes=" + includes); // false (should be true!!!)
Repeated addition of a period to ZonedDateTime
can shift the local time interval for ever due to DST-effects. But fixed work time schedules are usually defined in terms of local timestamps (in the example from 2 AM to 10 AM). So applying a kind of universal timestamp on a basically local time issue is inherently buggy.
The right datatype to solve the problem of matching work time intervals is: LocalDateTime
:
public boolean includes(LocalDateTime dateTime) {
LocalDateTime tmpStart = start;
LocalDateTime tmpEnd = end;
int distance = (int) ChronoUnit.DAYS.between(start, dateTime) - 1;
if (distance > 0) {
int factor = (int) (distance / recurrence.getDays());
if (factor > 0) {
Period quickAdvance = recurrence.multipliedBy(factor);
tmpStart = start.plus(quickAdvance);
tmpEnd = end.plus(quickAdvance);
}
}
while (!tmpStart.isAfter(dateTime)) {
if (tmpEnd.isAfter(dateTime)) {
return true;
}
tmpStart = tmpStart.plus(recurrence);
tmpEnd = tmpEnd.plus(recurrence);
}
return false;
}
Is it also the right datatype to determine the physical real work time in hours? Yes if you combine a LocalDateTime
with a timezone.
int minutes = (int) ChronoUnit.MINUTES.between(start.atZone(zone), end.atZone(zone));
System.out.println("hours=" + minutes / 60); // 7 (one hour less due to DST else 8)
Conclusion:
The type ZonedDateTime
has many disadvantages. One example is time arithmetic involved here. Most DST-related problems can be better tackled by choosing a combination of LocalDateTime
and explicit timezone parameters.
About resolving of invalid local times:
Good question. JSR-310 only offers one built-in transition strategy for gaps so the final result of resolving an invalid local time 02:30 would be 3:30, not 3:00. That is also what the type ZonedDateTime
does. Citation:
...the local date-time is adjusted to be later by the length of the
gap. For a typical one hour daylight savings change, the local
date-time will be moved one hour later into the offset typically
corresponding to "summer".
However, following workaround can be applied to achieve the next valid time (be careful, the documentation of ZoneRules.getTransition(LocalDateTime)
shows a wrong code schema and example):
LocalDateTime ldt = LocalDateTime.of(2015, 3, 29, 2, 30, 0, 0);
ZoneRules rules = ZoneId.of("Europe/Berlin").getRules();
ZoneOffsetTransition conflict = rules.getTransition(ldt);
if (conflict != null && conflict.isGap()) {
ldt = conflict.getDateTimeAfter();
}
System.out.println(ldt); // 2015-03-29T03:00
For comparison (because you have originally also asked for a solution in other libraries): Joda-Time would only throw an exception if a local time is invalid, probably not what you want. In my library Time4J, I have defined the type PlainTimestamp
corresponding to LocalDateTime
and solved the resolving problem this way (no if-else-constructions):
import static net.time4j.tz.GapResolver.NEXT_VALID_TIME;
import static net.time4j.tz.OverlapResolver.EARLIER_OFFSET;
PlainTimestamp tsp = PlainTimestamp.of(2015, 3, 29, 2, 30);
Moment nextValidTime = // equivalent of java.time.Instant
tsp.in(Timezone.of(EUROPE.BERLIN).with(NEXT_VALID_TIME.and(EARLIER_OFFSET)));
tsp = nextValidTime.toZonalTimestamp(EUROPE.BERLIN);
System.out.println(tsp);
// 2015-03-29T03 (minute is zero and therefore left out here)