Skip to content

Fix DurationFormatUtils.formatPeriod() calculation when pattern omits 'M' - #1780

Open
Alwaysgaurav1 wants to merge 1 commit into
apache:masterfrom
Alwaysgaurav1:fix/duration-format-period-without-months
Open

Fix DurationFormatUtils.formatPeriod() calculation when pattern omits 'M'#1780
Alwaysgaurav1 wants to merge 1 commit into
apache:masterfrom
Alwaysgaurav1:fix/duration-format-period-without-months

Conversation

@Alwaysgaurav1

Copy link
Copy Markdown
Contributor

Description

Fixes a calculation bug in DurationFormatUtils.formatPeriod(long, long, String, boolean, TimeZone) where duration formatting with patterns containing year (y) and day (d) tokens without month (M) tokens (e.g. "y' years 'd' days'" or "y'y 'd'd'") incorrectly inflates the duration by +1 full year (+365 days) when spanning across a calendar year boundary that is less than a full 12-month anniversary.

Problem / Reproduction

Prior to this fix:

  • 31 days (2024-12-15 to 2025-01-15) was formatted as "1 years 31 days" (396 days instead of 31 days).
  • 361 days (2024-01-15 to 2025-01-10) was formatted as "1 years 26 days" (392 days instead of 361 days).
  • 365 days (2024-02-29 to 2025-02-28) was formatted as "1 years 28 days" (393 days instead of 365 days).

Root Cause

  1. years was initialized as end.get(Calendar.YEAR) - start.get(Calendar.YEAR).
  2. When M was not present in tokens, the code rolled month differences into days, but did not decrement years when less than a full 12-month calendar year had elapsed.
  3. start was not advanced to match the subtracted years, which caused subsequent month-to-day accumulations to miscount intervening days across year boundaries.

Solution

  1. When M is omitted and y is present:
    • Check if a full calendar year has elapsed (months < 0 || (months == 0 && days < 0)). If not, decrement years.
    • Advance start by the elapsed years (start.add(Calendar.YEAR, (int) years)).
  2. Roll all remaining intervening months into days until start.get(YEAR) == end.get(YEAR) && start.get(MONTH) == end.get(MONTH).
  3. Borrow any negative remaining days from start.getActualMaximum(Calendar.DAY_OF_MONTH).

Tests Added & Verification

  • Added testFormatPeriodWithoutMonths() to DurationFormatUtilsTest.java verifying:
    • Cross-year durations (< 1 year).
    • Periods near 1 full year.
    • Leap-year to non-leap-year boundaries (e.g. Feb 29 to Feb 28).
  • Ran full test suite via Maven: all 46/46 tests passed with 0 failures and 0 regressions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant