Skip to content

timeinterval: fix negative days_of_month across DST transitions - #5447

Merged
SoloJacobs merged 2 commits into
prometheus:mainfrom
sueun-dev:fix-daysinmonth-dst
Aug 22, 2026
Merged

timeinterval: fix negative days_of_month across DST transitions#5447
SoloJacobs merged 2 commits into
prometheus:mainfrom
sueun-dev:fix-daysinmonth-dst

Conversation

@sueun-dev

Copy link
Copy Markdown
Contributor

daysInMonth computes a month's length by dividing the elapsed duration between the first of the month and the first of the next month by 24 hours. When a location is set and that month contains a spring-forward DST transition, the span is 743 wall-clock hours rather than 744, so the truncating division returns 30 for a 31-day month.

ContainsTime uses that count to resolve negative days_of_month entries such as -1 (the last day of the month). So a "last day of the month" mute with a non-UTC location resolves to the 30th during a spring-forward month: it does not fire on the actual last day (the 31st) and fires a day early on the 30th.

Reproduced with days_of_month: ['-1'] and location: America/New_York for March 2021 (spring-forward on the 14th): before the change ContainsTime(Mar 31) is false and ContainsTime(Mar 30) is true; after, it is the reverse. A non-DST month (May) is unaffected either way.

The fix computes the count with calendar arithmetic — day 0 of the following month is the last day of the current month — which does not depend on DST. I added a regression case to timeIntervalTestCases covering -1 in America/New_York across the March transition; it fails before and passes after.

Tested with go test ./timeinterval/... plus the dependent notify, config, and dispatch packages.

[BUGFIX] Time intervals: fix negative `days_of_month` indices resolving one day early in a `location` whose month contains a spring-forward DST transition.

…ions

daysInMonth derived the month length by dividing the elapsed duration
between the first of the month and the first of the next month by 24
hours. In a location whose month contains a spring-forward daylight
saving transition that span is 743 wall-clock hours, not 744, so the
truncating division returns 30 for a 31-day month.

ContainsTime feeds that count into negative days_of_month resolution
(for example -1 for the last day of the month), so with a non-UTC
location set, a "last day" mute resolves to the 30th during a
spring-forward month: it fails to fire on the true last day (31st) and
fires a day early on the 30th.

Compute the count with calendar arithmetic instead: day 0 of the
following month normalizes to the last day of the current month, which
is DST-independent. Add a regression case covering -1 in
America/New_York across the March spring-forward transition.

Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
@sueun-dev
sueun-dev requested a review from a team as a code owner August 13, 2026 09:44
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f503ff2f-108d-4ff4-a970-d7d00d302c8e

📥 Commits

Reviewing files that changed from the base of the PR and between bd6e84e and 3fa310a.

📒 Files selected for processing (2)
  • timeinterval/timeinterval.go
  • timeinterval/timeinterval_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • timeinterval/timeinterval_test.go

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

daysInMonth now uses calendar normalization instead of elapsed-duration arithmetic. A regression test covers negative day-of-month handling across the America/New_York daylight-saving transition.

Changes

Calendar Month Length

Layer / File(s) Summary
Calendar month calculation and regression coverage
timeinterval/timeinterval.go, timeinterval/timeinterval_test.go
daysInMonth performs month-end normalization at noon. Tests verify that -1 includes March 31 and excludes March 30 during the March 2021 daylight-saving transition.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 3fa31

This localized change corrects negative month-day resolution across spring-forward DST transitions and adds regression coverage; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required area: short description format and clearly identifies the DST-related negative days_of_month bug fix.
Description check ✅ Passed The description explains the bug, affected behavior, fix, regression test, test scope, and release note; omitted checklist items are non-critical.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@SoloJacobs SoloJacobs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall a nice change, just few nits on my part

Comment thread timeinterval/timeinterval.go Outdated
// Day 0 of the following month normalizes to the last day of t's month,
// so its day-of-month is the number of days in the month. This is
// calendar arithmetic and stays correct across daylight-saving
// transitions. Deriving the count from an elapsed duration instead

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to explain the previous method.

Comment thread timeinterval/timeinterval.go Outdated
// transitions. Deriving the count from an elapsed duration instead
// (monthEnd.Sub(monthStart).Hours()/24) undercounts by one in a
// spring-forward month, which spans 743 wall-clock hours rather than 744.
return time.Date(t.Year(), t.Month()+1, 0, 0, 0, 0, 0, t.Location()).Day()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do

return time.Date(t.Year(), t.Month()+1, 0, 12, 0, 0, 0, t.Location()).Day()

Better safe than sorry.

Comment thread timeinterval/timeinterval_test.go Outdated
},
},
{
// Negative days of month must resolve against the true month length,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment adds much. It should be concise and readable without the context of the PR

Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
@sueun-dev

Copy link
Copy Markdown
Contributor Author

Thanks, pushed 3fa310a to address the review nits: removed the extra explanatory comments and changed daysInMonth to use noon for the day-zero calculation.

I reran:

  • go test ./timeinterval -run 'TestTimeInterval' -count=1 -v
  • go test ./timeinterval ./notify ./config ./dispatch -count=1
  • go test -race ./timeinterval -run 'TestTimeInterval' -count=10
  • go vet ./timeinterval ./notify ./config ./dispatch
  • git diff --check

@SoloJacobs
SoloJacobs merged commit 68fb2d5 into prometheus:main Aug 22, 2026
7 checks passed
suprjinx pushed a commit to suprjinx/alertmanager that referenced this pull request Aug 31, 2026
…etheus#5447)

daysInMonth derived the month length by dividing the elapsed duration
between the first of the month and the first of the next month by 24
hours. In a location whose month contains a spring-forward daylight
saving transition that span is 743 wall-clock hours, not 744, so the
truncating division returns 30 for a 31-day month.

ContainsTime feeds that count into negative days_of_month resolution
(for example -1 for the last day of the month), so with a non-UTC
location set, a "last day" mute resolves to the 30th during a
spring-forward month: it fails to fire on the true last day (31st) and
fires a day early on the 30th.

Compute the count with calendar arithmetic instead: day 0 of the
following month normalizes to the last day of the current month, which
is DST-independent. Add a regression case covering -1 in
America/New_York across the March spring-forward transition.

Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants