Great work!

XP to next level

BugEater

Date Arithmetic Across Boundaries

Learning Objectives

  • Distinguish between adding calendar units (months, years) and adding absolute units (days, seconds) and understand when each is appropriate
  • Identify common boundary-crossing bugs in date arithmetic, including end-of-month clamping and year rollover
  • Analyze the ambiguity in phrases like "30 days from February 1" and know how to test both interpretations

Duration vs Period: Two Different Things

Date arithmetic comes in two flavors that are often confused:

Duration (absolute): a fixed amount of time — 30 days = 30 × 86400 seconds. The result depends only on the starting instant and the duration.

Period (calendar-relative): "1 month" means advance the month counter by one, keeping the same day if possible, clamping if not. The result depends on the calendar structure.

In Java, Duration is for absolute time; Period is for calendar time. In JavaScript, there is no built-in distinction — adding months manually requires calendar awareness.

// These are NOT the same for February 1:
LocalDate start = LocalDate.of(2024, 2, 1);
LocalDate plusThirtyDays = start.plusDays(30);  // March 2, 2024
LocalDate plusOneMonth = start.plusMonths(1);    // March 1, 2024

The difference is one day. For a subscription "valid for 1 month from purchase," this distinction determines whether the user gets March 1 or March 2 as their expiry.

The "30 Days from February 1" Ambiguity

Consider a trial period described as "30 days." Starting February 1 in a non-leap year:

  • Adding 30 days: March 2 (February has 28 days, so 28 days = March 1, +2 = March 2... wait: 28 days of Feb remains = 27 days remaining; day 28 is March 1; day 30 is March 3)

Let's be precise: Feb 1 + 30 days in 2023 (non-leap):

  • Feb has 28 days; Feb 1 + 27 days = Feb 28; + 3 more = March 3
  • So 2023-02-01 + 30 days = 2023-03-03

But 2023-02-01 + 1 month = 2023-03-01.

"30 days" and "1 month" differ by 2 days here. The correct interpretation depends on the business rule, and testers should verify which is implemented.

Year-Crossing Arithmetic

Adding days or months that cross a year boundary is straightforward in most libraries but can be tricky in custom implementations:

Operation Start Result
+35 days 2024-12-15 2025-01-19
+3 months 2024-11-30 2025-02-28 (Feb clamp)
+1 year 2024-02-29 2025-02-28 (Feb clamp, non-leap)
-1 month 2024-03-31 2024-02-29 (Feb clamp, leap)

Off-by-One in Duration Calculations

A common bug: when calculating the duration between two dates, should the start day count, the end day count, or both?

  • "A 7-day trial starts Monday" — does it expire Sunday night or Monday night?
  • "Grace period of 30 days after cancellation" — is the 30th day included?

The off-by-one matters for expiry checks. A system that uses > instead of >= in its expiry comparison lets users through for one extra day; one using >= instead of > cuts them off one day early.

Testing Checklist

  • Add 1 month to every month-end date (28th through 31st)
  • Add months that cross a year boundary (October + 3 months = January)
  • Subtract months from early-year dates (March - 3 months = December previous year)
  • Verify "30 days" vs "1 month" if both terms appear in business requirements
  • Test adding 1 year to February 29 of a leap year
  • Check inclusive/exclusive boundary of duration end dates

Quiz

What is the result of adding 1 month to January 31st in most date libraries?

When you add 1 year to February 29th, 2024 (a leap year), what is the expected result?

Which concept represents a fixed number of seconds, regardless of calendar irregularities like DST or leap years?

What is the correct test case to verify "30 days from February 28th" (non-leap year)?