Date calculation is not timestamp subtraction

Dates look ordered, so the tempting implementation is to convert both values to timestamps and divide by 86,400,000. That works in many demos and fails where trust matters most: time-zone changes, daylight-saving transitions, nonexistent local times, inclusive counting, and regional holiday rules. A reliable date utility starts by classifying the question, not by reaching for a timestamp.

“How many days from October 1 to October 3?” can mean two elapsed boundaries or three included calendar dates. “Thirty business days later” requires weekends and a holiday set. Date Calculator should keep these meanings separate in its domain model and state the selected rule beside the answer.

Use a type that preserves meaning

A date without a time belongs in a LocalDate-like type, not an instant at midnight. An instant is one absolute point on the timeline; a local date is a cell on a calendar. Encoding birthdays, contract dates, or holidays as midnight instants accidentally attaches a time zone and can display the previous date elsewhere.

DateRange = { start: LocalDate, end: LocalDate, inclusion: "exclusive" | "inclusive" }
BusinessCalendar = { weekendDays, holidays, region }

Convert to an instant only when the requirement truly includes a moment, such as a reminder at 09:00. That model needs a local date, local time, and zone. A fixed offset alone is insufficient for future schedules because a region’s daylight-saving rules can change.

Inclusive and exclusive are product decisions

An off-by-one result is often an undefined requirement rather than bad arithmetic. Half-open ranges [start, end) are convenient in code: their length is the number of day boundaries and adjacent ranges do not overlap. People planning leave commonly want both the first and final dates counted. The interface should offer an explicit mode or use domain language that makes the choice obvious instead of silently adding one.

  • Date difference measures calendar boundaries between two values.
  • Count dates counts the calendar cells included in a selection.
  • Add days moves a date by N calendar steps.
  • Business days filter those steps through a defined work calendar.

Holiday data needs a version

Weekends can usually be rules, while public holidays are data scoped by country, year, and sometimes late government decisions. Do not hard-code one permanent array in the presentation layer. Treat a holiday calendar as a dataset with a region, version, source, and update time. When a year is unsupported, explain that limitation instead of silently treating every weekday as a workday.

Personal exceptions should remain separate from official holidays. A user may add a company shutdown or remove a public holiday on which they still work. Calculation can receive one merged BusinessCalendar with documented precedence, keeping the date algorithm testable without storage or network access.

Invalid dates expose policy

Adding one month to January 31 is a policy question: February 28, March 3, or an error? Many libraries clamp to the last valid day, but a library default should not accidentally become a product promise. Define addition separately for days, weeks, months, and years, then test leap years, month ends, and year boundaries.

addMonths(2028-01-31, 1, policy="clamp") -> 2028-02-29
addYears(2028-02-29, 1, policy="clamp") -> 2029-02-28

Reminders introduce a second time model

When a result becomes an event or reminder, LocalDate must meet local time and time zone. “Nine in the morning on that date” may stay at 09:00 wherever the user travels, while a global call should preserve one instant. Those are distinct behaviors. Labels and storage need to make the choice durable.

Notification permission, schedule changes, device restarts, and background limits belong in an orchestration layer. Keep calculation pure. A notification adapter receives an already-defined event and deals with the operating system, which makes both sides easier to verify.

Property tests cover a large calendar

A few examples cannot span the date space. Property-based tests can generate thousands of pairs and verify invariants: distance from A to A is zero; reversing inputs reverses a signed distance; adding N days and subtracting N returns the start; a business-day result never lands on an excluded date. Boundary examples remain essential for February 29, month ends, and daylight-saving transitions.

Copy deserves tests too. A numerically correct result with an ambiguous label is still a user-facing defect. Assert that inclusive mode says both endpoints are included and that holiday-aware results identify the regional calendar used.

Make the result inspectable

Date tools often emphasize the large number and hide the method. A more trustworthy result includes a compact explanation: start and end dates, whether endpoints count, excluded weekends, excluded holidays, and the calendar region. Screen readers should announce the result before secondary controls, while copied or shared output should carry the same assumptions in plain text. This explanation also becomes a debugging artifact when someone reports a discrepancy. Inspectability is especially valuable for business-day calculations, where two correct systems can disagree because they use different holiday datasets rather than different arithmetic.

Accuracy begins with language

A trustworthy date app is not the one with the most formulas. It translates a natural question into a defined operation, puts assumptions next to the result, and keeps LocalDate, instant, holiday calendar, and reminder concepts separate. When the model reflects user language, arithmetic is often straightforward. When the model is vague, no date library can rescue the experience.