data center, cable, information, technology, technician, server, system, room, engineer, work, rack, man, computer, network, industry, data, connection, equipment, fixing, optical, wireless, pc, data center, data center, data center, data center, data center, server. Working days per year API: the version that survives contact with reality
Photo by cookieone on Pixabay

Rules

Part of Why a year's working day total starts with 260, 261 or 262 weekdays

Working days per year API: the version that survives contact with reality

A working days per year API is a caching problem: pin the calendar version, define which year you mean, and make the response carry its own provenance.

An annual count is one integer between 260 and 262 before closures, and a slightly smaller integer after. Computing it is not the hard part. Serving it is, because the answer is stable for months and then changes for a reason that has nothing to do with the caller.

What to take away

  • The word "year" has at least three meanings on the wire. Say which one the request means, or the service is answering a different question from the one asked.
  • Pin the calendar version in the request and echo it in the response. An annual count with no calendar version is an integer with no meaning.
  • Cache aggressively, but key the cache on the calendar version rather than on the year, so a list change invalidates exactly the answers it should.

Three different years

Boundary

Calendar year
1 January to 31 December
Fiscal year
Whatever the organization set
ISO week year
Monday of week 1 to Sunday of week 52 or 53

Length

Calendar year
365 or 366 days
Fiscal year
Usually 365 or 366, occasionally not
ISO week year
364 or 371 days

Those are genuinely different spans. An ISO week year of 53 weeks is a full week longer than one of 52, and its boundaries fall inside the neighboring calendar years: 1 January 2027 sits in the last ISO week of 2026.

Three meanings of year

Calendar year

Boundary
Jan 1 to Dec 31
Length
365 or 366 days
Parameter
year: 2027

Fiscal year

Boundary
Organization set
Length
Usually 365 or 366
Parameter
year + fiscal_start

ISO week year

Boundary
Mon week 1 to Sun
Length
364 or 371 days
Parameter
year + year_type

A parameter named year that takes 2027 and nothing else has chosen the first meaning silently. Better to take a span, or to take a year plus an explicit year_type, and to reject a request that does not say.

The request


<figure class="inline-figure">
  <img src="/inline/working-days-per-year-api-fig2-1280.webp" alt="Checklist of request parameters including year type, weekend string and calendar version (Working days per year API: the version that survives contact with reality)" loading="lazy" decoding="async" />
  <figcaption>Every field here exists to stop the endpoint guessing what the caller meant. <a href="https://workdaydatecalculator.com/image-licence">Image: Workday Date Calculator</a></figcaption>
</figure>
year:          2027
year_type:     calendar | fiscal | iso
fiscal_start:  "04-01"                (fiscal only)
weekend:       "0000011"              (Monday to Sunday, 1 marks a rest day)
calendar:      { id: "...", version: "..." }
include:       weekdays | working

[weekend](/country-calendars-google-sheets) as a seven-character string is worth copying from spreadsheets: compact, log-readable, covers every convention without an enumeration to extend.

include separates two questions the endpoint can answer: weekday count is pure arithmetic; working count is arithmetic plus somebody's list.

The response

result:        261
weekdays:      261
closures:      0
applied:       { year_type, span: { start, end }, weekend, calendar }
skipped:       [ ...the closure dates that actually reduced the count... ]

Return the weekday base and the deduction as separate fields even when the caller asked for one number.

They move for different reasons: the base changes because the year starts on a different day, the deduction because a list changed. A caller who sees a total move by two cannot tell which happened; a caller who sees both can.

The skipped list is the working shown. It contains only the closures that reduced the count, so a weekend closure that had no effect is absent, and its absence is the answer to the most common support question about these endpoints.

Caching

The answer for a given year, weekend convention and calendar version never changes. That makes it ideal to cache and dangerous to cache by year alone.

Key on the whole tuple. When the calendar publisher issues a new version, the old entries stay valid for callers still pinned to the old version, and new requests get new entries. Nothing has to be purged, and no caller sees an answer change under them without asking for it.

Serve the calendar version as data the caller can pin to, and let them pin. A caller that always takes the newest list gets a total that moves mid-year with no deployment on their side, which is correct for some callers and a reconciliation failure for others. Let them choose, and record which they chose.

The same call with the same inputs must always return the same result, which is idempotence in its plain sense and is what makes the endpoint safe to retry, safe to cache and safe to compare across two runs.

Test vectors

RequestExpectWhat it catches
2027, calendar, weekdays261Common year starting Friday
2028, calendar, weekdays260Leap year starting Saturday, fewer than the year before
2027, weekend 0000110260A Friday and Saturday rest convention
2027, weekend 11111110Every day a rest day, defined rather than an error
2027, sum of twelve monthly calls261Month boundaries, which is where an off-by-one lives

The fourth row is the one implementations disagree on. Returning zero is defensible, and rejecting the request is too. Returning 365 is not: that happens when the weekend string is parsed but never applied.

The reasoning behind the expected totals is under the annual formula. The monthly figures the last row sums are under counting a month.

What the endpoint should refuse

Refuse a request with no calendar version when include is working, because there is no default calendar that is right for two callers. Refuse a fiscal year with no start. Refuse a weekend string that is not seven characters of 0 and 1, rather than padding or truncating it.

Where the closure list itself comes from, and how it should be versioned before it ever reaches an interface, is under how closure calendars are made and, for layered jurisdictions, state and regional calendars.

Common questions

Should the endpoint accept a range instead of a year?

It should accept both. A range is the general case and a year is the common one, and supporting only the general case pushes the year-boundary question onto every caller.

Is it worth returning the list of working dates?

For a year, rarely: it is 261 dates for an answer of 261. Return the closures that applied instead, which is short and is what a caller needs to check the number.

What should happen when a calendar version is retired?

Keep answering for it and mark the response as pinned to a retired version. Failing the request turns a data change into an outage for callers who did nothing wrong.

Can the caller send their own closure list?

That is the honest design for organizations with their own calendar. Accept a list, echo it back in applied, and treat it as a calendar with the caller as its publisher.

More in Rules

Latest from Planning Desk