feat: split overlapping minutes between projects
Five minutes with two projects open was counted as five for each, so Wednesday's columns summed to 18h against 13h10 of wall clock and no column said which number to trust. Each slot's minutes are now divided evenly among the projects live in it, largest remainder over whole minutes, so the columns add up to the total exactly. That ratio is what a submission scales the user's stated day length by. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,8 +48,13 @@ time that project was actually active, plus wall-clock `total`, the day's
|
||||
`window`, and `flags`. That table is the deliverable — paste it as-is rather
|
||||
than restating it in prose.
|
||||
|
||||
`total` is the union of active slots, so on a day with parallel sessions it is
|
||||
less than the row sum. Both numbers are true; they answer different questions.
|
||||
Overlap is divided, not double-counted: five minutes with two projects open is
|
||||
five minutes of the day, half to each. The project columns therefore add up to
|
||||
`total`, which is wall-clock presence.
|
||||
|
||||
That division is also the ratio to submit with. The user gives the day's real
|
||||
total; you split it by these proportions. Never submit the measured numbers as
|
||||
the hours unless the user says they are right.
|
||||
|
||||
Paste it as plain markdown in the reply. Never wrap it in a code fence: a
|
||||
fence shows the raw pipes and dashes instead of a rendered table.
|
||||
|
||||
@@ -22,8 +22,9 @@ a guessed number is worse than a small true one.
|
||||
Measured time is a floor. Meetings, review and thinking leave no transcript,
|
||||
so the user adds those back; the script never does.
|
||||
|
||||
The 'total' column is wall-clock presence, the union of active slots across
|
||||
projects, so on days with parallel sessions it is less than the row sum.
|
||||
Overlap is divided, not double-counted: five minutes with two projects open is
|
||||
five minutes of the day, half to each. So the project columns add up to the
|
||||
'total' column, which is wall-clock presence.
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
@@ -114,6 +115,21 @@ def read_session(path):
|
||||
return cwd, stamps
|
||||
|
||||
|
||||
def attribute(slots_of, owners, mins):
|
||||
"""Divide each slot's minutes evenly among the projects live in it.
|
||||
|
||||
Five minutes with two projects open is five minutes of the user's day, not
|
||||
ten, so each project gets half. Largest remainder over whole minutes keeps
|
||||
the parts summing to wall clock exactly.
|
||||
"""
|
||||
exact = {p: sum(mins / owners[s] for s in ss) for p, ss in slots_of.items()}
|
||||
floors = {p: int(v) for p, v in exact.items()}
|
||||
left = round(sum(exact.values())) - sum(floors.values())
|
||||
for p in sorted(exact, key=lambda p: -(exact[p] - floors[p]))[:left]:
|
||||
floors[p] += 1
|
||||
return floors
|
||||
|
||||
|
||||
def fmt_dur(minutes):
|
||||
"""45 -> '45min', 155 -> '2h35', 180 -> '3h', 0 -> '—'."""
|
||||
if not minutes:
|
||||
@@ -216,22 +232,27 @@ def main():
|
||||
d = since
|
||||
while d <= until:
|
||||
per = grid.get(d.isoformat(), {})
|
||||
totals = {p: sum(len(s) for s in hrs.values()) for p, hrs in per.items()}
|
||||
# Wall clock, not the row sum: a slot with two projects live is one
|
||||
# minute of the user's day, counted once here and once per project.
|
||||
union = len({s for hrs in per.values() for ss in hrs.values()
|
||||
for s in ss})
|
||||
slots_of = {p: {s for ss in hrs.values() for s in ss}
|
||||
for p, hrs in per.items()
|
||||
if not p.startswith("unmapped:")}
|
||||
owners = collections.Counter()
|
||||
for ss in slots_of.values():
|
||||
owners.update(ss)
|
||||
share = attribute(slots_of, owners, mins)
|
||||
entry = {
|
||||
"date": d.isoformat(),
|
||||
"weekday": d.strftime("%a"),
|
||||
"active_minutes": union * mins,
|
||||
"active_minutes": len(owners) * mins,
|
||||
"projects": [],
|
||||
}
|
||||
for proj, n in sorted(totals.items(), key=lambda kv: -kv[1]):
|
||||
raw = {p: sum(len(s) for s in hrs.values()) * mins
|
||||
for p, hrs in per.items()}
|
||||
for proj in sorted(per, key=lambda p: -raw[p]):
|
||||
hrs = sorted(per[proj])
|
||||
entry["projects"].append({
|
||||
"project": proj,
|
||||
"active_minutes": n * mins,
|
||||
"active_minutes": share.get(proj, raw[proj]),
|
||||
"raw_minutes": raw[proj],
|
||||
"first_hour": hrs[0],
|
||||
"last_hour": hrs[-1],
|
||||
"outside_workday": [
|
||||
@@ -256,7 +277,7 @@ def main():
|
||||
print(f"{since} .. {until} — {nfiles} sessions, {mins}-min slots, "
|
||||
f"{cfg['timezone']}")
|
||||
print("measured active time, nothing extrapolated; "
|
||||
"total is wall clock, so it can be less than the row\n")
|
||||
"overlapping minutes split evenly between projects\n")
|
||||
|
||||
cols = [p for p in dict.fromkeys(
|
||||
x["project"] for e in report for x in e["projects"])
|
||||
|
||||
Reference in New Issue
Block a user