Skip to content

Instantly share code, notes, and snippets.

@s4lt3d
Created June 18, 2025 17:43
Show Gist options
  • Select an option

  • Save s4lt3d/086f8e03c795603406276dc8e3f91bc9 to your computer and use it in GitHub Desktop.

Select an option

Save s4lt3d/086f8e03c795603406276dc8e3f91bc9 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
Generate a bi-weekly payroll calendar.
Rules
• “Last working day” is the final business day (Mon–Fri, not a holiday)
of each 14-day period.
• Pay date = exactly three business days before that last working day.
Run
python payroll_calendar.py
python payroll_calendar.py --start 2025-01-10 --periods 26 \
--holidays ca_holidays_2025.csv
"""
from __future__ import annotations
import argparse
from datetime import date, timedelta
from pathlib import Path
import csv
import sys
# ─────────────────────────── CONFIG (defaults) ────────────────────────────
ANCHOR_LAST_WORKING_DAY = date(2025, 6, 15) # first period end (Friday)
NUM_PERIODS = 26 # 26 bi-weekly periods/year
HOLIDAYS_FILE = None # optional csv/tsv of YYYY-MM-DD
# Example holiday fallback
# !!!! CHECK THESE DATES FOR ALBERTA !!!!
FALLBACK_HOLIDAYS = {
date(2025, 1, 1),
date(2025, 4, 18),
date(2025, 5, 19),
date(2025, 7, 1),
date(2025, 9, 1),
date(2025, 9, 30),
date(2025, 10, 13),
date(2025, 12, 25),
date(2025, 12, 26),
}
# ───────────────────────────────────────────────────────────────────────────
# ────────────────────────── core helpers ──────────────────────────────────
def load_holidays(path: str | None) -> set[date]:
"""Load YYYY-MM-DD strings (any csv/tsv) into a date set."""
if not path:
return set(FALLBACK_HOLIDAYS)
fp = Path(path)
if not fp.exists():
sys.exit(f"Holiday file not found: {fp}")
hol = set()
with fp.open(newline="") as f:
# accept plain list, csv, or tsv
reader = csv.reader(f, delimiter="," if "," in f.read(1024) else None)
f.seek(0)
for row in reader:
if not row:
continue
try:
y, m, d = map(int, row[0].strip().split("-"))
hol.add(date(y, m, d))
except Exception as e:
sys.exit(f"Bad holiday entry '{row[0]}': {e}")
return hol
def is_business_day(day: date, holidays: set[date]) -> bool:
return day.weekday() < 5 and day not in holidays
def prev_business_day(day: date, holidays: set[date]) -> date:
"""Move back to previous business day (if today is not one)."""
while not is_business_day(day, holidays):
day -= timedelta(days=1)
return day
def subtract_business_days(day: date, days: int, holidays: set[date]) -> date:
"""Go back exactly <days> business days from <day>."""
cur, done = day, 0
while done < days:
cur -= timedelta(days=1)
if is_business_day(cur, holidays):
done += 1
return cur
def generate_calendar(anchor: date,
periods: int,
holidays: set[date]) -> list[tuple[int, date, date]]:
"""
Return [(index, last_work_day, pay_date)] for <periods> bi-weekly spans
beginning with <anchor>.
"""
calendar = []
for idx in range(periods):
last_day = anchor + timedelta(days=14 * idx)
last_day = prev_business_day(last_day, holidays)
pay_day = subtract_business_days(last_day, 3, holidays)
calendar.append((idx + 1, last_day, pay_day))
return calendar
# ────────────────────────────── CLI ───────────────────────────────────────
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Bi-weekly payroll calendar")
p.add_argument("--start", type=str,
help="first period end (YYYY-MM-DD)",
default=str(ANCHOR_LAST_WORKING_DAY))
p.add_argument("--periods", type=int,
help="number of bi-weekly periods to build",
default=NUM_PERIODS)
p.add_argument("--holidays", type=str,
help="csv/tsv file of YYYY-MM-DD holiday dates")
return p.parse_args()
def main():
args = parse_args()
try:
y, m, d = map(int, args.start.split("-"))
anchor = date(y, m, d)
except Exception as e:
sys.exit(f"Bad --start date '{args.start}': {e}")
holidays = load_holidays(args.holidays)
cal = generate_calendar(anchor, args.periods, holidays)
print(f"{'Period':<6} {'Last work day':<15} Pay date")
print("-" * 40)
for idx, last_day, pay_day in cal:
print(f"{idx:<6} {last_day.isoformat():<15} {pay_day.isoformat()}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment