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.

© 2026 Xygeni. All rights reserved