Functional Weave
Code in Rust

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;
}