Saturday, 19 October 2024

Project1 - HowMuchIsThisMeetingCosting - Mistakes were made!

I am trying to level up my front-end development skills as part of a plan to build a side-hustle business. In the previous posts I have been discussing my first project  - howmuchisthismeetingcosting.com - a simple front-end only site for calculating the live cost of a meeting.

I was excited to finish and deploy this first project. Getting a side project - even a simple one - over the finish line is no small  feat.  In software development the Pareto Principle is often considered to apply: finishing the last 20% of a solution can take 80% of your effort.  For a side project or a self-training project it is all too easy to avoid this overhead and quit when the project is 'good enough', but not done.

I am completely guilty of this. I am a serial project starter and rarely get something finished before I get distracted by something shiny. One of the drives behind picking a simple project was to pick a task that wouldn't be too challenging to finish, and to start trying to get out of the bad habit of abandoning projects before they are done.

This is one of the core ideas from the book 'Atomic Habits' by James Clear.  To build positive habits or get rid of bad ones,  start by making easy to achieve changes and do them enough that it becomes automatic. Set the cadence with easy to achieve actions and then add complexity/challenge _after_ the desired behaviour has become automatic and habitual.  In my case I want to start building a habit of actually finishing projects - and one way to do this is to make sure that I start with projects that aren't overly challenging to get done.

Given all that, I was pleased to get my first project over the line.I was so excited that I couldn't wait to show it off.  Look friends - I actually finished something!  Cheers and Congratulations to me! 

Then, about 30 seconds after trying to show the site off, a good friend of mine pointed out that the meeting cost numbers were not making any sense. The site was miscalculating the cost by a factor of around 100. The web site - which was intended to be a small blow for the cubicle worker against management bureaucracy and waste - was making it seem like pointless meetings were 100 times cheaper than they should be!

In my excitement to get something finished, I used the 'hold my beer' approach to software review and testing . The site was simple enough - and it seemed to be working - surely eyeballing it would be fine, right? Wrong.

At the heart of this website is a simple typescript class that is responsible for maintaining the running state of the meeting timer (src/components/utilities/timerstate.ts in the source).  This class is a stateful service - a javascript singleton - that keeps track of the number of seconds that have elapsed in the meeting and use this to construct a string that displays the elapsed meeting cost, this string is then stored in a reactive variable that updates the HTML display.  

In the original version, that was all done in a single method. The method was about 20 lines long and looked simple enough.  What could go wrong?    Well obviously something did. 

To construct the meeting cost,  I calculate the total cost of the meeting in cents,  use a floor division by 100 to find the dollar component, then use the remainder to construct the cents string.

let cost_per_second = (this.avg_cost_per_hour * this.no_participants) / 3600;
let total_cost = cost_per_second * this.number_of_seconds;
let dollars = Math.floor(total_cost / 100);
let cents = Math.floor(total_cost - dollars * 100);
let dollarString = dollars.toString();
let centsString = cents.toString().padStart(2, "0");
this.cost_display_string.value = this.currency_symbol + dollarString + "." + centsString;


This issue here of cause is that I am actually calculating the total_cost as dollars not cents which is why the cost is out by a factor of 100.

The fix here is simple, but the fact that it occurred is not great. There are several contributing factors:
  • there are no unit tests. There wasn't even a test framework installed in the project
  • even if there were unit tests, this code isn't written to be well tested. It is making unpure calculations that change the state of the class, making them hard to isolate  
  • the variables are poorly named. if I expect the total cost to be in cents, it should be named something like 'total_cost_cents'.
In my rush to develop a good habit (getting something finished) I didn't look at reviewing this code, or taking the time to perform even basic unit testing.  For a simple site like this the harm was just my embarrassment, but if I want to eventually have my own paying customers then I need to do much better.

(there is a tension between learning new things and building robust things to navigate here. I think it is OK to make mistakes when learning, but the failure here was not being disciplined enough reviewing the solution before calling it 'done' and shipping it. This is probably much like writing a book. I have read that is OK to make mistakes in your first draft and being overly picky at first can probably break your flow, but that you need to be rigorous about editing once the draft is done.)

Anyway, deploying a broken site is unacceptable and the must rot stop here! Although this was just a 1 line fix, I decided to do things better.  I refactored that class for testability by moving the calculations into pure functions, and for readability by improving the variable names.  I then installed vitest and wrote some simple unit tests to make sure that the problem was fixed.  The relevant part of the class look much better now, and more importantly they actually work.

calculate_cost_per_second(cost_per_hour: number, no_particpants: number) {
    return (cost_per_hour * no_particpants) / 3600.0;
  }

  calculate_current_cost_cents(
    cost_per_second: number,
    no_seconds_elapsed: number
  ) {
    return cost_per_second * no_seconds_elapsed * 100;
  }

  calculate_cost_display_string(
    total_cost_cents: number,
    currency_symbol: string
  ) {
    let dollars = Math.floor(total_cost_cents / 100);
    let cents = Math.floor(total_cost_cents - dollars * 100);
    let dollarString = dollars.toString();
    let centsString = cents.toString().padStart(2, "0");

    return currency_symbol + dollarString + "." + centsString;
  }


vitest seems to be another of the v-family of products that are built around Vue (vue, vuetify, vite,)  that just works - it integrates nicely with vite and is simple to learn quickly.  There really wasn't much excuse not to use it. With the calculations extracted into pure functions they become easy to test:


test("MeetingTimer - calculate_cost_per_second", () => {
  const timer = new MeetingTimer();

  const no_people = 100;
  const avg_cost_per_hour = 100;
  const num_seconds_per_hour = 60 * 60;
  const expected = (no_people * avg_cost_per_hour) / num_seconds_per_hour; //100 people at 100 avf cost per hour
  const actual = timer.calculate_cost_per_second(no_people, avg_cost_per_hour);

  expect(actual).toBe(expected);
});

Now that I know the site is fixed, I have redeployed it and it is back to doing its job - waiting for the chance to passive-aggressively shame time wasters. 
 
I think the key take away lesson for this is that I need to be more disciplined about reviewing and testing code, and that not to let the sugar rush of getting something 'done'  overwhelm the need to do things 'right'.

Raspberry Pi Desk Clock

I have a raspberry pi 5 in one of the original touch screen v1 official 7 inch display case.  It is always on my desk but is normally powere...