Code vs. Cells

Why I've Ditched Excel and Google Sheets and instead built small domain models.

Code vs. Cells

Motivation

I like Domain-driven design, and seeing how to utilize it. A fun exercise is trying to model everyday problems while building them test driven. I've started doing that with simple problems I would previously have solved with a spreadsheet (i.e. Google Sheets). It's great for learning, and I've realized I get a better result.

Calculating velocity

I have on multiple occasions acted as Scrum Master (I'd argue I haven't run scrum, nor that I want to), and the question of velocity comes up. I've previously created excel sheets where each sheet was a sprint, and then I had a "front page" with the summarized calculations. It was a mess when I suddenly wanted to introduce a new datapoint, or reformat a given page - as references to specific cells would suddenly be broken. It was a mess.

Instead, I created Sprinthon. It's a very simple Python tool - Utilizing Pydantic I was able to fletch out the domain model rather quickly. A sprint simply consists of:

from pydantic import BaseModel, validator

class SprintScope(BaseModel):
    initial: Optional[StoryPoints]
    final: Optional[StoryPoints]
    burned: Optional[StoryPoints]

    @property
    def growth(self) -> Goal:
        if self.final and self.initial:
            return self.final - self.initial

        return None

    @property
    def spillover(self) -> Goal:
        if self.final and self.burned:
            return self.final - self.burned

        return None


class Sprint(BaseModel):
    number: int
    scope: Optional[SprintScope] = None

And suddenly I had a list of sprints:

sprints = [
    Sprint(
        number=2,
        scope=SprintScope(
            initial=50,
            final=55,
            burned=45,
        ),
        workdays=5,
    ),
    Sprint(
        number=1,
        scope=SprintScope(
            initial=60,
            final=61,
            burned=55,
        ),
        workdays=5,
    ),
]

And getting any sort of statistics out of this is pretty trivial. The average burned story points are pretty easy to get:

avg_burned = avg(sprint.scope.burned for sprint in sprints if sprint.scope),

One could easily make it a Rolling average or something more complex if wanted.

Furthermore, I realized I'd want to add working_days to the model after we had a sprint that was partly swallowed by vacations. Easy to add to the model:

class Sprint(BaseModel):
    number: int
    workdays: float | None = None
    scope: Optional[SprintScope] = None

    def get_daily_velocity(self) -> float | None:
        if self.workdays is None or self.scope is None or self.scope.burned is None:
            return None

        return round(self.scope.burned / self.workdays, 2)

By allowing optional fields, I can easily enforce various backward and future-proofing, and having an IDE, I can easily update the sprints that don't conform. By writing a few unit tests it is easy to validate that my results are what I expect:

def test_has_daily_velocity(some_sprint: Sprint):
    some_sprint.scope.burned = 10
    some_sprint.workdays = 5

    assert some_sprint.get_daily_velocity() == 2

I ended up extending it to create a MarkdownWriter which means I can export a report of the sprints, which I can convert to PDF or whatever I'd want. That would have been a lot more work in a spreadsheet.

Conclusion

I often forget how easy it is to write code. I often forget how much I enjoy writing code. By using scripting languages (I also wrote a similar tool for another purpose in F#), I can easily help automate the boring stuff. It might be what Automate The Boring Stuff With Python argues as well. But it's quite neat - and by making micro tools they might grow into an open-source package that can help others.

Worst case, you've solved your problem and improved your coding and problem-solving skills in the process.

Did you find this article valuable?

Support Casper Weiss Bang by becoming a sponsor. Any amount is appreciated!