AXLR / Catalog / administration

business_days

Count the working days between two dates, add or subtract working days to a date, or list them, skipping weekends and a given holiday list. Deadlines, SLAs and administrative terms.

Install

/tools install business_days
/tools install business_days@1.0.0

In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · business_days 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool business_days; each run is approved as a local_exec.

About

Working-day arithmetic for deadlines and administrative terms: count the working days in an inclusive range, add or subtract working days to a date, or list them. Weekends default to Saturday and Sunday (weekend takes other ISO weekdays, such as [5, 6] for Friday–Saturday) and holidays is an explicit list, because public holidays differ by country, region and year and a bundled table would silently go stale.

Limits: no half days; "add 0" returns the start date even when it is not a working day.

Arguments

One JSON object on stdin, validated against the schema before the program starts.

nametyperequireddefaultmeaning
operationstringyesone of "count", "add", "list"
startstringyespattern ^\d{4}-\d{2}-\d{2}$
endstringnoInclusive; count and list. pattern ^\d{4}-\d{2}-\d{2}$
daysintegernoWorking days to add (negative subtracts); add
holidaysarray of stringno[]
weekendarray of integerno[6, 7]ISO weekdays that are not working days
Input schema (JSON)
{
  "type": "object",
  "required": [
    "operation",
    "start"
  ],
  "additionalProperties": false,
  "properties": {
    "operation": {
      "type": "string",
      "enum": [
        "count",
        "add",
        "list"
      ]
    },
    "start": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    },
    "end": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
      "description": "Inclusive; count and list"
    },
    "days": {
      "type": "integer",
      "description": "Working days to add (negative subtracts); add"
    },
    "holidays": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
      },
      "default": []
    },
    "weekend": {
      "type": "array",
      "items": {
        "type": "integer",
        "minimum": 1,
        "maximum": 7
      },
      "default": [
        6,
        7
      ],
      "description": "ISO weekdays that are not working days"
    }
  }
}

Example

Input

{
  "operation": "add",
  "start": "2026-12-23",
  "days": 10,
  "holidays": [
    "2026-12-25",
    "2027-01-01",
    "2027-01-06"
  ]
}

Output (stdout)

{
  "result": "2027-01-11"
}

Files

The command the install card registers is python3 business_days.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.

business_days.py · sha256 9945f79f9b2d1b76bd8566ed2be5e90f87dbf33ed7353b5abc03d3c13c2d8920
#!/usr/bin/env python3
"""Business-day arithmetic: count working days between dates, add working days to a date,
or list the working days of a range. Weekends and an explicit holiday list are skipped.

Input (JSON on stdin):
  operation  "count" | "add" | "list"
  start      "YYYY-MM-DD"
  end        "YYYY-MM-DD"   (count, list; inclusive)
  days       integer        (add; may be negative)
  holidays   ["YYYY-MM-DD", ...]   optional
  weekend    [6, 7]         optional ISO weekdays that are not working days (default Saturday, Sunday)
"""
import datetime as dt
import json
import sys


def fail(message):
    print(json.dumps({"error": message}))
    sys.exit(2)


def parse(value, field):
    try:
        return dt.date.fromisoformat(value)
    except (TypeError, ValueError):
        fail(f"{field} must be a date YYYY-MM-DD")


def main():
    try:
        request = json.load(sys.stdin)
    except ValueError as error:
        fail(f"stdin is not JSON: {error}")
    operation = request.get("operation")
    weekend = set(request.get("weekend", [6, 7]))
    holidays = {parse(h, "holidays[]") for h in request.get("holidays", [])}
    working = lambda d: d.isoweekday() not in weekend and d not in holidays
    start = parse(request.get("start"), "start")
    if operation in ("count", "list"):
        end = parse(request.get("end"), "end")
        if end < start:
            fail("end is before start")
        days = [start + dt.timedelta(i) for i in range((end - start).days + 1)]
        found = [d for d in days if working(d)]
        result = {"operation": operation, "start": start.isoformat(), "end": end.isoformat(), "business_days": len(found),
                  "calendar_days": len(days)}
        if operation == "list":
            result["days"] = [d.isoformat() for d in found]
    elif operation == "add":
        days = request.get("days")
        if not isinstance(days, int):
            fail("days must be an integer")
        step = 1 if days >= 0 else -1
        current, left = start, abs(days)
        while left:
            current += dt.timedelta(step)
            if working(current):
                left -= 1
        result = {"operation": "add", "start": start.isoformat(), "days": days, "result": current.isoformat(),
                  "weekday": current.strftime("%A")}
    else:
        fail("operation must be count, add or list")
    print(json.dumps(result))


if __name__ == "__main__":
    main()

Where it is served

The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:

curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/business_days/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/business_days/1.0.0/business_days.py