Time Tracking Using Plain Text and Drafts - Part 3 (Processing)

A plain text time slip is a good first step. We could simply treat it as paper and do the time calculations by hand (calculations made easier if we rounded each time using a button as described in the last post). But we can make Drafts do the work for us instead.

Below I’ll through the steps to turn a time slip like the one below into more useful data. I’m breaking the script I use up into a lot of pieces to help explain what it does over the course of a couple posts. But I’ll share the complete script in the final post to this series.

Here’s that time slip again:

2020-02-07 (Friday)

08:30 ABC Corp. - Review lease.

10:12 XYZ Co., motion to dismiss - Prepare for oral argument of upcoming motion to dismiss.

12:06 lunch

Had a great lunch at new sushi place.

13:00 ABC Corp. - Review and respond to addition email question from Mr. ABC.

13:30 Jones, meetings - Interview Ms. Jones regarding dispute with business partners.

14:48 Jones, strategy - Prepare memo outlining potential claims.

16:30 ABC Corp.

17:00 head home

Remember to buy wine on the way home!

We’ll begin by saving that time slip text into a constant:

const timeslip = editor.getText();

Then we’ll grab the date:

const date = timeslip.match(/^\d\d\d\d-\d\d-\d\d/g);

Then the times:

const times = timeslip.match(/^\d\d\:\d\d/gm);

Then the lines with the client, project, and description data:

var lines = timeslip.match(/^\d\d:\d\d.*\n?/gm);

(We drop the last line because it just has the time we stopped working).

Now we need to turn those lines into more structured data—a list of entries, each with a date, a duration, a client, a project if any, and a description. The below bit of the script just loops through those lines, creating an entry for each:

var entries = [];
for (i = 0; i < lines.length; i++) {
	entries[i] = {}
	entries[i].date = date;
	entries[i].duration = duration(times[i],times[i+1]);
	let name = lines[i].match(/.+?(?=(,|\n| - ))/g)[0].slice(6).trim(); // match beginning of line, as few as possible, to comma, new line, or space-hyphen-space
	entries[i].client = getClient(name).name; // standardize the client name
	entries[i].project = lines[i].match(/, [\w ]+(?=( \- |\n))/g); // match from comma space until you either get a new line or an isolated hyphen 
		if (entries[i].project === null) {
			entries[i].project = '';
		} else {
			entries[i].project = entries[i].project[0].slice(2).toLowerCase().trim();
	entries[i].note = lines[i].match(/- .*/g);   // match from hyphen space forward
		if (entries[i].note === null) {
			entries[i].note = '';
		}	else {
			entries[i].note = entries[i].note[0].slice(2).trim();
	entries[i].billable = getClient(entries[i].client).billable;
	entries[i].number = getClient(entries[i].client).number;

There are a couple of functions used in there that need to be explained.

First, there’s the duration function, which takes two times and returns the duration between them. That function is as follows:

// function to calculate durations from time-based strings
function duration(start, end) {
	var h1 = start.slice(0,2);
	var h2 = end.slice(0,2);
	var hours = h2 - h1;
	var m1 = start.slice(3);
	var m2 = end.slice(3);
	var tenths = (m2 - m1)/60;
	var totalTime = hours + tenths;
	return totalTime.toFixed(1);

Next, there’s a getClient function that takes a client name, standardizes it, and provides data associated with that name (like a billing number and whether the matter is billable or not). This getClient function isn’t strictly necessary, but it’s very nice to have.

At the end of all that processing we have a list called entries where each entry in the list has a date, a duration, a client, a project, a note, and (as a bonus) a billing number and a flag indicating whether the matter is billable or not.

But we aren’t done yet. In the next post, we’ll go through this list of entries, combine entries with the same client and project, and filter out entries for things we aren’t tracking (like “lunch”), sort the entries, and turn them into useful strings to send out of Drafts.

Ciaran Connelly @ciaran