Qalculate time hacks | Colin Watson's blog
Anarcat recently wrote about Qalculate, and I think I’m a convert, even though<br>I’ve only barely scratched the surface.
The thing I almost immediately started using it for is time calculations.<br>When I started tracking my time, I<br>quickly found that Timewarrior was good at<br>keeping all the data I needed, but I often found myself extracting bits of<br>it and reprocessing it in variously clumsy ways. For example, I often don’t<br>finish a task in one sitting; maybe I take breaks, or I switch back and<br>forth between a couple of different tasks. The raw output of timew<br>summary is a bit clumsy for this, as it shows each chunk of time spent as<br>a separate row:
$ timew summary 2025-02-18 Debian
Wk Date Day Tags Start End Time Total<br>W8 2025-02-18 Tue CVE-2025-26465, Debian, 9:41:44 10:24:17 0:42:33<br>next, openssh<br>Debian, FTBFS with GCC-15, 10:24:17 10:27:12 0:02:55<br>icoutils<br>Debian, FTBFS with GCC-15, 11:50:05 11:57:25 0:07:20<br>kali<br>Debian, Upgrade to 0.67, 11:58:21 12:12:41 0:14:20<br>python_holidays<br>Debian, FTBFS with GCC-15, 12:14:15 12:33:19 0:19:04<br>vigor<br>Debian, FTBFS with GCC-15, 12:39:02 12:39:38 0:00:36<br>python_setproctitle<br>Debian, Upgrade to 1.3.4, 12:39:39 12:46:05 0:06:26<br>python_setproctitle<br>Debian, FTBFS with GCC-15, 12:48:28 12:49:42 0:01:14<br>python_setproctitle<br>Debian, Upgrade to 3.4.1, 12:52:07 13:02:27 0:10:20 1:44:48<br>python_charset_normalizer
1:44:48<br>So I wrote this Python program to help me:
#! /usr/bin/python3
"""<br>Summarize timewarrior data, grouped and sorted by time spent.<br>"""
import json<br>import subprocess<br>from argparse import ArgumentParser, RawDescriptionHelpFormatter<br>from collections import defaultdict<br>from datetime import datetime, timedelta, timezone<br>from operator import itemgetter
from rich import box, print<br>from rich.table import Table
parser = ArgumentParser(<br>description=__doc__, formatter_class=RawDescriptionHelpFormatter<br>parser.add_argument("-t", "--only-total", default=False, action="store_true")<br>parser.add_argument(<br>"range",<br>nargs="?",<br>default=":today",<br>help="Time range (usually a hint, e.g. :lastweek)",<br>parser.add_argument("tag", nargs="*", help="Tags to filter by")<br>args = parser.parse_args()
entries: defaultdict[str, timedelta] = defaultdict(timedelta)<br>now = datetime.now(timezone.utc)<br>for entry in json.loads(<br>subprocess.run(<br>["timew", "export", args.range, *args.tag],<br>check=True,<br>capture_output=True,<br>text=True,<br>).stdout<br>):<br>start = datetime.fromisoformat(entry["start"])<br>if "end" in entry:<br>end = datetime.fromisoformat(entry["end"])<br>else:<br>end = now<br>entries[", ".join(entry["tags"])] += end - start
if not args.only_total:<br>table = Table(box=box.SIMPLE, highlight=True)<br>table.add_column("Tags")<br>table.add_column("Time", justify="right")<br>for tags, time in sorted(entries.items(), key=itemgetter(1), reverse=True):<br>table.add_row(tags, str(time))<br>print(table)
total = sum(entries.values(), start=timedelta())<br>hours, rest = divmod(total, timedelta(hours=1))<br>minutes, rest = divmod(rest, timedelta(minutes=1))<br>seconds = rest.seconds<br>print(f"Total time: {hours:02}:{minutes:02}:{seconds:02}")<br>$ summarize-time 2025-02-18 Debian
Tags Time<br>───────────────────────────────────────────────────────────────<br>CVE-2025-26465, Debian, next, openssh 0:42:33<br>Debian, FTBFS with GCC-15, vigor 0:19:04<br>Debian, Upgrade to 0.67, python_holidays 0:14:20<br>Debian, Upgrade to 3.4.1, python_charset_normalizer 0:10:20<br>Debian, FTBFS with GCC-15, kali 0:07:20<br>Debian, Upgrade to 1.3.4, python_setproctitle 0:06:26<br>Debian, FTBFS with GCC-15, icoutils 0:02:55<br>Debian, FTBFS with GCC-15, python_setproctitle 0:01:50
Total time: 01:44:48<br>Much nicer. But that only helps with some of my reporting. At the end of a<br>month, I have to work out how much time to bill Freexian for and fill out a<br>timesheet, and for various reasons those queries don’t correspond to single<br>timew tags: they sometimes correspond to the sum of all time spent on<br>multiple tags, or to the time spent on one tag minus the time spent on<br>another tag, or similar. As a result I quite often have to do basic<br>arithmetic on time intervals; but that’s surprisingly annoying! I didn’t<br>previously have good tools for that, and was reduced to doing things like<br>str(timedelta(hours=..., minutes=..., seconds=...) + ...) in Python,<br>which gets old fast.
Instead:
$ qalc '62:46:30 - 51:02:42 to time'<br>(225990 / 3600) − (183762 / 3600) = 11:43:48<br>I also often want to work out how much of my time I’ve spent on Debian work<br>this month so far, since Freexian pays me for up to 20% of my work time on<br>Debian; if I’m<br>under that then I might want to prioritize more Debian projects, and if I’m<br>over then I should be prioritizing more Freexian projects as otherwise I’m<br>not going to get paid for that time.
$ summarize-time -t :month Freexian<br>Total time: 69:19:42<br>$ summarize-time -t :month Debian<br>Total time: 24:05:30<br>$ qalc '24:05:30 / (24:05:30 + 69:19:42) to %'<br>(86730 / 3600) / ((86730 / 3600) + (249582 / 3600)) ≈ 25.78855349%<br>I love it.
Comments
With an account on...