dates.business-days-between@1.0.0
impl/typescript.ts
2,383 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { epochDayFromIso } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { dayOfWeek } from "./dates_day_of_week.ts"; ← from dates.day-of-week ^1.0.0 · built alongside by fune
/**
* Working days between two dates, excluding Saturdays, Sundays and any date in
* the holiday list.
*
* The interval is half-open: the start date counts, the end date does not.
* That is the convention that makes ranges compose - businessDaysBetween(a, b)
* plus businessDaysBetween(b, c) equals businessDaysBetween(a, c) - and it
* makes "how many working days until the deadline" come out at zero on the
* deadline itself rather than one. If you want the end date included, ask for
* the day after it.
*
* A start later than the end returns a negative count, and the magnitude is
* the same as the forward direction: businessDaysBetween(a, b) is exactly
* -businessDaysBetween(b, a). Returning zero or throwing would both hide a
* caller's argument-order bug.
*
* Holidays are an argument, not built in, because no library knows which days
* your company is closed. Weekend holidays are not double counted, duplicates
* in the list are harmless, and holidays outside the interval are ignored.
*/
export function businessDaysBetween(startIso: string, endIso: string, holidays: readonly string[] = []): number {
const start = epochDayFromIso(startIso);
const end = epochDayFromIso(endIso);
// Every holiday is validated even when it falls outside the interval: a typo
// in a holiday calendar should fail loudly on the next run, not lie dormant
// until the year the date is finally inside a query.
const excluded = new Set<number>();
for (const holiday of holidays) excluded.add(epochDayFromIso(holiday));
if (start > end) return -countWorkingDays(end, endIso, start, excluded);
return countWorkingDays(start, startIso, end, excluded);
}
/** Working days in [from, to), given the weekday of `from` as an ISO date. */
function countWorkingDays(from: number, fromIso: string, to: number, excluded: Set<number>): number {
// The weekday is carried forward rather than recomputed per day: one date
// parse, then a seven-day cycle, which is the same loop in all three
// languages and cannot drift between them.
let weekday = dayOfWeek(fromIso);
let count = 0;
for (let day = from; day < to; day++) {
if (weekday <= 5 && !excluded.has(day)) count++;
weekday = weekday === 7 ? 1 : weekday + 1;
}
return count;
}