新增 AutoRDO 需求清洗工作台与消息中枢,迭代 OneOS V2 设计规范及租赁合同/工作台/车辆等原型,同步云效技能与导航注册;并归档一批 legacy 原型快照。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
92
.claude/skills/YunxiaoPM/scripts/workday_hours.py
Executable file
92
.claude/skills/YunxiaoPM/scripts/workday_hours.py
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compute YunxiaoPMapp stage calendar hours = workdays × 8.
|
||||
|
||||
Usage:
|
||||
python3 workday_hours.py YYYY-MM-DD YYYY-MM-DD
|
||||
python3 workday_hours.py YYYY-MM-DD YYYY-MM-DD --calendar /path/to/cn-workday-calendar.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_day(s: str) -> date:
|
||||
return datetime.strptime(s, "%Y-%m-%d").date()
|
||||
|
||||
|
||||
def load_calendar(path: Path) -> dict:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
years = data.get("years") or {}
|
||||
holidays: set[str] = set()
|
||||
makeup: set[str] = set()
|
||||
for _y, block in years.items():
|
||||
holidays.update(block.get("holidays") or [])
|
||||
makeup.update(block.get("workdays_on_weekend") or [])
|
||||
return {"holidays": holidays, "makeup": makeup, "years": set(years.keys())}
|
||||
|
||||
|
||||
def ensure_years_covered(start: date, end: date, years: set[str]) -> None:
|
||||
y = start.year
|
||||
while y <= end.year:
|
||||
if str(y) not in years:
|
||||
raise SystemExit(
|
||||
f"缺少 {y} 年日历:请在 assets/cn-workday-calendar.json 补全后再计算。"
|
||||
"禁止仅去周末静默估算。"
|
||||
)
|
||||
y += 1
|
||||
|
||||
|
||||
def is_workday(d: date, holidays: set[str], makeup: set[str]) -> bool:
|
||||
key = d.isoformat()
|
||||
if key in holidays:
|
||||
return False
|
||||
if key in makeup:
|
||||
return True
|
||||
return d.weekday() < 5 # Mon=0 .. Fri=4
|
||||
|
||||
|
||||
def count_workdays(start: date, end: date, holidays: set[str], makeup: set[str]) -> int:
|
||||
if end < start:
|
||||
raise SystemExit("结束日早于开始日")
|
||||
n = 0
|
||||
cur = start
|
||||
while cur <= end:
|
||||
if is_workday(cur, holidays, makeup):
|
||||
n += 1
|
||||
cur += timedelta(days=1)
|
||||
return n
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="阶段日历工时 = 工作日×8")
|
||||
parser.add_argument("start", help="计划开始 YYYY-MM-DD")
|
||||
parser.add_argument("end", help="计划完成 YYYY-MM-DD")
|
||||
parser.add_argument(
|
||||
"--calendar",
|
||||
default=str(Path(__file__).resolve().parent.parent / "assets" / "cn-workday-calendar.json"),
|
||||
help="日历 JSON 路径",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
start = parse_day(args.start)
|
||||
end = parse_day(args.end)
|
||||
cal = load_calendar(Path(args.calendar))
|
||||
ensure_years_covered(start, end, cal["years"])
|
||||
days = count_workdays(start, end, cal["holidays"], cal["makeup"])
|
||||
hours = days * 8
|
||||
out = {
|
||||
"start": start.isoformat(),
|
||||
"end": end.isoformat(),
|
||||
"workdays": days,
|
||||
"stage_calendar_hours": hours,
|
||||
"label": "阶段日历工时(工作日×8),非人力投入预估",
|
||||
}
|
||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user