Duration Between Local Date Times
ID |
java.duration_between_local_date_times |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Java |
Tags |
reliability |
Description
Reports Duration.between(a, b), or a DST-sensitive ChronoUnit.X.between(a, b) (unit DAYS or finer), where an operand’s declared type is LocalDateTime.
Rationale
LocalDateTime carries no time zone or offset, so computing a duration between two LocalDateTime values is a pure calendar/clock-face delta — not a measure of physical elapsed time. When the two values actually correspond to moments observed in different zones, the result silently differs from the real elapsed duration by the zone offset and by any Daylight Saving Time transition inside the window. No exception is thrown; the wrong number simply propagates into whatever depends on it — a billing period, an SLA timer, a scheduling window.
// Bad - ignores the 15-hour gap between the two zones these values were observed in
LocalDateTime callStartLosAngeles = LocalDateTime.of(2026, 9, 25, 20, 0);
LocalDateTime callEndSingapore = LocalDateTime.of(2026, 9, 26, 12, 0);
long hours = ChronoUnit.HOURS.between(callStartLosAngeles, callEndSingapore); // returns 16, not 1
Duration elapsed = Duration.between(callStartLosAngeles, callEndSingapore); // PT16H, not PT1H
Remediation
Anchor each LocalDateTime to its zone with atZone(ZoneId) before computing the duration.
// Good - anchored to each moment's own zone; the real elapsed time is used
ZonedDateTime callStart = LocalDateTime.of(2026, 9, 25, 20, 0)
.atZone(ZoneId.of("America/Los_Angeles"));
ZonedDateTime callEnd = LocalDateTime.of(2026, 9, 26, 12, 0)
.atZone(ZoneId.of("Asia/Singapore"));
long hours = ChronoUnit.HOURS.between(callStart, callEnd); // returns 1
Duration elapsed = Duration.between(callStart, callEnd); // PT1H
What is not reported
Only the sub-day/day-scale ChronoUnit constants are DST-sensitive (NANOS through DAYS, plus HALF_DAYS); WEEKS and coarser are compliant, since a calendar-scale delta is the same whether or not a zone was crossed. LocalTime and LocalDate operands, and operands already anchored to a zone or instant (ZonedDateTime, Instant, OffsetDateTime), are never reported.
Neither is a LocalDateTime built from an explicit zone — LocalDateTime.now(ZoneOffset.UTC), or instant.atZone(ZoneId.systemDefault()).toLocalDateTime(). The declared type is LocalDateTime, but the author has already named the zone the value belongs to, so the pair is not the comparison of two unanchored clock faces this rule is about. This is read from the operand’s own declaration initializer; a parameter handed in from elsewhere carries nothing to read and stays reportable.