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