
Maintenance
Part of Days between dates: six honest answers to one simple-looking pair
Days between dates API: signal versus noise
Designing a days between dates API: plain dates against moments, the request and response fields worth echoing, and vectors that catch a merged type.
An interface that takes two dates and returns a number is answering one of two different questions, and the request has to say which. The difference between two dates is an integer. The difference between two moments is a duration, and dividing it by 24 does not turn it back into the integer.
This page sets out the two types, the clock-change arithmetic that separates them, a request and response shape that keeps them apart, and the test vectors that expose an implementation that merged them. No service is named.
What to take away
- A plain date has no zone and no time. A moment has both. Keep them as separate types on the wire and in the code.
- Between two local midnights on a clock-change day there are 23 or 25 hours, not 24. Any "days between" derived from moments is wrong by an hour somewhere, and by a day at the boundary.
- Working-day counts need a calendar with a version, and the response should echo it.
Two types, two answers
Type
2027-03-05and2027-03-08- Plain dates
2027-03-05T15:00-05:00and2027-03-08T11:00-05:00- Moments
Difference is
2027-03-05and2027-03-08- An integer number of days
2027-03-05T15:00-05:00and2027-03-08T11:00-05:00- A duration
Example
2027-03-05and2027-03-08- 3
2027-03-05T15:00-05:00and2027-03-08T11:00-05:00- 68 hours
The first answer is exact and has no zone. The second is exact and has one. What is never exact is converting the second into the first by dividing hours by 24, because the local day is not always 24 hours long.
Plain dates vs moments
Plain dates
- Input
- 2027-03-05
- Difference is
- Integer days
- Example
- 3
- Zone
- None
- Divide by 24
- Never needed
Moments
- Input
- 2027-03-05T15:00-05:00
- Difference is
- A duration
- Example
- 68 hours
- Zone
- Required
- Divide by 24
- Never exact
Modern date libraries make the distinction a type: a plain date object that cannot hold a time, and an instant object that cannot be read as a date without a zone. The Temporal proposal for JavaScript is one published design of that separation, and whatever language the service is in, the same split is the one to copy.
What clock changes do to a moment-based count
In a zone that moves its clocks, in 2027, with each day measured from local midnight to the next local midnight:
| From | To | Hours |
|---|---|---|
| 13 March | 14 March | 24 |
| 14 March | 15 March | 23 |
| 6 November | 7 November | 24 |
| 7 November | 8 November | 25 |
| 1 March | 1 April | 743 |
The last row is a whole month one hour short of 31 times 24.
A service that computes "days between" as hours divided by 24 returns 30.96 for March, then rounds or truncates, deciding something the caller did not ask.
A service that subtracts plain dates returns 31 and has no rounding to do.
The figures above are for a zone with a spring-forward in March and a fall-back in November; the pattern is the same in any zone that changes, with the dates shifted to that zone's own rules.
Zones move the date itself
2027-03-05T23:30:00-05:00 is 04:30 on 6 March in UTC. 2027-03-06T08:30:00+09:00 is 23:30 on 5 March in UTC. A service that receives a moment, converts it to UTC, and reads off the date part has moved one of those from Friday to Saturday and the other from Saturday to Friday.
For a between-dates operation the caller must either send plain dates, already read off in the governing zone, or send moments and name the zone the dates should be read in. A moment with no instruction about the zone is a date with two possible values.
The request
operation: between
mode: dates | moments
start: 2027-03-05 (mode dates)
end: 2027-03-08
zone: "America/Chicago" (mode moments only: the zone to read dates in)
unit: calendar | working | hours
start_counts: false
end_counts: true
weekend: "0000011" (working only, Monday to Sunday)
calendar: { id, version } (working only)
window: { open: "09:00", close: "17:00" } (hours only)
Date and moment text should follow the ISO 8601 profile, YYYY-MM-DD for dates and YYYY-MM-DDThh:mm:ss with an offset for moments; the W3C note on date and time formats sets out the profile most interfaces already accept. Reject any moment without an offset. Reject any plain date that carries a time.
The response
result: 3
unit: calendar
applied: { mode, zone, start_counts, end_counts, weekend, calendar }
skipped: [ ...for working mode, the rest days and closures inside the span... ]
The applied block is the answer's provenance. The skipped list is the working shown, and it is where a wrong closure list becomes visible without anyone recomputing anything.
Test vectors
All March 2027, Saturday-Sunday rest days, nothing closed unless stated.
| Mode | Start | End | Unit and convention | Expect |
|---|---|---|---|---|
| dates | Friday 5th | Monday 8th | calendar, exclusive | 3 |
| dates | Friday 5th | Monday 8th | calendar, both ends in | 4 |
| dates | Monday 1st | Monday 8th | working, both ends in | 6 |
| dates | Monday 1st | Monday 15th | working, both ends in, Wednesday 10th closed | 10 |
| dates | Saturday 6th | Sunday 7th | working, both ends in | 0 |
| moments | Friday 5th 15:00 | Monday 8th 11:00 | hours, 09:00 to 17:00 window | 4 |
| moments | Friday 5th 15:00 | Monday 8th 11:00 | elapsed | 68 |
| moments | 14 March 00:00 local | 15 March 00:00 local, changing zone | elapsed | 23 |
An implementation that returns 24 on the last row is computing in a fixed offset rather than a zone. One that returns 2.83 or 3 for the first row from moments has merged the two types.
Where this sits
The six readings a pair of dates can carry, before any interface is involved, are under counting between two dates. What changes when the two dates are in two countries, the working week and the closure list and the zone, is under counting across borders. The hour operation, and the spreadsheet formula that reproduces it, is under working hours.
The serial-versus-text contract when a sheet is one end of the call is under spreadsheet and API tools. The calendar the working mode needs, with its publisher and as-of date, is under how country calendars are made.
Common questions
Should the service default to a zone when none is sent?
No. A default zone is a silent assumption that produces a different date for callers in different places. Reject the request.
Can one call return both the date count and the hour count?
It can, and the two should be labeled as different quantities with different units rather than presented as one number and a conversion.
How should the service handle a start later than the end?
Return a negative count or an error, and say which in the documentation. Silently swapping the arguments hides a caller's mistake.
Is the skipped list needed for calendar mode?
No; nothing is skipped. It is needed for working mode, where it is the only part of the answer a reader can check against the calendar by eye.







